MyBatisPlus学习(1)

MyBatisPlus是MyBatis的增强工具,在MyBatis的基础上只做增强不做改变,为简化开发、提高效率而生。

依赖引入:在Mybatis的基础上添加

<dependency>
     <groupId>com.baomidou</groupId>
     <artifactId>mybatis-plus-boot-starter</artifactId>
     <version>3.5.3.1</version>
</dependency>

基本使用

首先我们需要直到,MybatisPlus内置所有SQL都是不可见的,因此为了在后台能看到SQL的执行,我们需要配置日志。

我们在配置文件中配置:

#配置日志
mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

我们设定初始表:

public class User {
    Long id;
    String name;
    Integer age;
}

创建对应的UserMapper:

@Mapper
public interface UserMapper extends BaseMapper<User> {
}

这里我们直接继承MyBatisPlus提供的BaseMapper,泛型使用对应的Data,该继承内部封装了大部分我们需要的代码,这样我们就不需要在写UserMapper的逻辑了。

测试使用:

@Controller
public class TestController {
    @Autowired
    UserMapper userMapper;

    @RequestMapping("/test")
    @ResponseBody
    public void test(){
        User user=new User();
        user.setName("test");
        user.setAge(12);
        userMapper.insert(user);
    }
}

我们发现直到现在,我们都没去设定目标表,MybatisPlus会自动去设定目标表,具体为类的小写对应表,这里如果我们设定表名为users,就找不到了。

但MyBatisPlus自然也存在指定表的功能,同理,也可以指定字段名:

@Data
@TableName("users")  //对应的表名
public class User {
    @TableId("id")   //对应的主键
    Long id;
    @TableField("name")   //对应的字段
    String name;
    @TableField("age")
    Integer age;
}

我们执行insert,就成功插入,并且我们发现,MybatisPlus自动为我们添加了id:

自定义ID生成器

上面我们提到了MybatisPlus会自动为我们插入ID添加一个全局唯一ID。我们可以自定义ID生成

User中,我们给id字段添加了@TableId注解,内部可以设定主键生成的方法

自 3.3.0 开始,默认使用雪花算法+UUID(不含中划线)

//对应数据库中的主键(UUID、自增id、雪花算法、redis、zookeeper)
@TableId(type = IdType.ASSIGN_ID)   //对应的主键
    Long id;

我们打开IdType就能看到其他生成方法:

/**
 * 生成ID类型枚举类
 *
 * @author hubin
 * @since 2015-11-10
 */
@Getter
public enum IdType {
    /**
     * 数据库ID自增
     * <p>该类型请确保数据库设置了 ID自增 否则无效</p>
     */
    AUTO(0),
    /**
     * 该类型为未设置主键类型(注解里等于跟随全局,全局里约等于 INPUT)
     */
    NONE(1),
    /**
     * 用户输入ID
     * <p>该类型可以通过自己注册自动填充插件进行填充</p>
     */
    INPUT(2),
​
    /* 以下3种类型、只有当插入对象ID 为空,才自动填充。 */
    /**
     * 分配ID (主键类型为number或string),
     * 默认实现类 {@link com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator}(雪花算法)
     *
     * @since 3.3.0
     */
    ASSIGN_ID(3),
    /**
     * 分配UUID (主键类型为 string)
     * 默认实现类 {@link com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator}(UUID.replace("-",""))
     */
    ASSIGN_UUID(4);
​
    private final int key;
​
    IdType(int key) {
        this.key = key;
    }
} 

1. UUId

UUID(Universally Unique Identifier)的标准型式包含32个16进制数字,以连字号分为五段,形式为8-4-4-4-12的36个字符,示例:550e8400-e29b-41d4-a716-446655440000,到目前为止业界一共有5种方式生成UUID,详情见IETF发布的UUID规范

优点:

  • 性能非常高:本地生成,没有网络消耗。

缺点:

  1. 没有排序,无法保证趋势递增。
  2. UUID往往使用字符串存储,查询的效率比较低。
  3. 不易于存储:UUID太长,16字节128位,通常以36长度的字符串表示,很多场景不适用。
  4. 信息不安全:基于MAC地址生成UUID的算法可能会造成MAC地址泄露,这个漏洞曾被用于寻找梅丽莎病毒的制作者位置。
  5. ID作为主键时在特定的环境会存在一些问题,比如做DB主键的场景下,UUID就非常不适用:
    1. MySQL官方有明确的建议主键要尽量越短越好,36个字符长度的UUID不符合要求。
    2. 对MySQL索引不利:如果作为数据库主键,在InnoDB引擎下,UUID的无序性可能会引起数据位置频繁变动,严重影响性能。

2. 雪花算法

这种方案大致来说是一种以划分命名空间(UUID也算,由于比较常见,所以单独分析)来生成ID的一种算法,这种方案把64-bit分别划分成多段,分开来标示机器、时间等,比如在snowflake中的64-bit分别表示如下图(图片来自网络)所示:

41-bit的时间可以表示(1L<<41)/(1000L360024*365)=69年的时间,10-bit机器可以分别表示1024台机器。如果我们对IDC划分有需求,还可以将10-bit分5-bit给IDC,分5-bit给工作机器。这样就可以表示32个IDC,每个IDC下可以有32台机器,可以根据自身需求定义。12个自增序列号可以表示2^12个ID,理论上snowflake方案的QPS约为409.6w/s,这种分配方式可以保证在任何一个IDC的任何一台机器在任意毫秒内生成的ID都是不同的。

核心思想:

使用41bit作为毫秒数,10bit作为机器的ID(5个bit是数据中心,5个bit的机器ID),12bit作为毫秒内的流水号(意味着每个节点在每毫秒可以产生4096个ID),最后还有一个符号位,永远是0,。

优点:

  1. 毫秒数在高位,自增序列在低位,整个ID都是趋势递增的。
  2. 不依赖数据库等第三方系统,以服务的方式部署,稳定性更高,生成ID的性能也是非常高的。
  3. 可以根据自身业务特性分配bit位,非常灵活。

缺点:

  1. 强依赖机器时钟,如果机器上时钟回拨,会导致发号重复或者服务会处于不可用状态。

更新操作

例子:


public void testUpdate(){
    User user = new User();
    //可以通过条件自动拼接动态SQL
    user.setId(5L);
    user.setName("id:5,修改过后");
    //updateById 参数是一个对象!
    int i = userMapper.updateById(user);
    System.out.println(i);
}

需要注意的是,updateById内传入的不是id而是user对象

自动填充

在通常业务中,数据库中的某些配置需要一些默认值如时间更新,而MyBatisPlus也实现了这个功能。

我们可以在注解@TableField中设置填充方案:

@TableField(fill = FieldFill.INSERT)
private Data createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Data updateTime;

并自定义类实现MyMetaObjectHandler来处理这个注解:

@Slf4j
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
    //插入时的填充策略
    @Override
    public void insertFill(MetaObject metaObject) {
        log.info("start intsert fill ....");
        //strictInsertFill(MetaObject metaObject, String fieldName, Class<T> fieldType, E fieldVal)
        this.strictInsertFill(metaObject,"createTime", LocalDateTime.class,LocalDateTime.now());// 起始版本 3.3.0(推荐使用)
    }
​
    //更新时的填充策略
    @Override
    public void updateFill(MetaObject metaObject) {
        log.info("start update fill ....");
        this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); // 起始版本 3.3.0(推荐)
    }
}

这里的两个方法分别对应INSERT和INSERT_UPDATE两个使用。

之后在执行更新操作和插入操作时,就会自动填充了。

增删改查

1.通过id查询

User user = userMapper.selectById(1L);

2.根据id批量查询

List<User> users = userMapper.selectBatchIds(Arrays.asList(1, 2, 3));

3.条件查询

    HashMap<String,Object> map = new HashMap<>();
    //自定义查询
    map.put("name","小文");
    map.put("age",20);
    List<User> users = userMapper.selectByMap(map);

map类的参数(字段名,参数)会被MySQLPlus自动组合成查询条件

分页查询

MybatisPlus内置了分页查询插件其内置几个参数:

属性名类型默认值描述
overflowbooleanfalse溢出总页数后是否进行处理(默认不处理)
maxLimitLong单页分页条数限制(默认无限制)
dbTypeDbType数据库类型(根据类型获取应使用的分页方言)
dialectIDialect方言实现类

需要使用,我们要先配置拦截器组件:

/**
 * 注册插件
 */
@Bean
public MybatisPlusInterceptor paginationInterceptor() {
​
    MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
    // 添加分页插件
    PaginationInnerInterceptor pageInterceptor = new PaginationInnerInterceptor();
    // 设置请求的页面大于最大页后操作,true调回到首页,false继续请求。默认false
    pageInterceptor.setOverflow(false);
    // 单页分页条数限制,默认无限制
    pageInterceptor.setMaxLimit(500L);
    // 设置数据库类型
    pageInterceptor.setDbType(DbType.MYSQL);
​
    interceptor.addInnerInterceptor(pageInterceptor);
    return interceptor;
}

之后就可以进行分页查询:

public void testMybatisPlus_Page(){
    // 两个参数:current和size
    //current是当前页数,默认是1,从1开始,不是0。size是每一页的条数。
    Page<User> page = new Page<>(1, 4);
    userMapper.selectPage(page,null);
    page.getRecords().forEach(System.out::println);
}

除此之外,Page的方法还有很多:

//page的其他方法
System.out.println("当前页:" + page.getCurrent());
System.out.println("总页数:" + page.getPages());
System.out.println("记录数:" + page.getTotal());
System.out.println("是否有上一页:" + page.hasPrevious());
System.out.println("是否有下一页:" + page.hasNext());

删除操作

删除操作和查询类似:

userMapper.deleteById(4L);
userMapper.deleteBatchIds(Arrays.asList(1L,2L));
Map<String, Object> map = new HashMap<>();
map.put("name","xiaotian");
userMapper.deleteByMap(map);

逻辑删除

物理删除:从数据库中直接删除

逻辑删除:在数据库中没有被删除,而是通过一个变量来让它失效。 deleted=0 ==》deleted=1

首先我们在数据表中增加一个deleted字段,之后在实体类上添加@TableLogic注解

@TableLogic
Integer deleted;

配置配置项:

#配置日志
mybatis-plus:
  global-config:
    db-config:
      logic-delete-field: flag # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)
      logic-delete-value: 1 # 逻辑已删除值(默认为 1)
      logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)

这里可以通过注解指定逻辑删除字段,也可以在配置项中设置。

逻辑删除只对自动注入的SQL有效:

  1. 在更新和查找时会追加where忽略逻辑删除的数据
  2. 删除操作会转变为更新操作,进行逻辑删除

条件构造器

MyBatisPlus可以使用Wrapper,通过其构造复杂的SQL。

但需要注意的是,使用条件构造器会使得代码耦合性高,传输wrapper相当于controller用map接收值,后期维护极为困难。

具体使用看Wrapper的使用。

接口操作

MybatisPlus除了对Mapper封装了方便的模版,也对Service业务进行了模版设计,我们继承对应类:

@Service
public interface UserService extends IService<User> {
  	//除了继承模版,我们也可以把它当成普通Service添加自己需要的方法
}

内部提供了很多方便的方法,如我们希望批量插入:

List<User> users = List.of(new User("xxx"), new User("yyy"));
  	//预设方法中已经支持批量保存了,这相比我们直接用for效率高不少
service.saveBatch(users);

比如还有更加方便快捷的保存或更新方法,当数据不存在时插入,存在时则更新:

service.saveOrUpdate(new User("aaa"));