MyBatis-Plus和MyBatis是共生的,而非替代品。MyBatis-Plus只做增强不做改变,引入它不会对现有工程产生影响。其会在启动时自动注入基本CRUD(增删改查),性能基本无损耗,直接面向对象操作,使得开发人员能够更加方便地进行数据库操作。

官网地址:Mybatis-Plus

参考文章:Mybtis和Mybatis-Plus区别、MyBatisPlus基础

推荐文章:MyBatis Plus 讲解

Mybatis-Plus 简介

MP架构

image-20250731220225906

MP的特性

  • 无侵入:只做增强不做改变,不会对现有工程产生影响
  • 强大的 CRUD 操作:内置通用 Mapper,少量配置即可实现单表CRUD 操作
  • 支持 Lambda:编写查询条件无需担心字段写错
  • 支持主键五种自动生成策略
  • 内置分页插件、代码生成器、全局拦截插件、sql注入剥离器(支持sql注入剥离,防止SQL注入攻击)

快速入门

  1. 导入mybatis-plus-boot-starter依赖,dao继承BaseMapper<实体类名>

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.4.1</version>
    </dependency>
    <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.16</version>
    </dependency>

    说明:

    • druid数据源可以加也可以不加,SpringBoot有内置的数据源,可以配置成使用Druid数据源
    • 从MP的依赖关系可以看出,通过依赖传递已经将MyBatis与MyBatis整合Spring的jar包导入,我们不需要额外在添加MyBatis的相关jar包
  2. 加MP的相关配置信息

    resources默认生成的是properties配置文件,可以将其替换成yml文件,并在文件中配置数据库连接的相关信息:application.yml

    1
    2
    3
    4
    5
    6
    7
    8
    spring:
    datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    #serverTimezone是用来设置时区,UTC是标准时区,和咱们的时间差8小时,所以可以将其修改为Asia/Shanghai
    url: jdbc:mysql://localhost:3306/mybatisplus_db?serverTimezone=UTC
    username: root
    password: root

    注意:如果需要导入druid专有属性,就必须换依赖和配置方法了:pom.xml

    导入了druid-spring-boot-starter依赖,就不用再导入druid依赖了,它里面包含了与druid相关的配置超过200条以上

    1
    2
    3
    4
    5
    <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.2.6</version>
    </dependency>

    application.yaml

    1
    2
    3
    4
    5
    6
    7
    spring:
    datasource:
    druid:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/ssm_db?serverTimezone=UTC
    username: root
    password: root
  3. mp在实体类的注解:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    // 注解了lombok的@Data会自动生成getter,setter,toString方法
    @Data
    // 一般数据库表名tbl_user,这里注解@TableName("tbl_user"),就可以对应上表名
    // 毕竟mp的语句里没有指定表名,都是在数据库中搜索和首字母小的的实体类名对应的表的。
    @TableName("user")
    public class User {
    // 设置主键自增策略为auto,mp默认自增策略是ASSIGN_ID,分布式、雪花算法。自增策略也可以在yml中全局配置。
    @TableId(type = IdType.AUTO)
    private Long id;
    private String name;
    // value属性起别名,select设置该字段是否参与查询,针对于一些密码等隐私数据不希望被查出来
    @TableField(value = "password",select = false)
    private String password;
    private Integer age;
    private String tel;
    // exist属性设置是否在数据库中存在该字段
    @TableField(exist = false)
    private String online;
    // 乐观锁注解版本,需要搭配乐观拦截器
    @Version
    private Integer version;
    // 逻辑删除,本质是更新,数据库内该字段默认是0,通过标记为1来判定删除。
    @TableLogic
    private Integer delete;
    }

    Lombok常见的注解有:

    @Setter:为模型类的属性提供setter方法
    @Getter:为模型类的属性提供getter方法
    @ToString:为模型类的属性提供toString方法
    @EqualsAndHashCode:为模型类的属性提供equals和hashcode方法
    @Data:是个组合注解,包含Setter、Getter、ToString、EqualsAndHashCode
    @NoArgsConstructor:提供一个无参构造函数
    @AllArgsConstructor:提供一个包含所有参数的构造函数

  4. **创建Dao接口,继承 基础mapper:BaseMapper**。继承之后dao类多出了很多方法如selectById……

  5. 编写引导类

    1
    2
    3
    4
    5
    6
    7
    @SpringBootApplication
    //@MapperScan("com.itheima.dao")
    public class Mybatisplus01QuickstartApplication {
    public static void main(String[] args) {
    SpringApplication.run(Mybatisplus01QuickstartApplication.class, args);
    }
    }

    Dao接口要想被容器扫描到,有两种解决方案:

    1. 在Dao接口上添加@Mapper注解,并且确保Dao处在引导类所在包或其子包中。需要在每一Dao接口中添加注解
    2. 在引导类上添加@MapperScan注解,其属性为所要扫描的Dao所在包。只需要写一次,则指定包下的所有Dao接口都能被扫描到,@Mapper就可以不写。

userDao注入的时候下面有红线提示的原因是什么?

UserDao是一个接口,不能实例化对象,只有在服务器启动IOC容器初始化后,由框架创建DAO接口的代理对象来注入。服务器并未启动,代理对象也未创建,IDEA查找不到对应的对象注入就会提示报红。一旦服务启动,就能注入其代理对象,故该提示不影响正常运行。

跟之前整合MyBatis相比,不需要在DAO接口中编写方法和SQL语句了,只需要继承BaseMapper接口即可。整体来说简化很多。


DQL编程控制

条件查询的Wrapper包装器类

MyBatisPlus将书写复杂的SQL查询条件进行了封装,使用编程的形式完成查询条件的组合。包装器Wrapper是接口,实际开发中主要使用它的两个实现类:QueryWrapper和LambdaQueryWrapper。两种方式各有优劣:

  • QueryWrapper存在属性名写错的危险,但是支持聚合、分组查询;
  • LambdaQueryWrapper没有属性名写错的危险,但不支持聚合、分组查询;

基本比较操作

方法 说明 方法 说明 方法 说明
eq 等于 = ge 大于等于 >= notBetween NOT BETWEEN 值1 AND 值2
alleq 全部eq(或个别isNull) It 小于 < in 字段 IN (value.get(0), value.get(1), …)**
ne 不等于 <> le 小于等于 <= notIn 字段 NOT IN (v0, v1, …)
gt 大于 > between BETWEEN 值1 AND 值2

构建条件查询

  1. 查询包装器QueryWrapper(不建议)

    1
    2
    3
    4
    5
    // 创建QueryWrapper对象
    QueryWrapper qw = new QueryWrapper();
    //lt代表小于,大于是gt
    qw.lt("age",18); // 属性名写错就错了
    List<User> userList = userDao.selectList(qw);
    1
    SELECT id,name,password,age,tel FROM user WHERE (age < ?)
  2. QueryWrapper的基础上使用lambda(不建议)。可以防止数据库属性名写错。

    1
    2
    3
    4
    // 使用Lambda,QueryWrapper<User>必须加泛型
    QueryWrapper<User> qw = new QueryWrapper<User>();
    qw.lambda().lt(User::getAge, 10);// 添加条件,使用Lambda不容易写错属性名
    List<User> userList = userDao.selectList(qw);

    User::getAget,为lambda表达式中的,类名::方法名,最终的sql语句为:

    1
    SELECT id,name,password,age,tel FROM user WHERE (age < ?)

    注意:构建LambdaQueryWrapper的时候泛型不能省。

  3. LambdaQueryWrapper(推荐)

    1
    2
    3
    4
    LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<User>();
    lqw.lt(User::getAge, 30);
    lqw.gt(User::getAge, 10);
    List<User> userList = userDao.selectList(lqw);
    1
    SELECT id,name,password,age,tel FROM user WHERE (age < ? AND age > ?)

多条件构建,链式编程

构建多条件的时候,可以支持链式编程

1
2
3
4
5
6
7
LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<User>();
// 多条件查询默认是and,or要用.or()
lqw.lt(User::getAge, 30).gt(User::getAge, 10);
// lqw.lt(User::getAge, 30).or().gt(User::getAge, 10);
// SELECT id,name,password,age,tel FROM user WHERE (age < ? OR age > ?)
List<User> userList = userDao.selectList(lqw);
System.out.println(userList);

条件查询null值处理

针对问题:区间条件查询,例如某宝筛选价格范围,用户只填最高价,最低价为null不算在查询条件里。

解决办法:新建实体类继承原实体类,多出属性范围,使用lqw.lt(null!=uq.getAge2(),User::getAge, uq.getAge2());判定。

后台如果想接收前端的两个数据,该如何接收?

一般使用的参数类只有一个age属性,接收页面上的两个值有两个解决方案:

  1. 添加属性age2,这种做法可以但是会影响到原模型类的属性内容(不推荐)

  2. 在domain.query下新建一个模型类继承User类,并为其添加age2属性,UserQuery在拥有User属性和age2属性。(推荐)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 模拟页面传递过来的查询数据
    UserQuery uq = new UserQuery();
    uq.setAge(10);
    uq.setAge2(30);
    LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<User>();
    // lt(boolean condition, SFunction<User,?>column, Object val)
    // 第一个参数为判断条件condition为boolean类型,返回true,则添加条件,返回false则不添加条件
    lqw.lt(null!=uq.getAge2(), User::getAge, uq.getAge2());
    lqw.gt(null!=uq.getAge(), User::getAge, uq.getAge());
    List<User> userList = userDao.selectList(lqw);

查询投影

查询指定字段

查询投影:不查询所有字段,**只查询出指定字段的数据。查询指定字段lqw.select()**。

  1. 使用Lambda(推荐)

    1
    2
    3
    LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<User>();
    lqw.select(User::getId,User::getName,User::getAge);
    List<User> userList = userDao.selectList(lqw);

    select(…)方法用来设置查询的字段列,可以设置多个,最终的sql语句为:

    1
    SELECT id,name,age FROM user
  2. 不用Lambda(不推荐)

    1
    2
    3
    QueryWrapper<User> lqw = new QueryWrapper<User>();
    lqw.select("id","name","age","tel");
    List<User> userList = userDao.selectList(lqw);
    1
    SELECT id,name,age,tel FROM user

聚合查询

聚合查询不能用Lambda,只能用QueryWrapper。方法qw.select(“count(*) as count”);和userDao.selectMaps(qw)

聚合函数查询

count:总记录数,max:最大值,min:最小值,avg:平均值,sum:求和

1
2
3
4
5
6
7
8
9
10
11
12
QueryWrapper<User> lqw = new QueryWrapper<User>();
//lqw.select("count(*) as count");
//SELECT count(*) as count FROM user
//lqw.select("max(age) as maxAge");
//SELECT max(age) as maxAge FROM user
//lqw.select("min(age) as minAge");
//SELECT min(age) as minAge FROM user
//lqw.select("sum(age) as sumAge");
//SELECT sum(age) as sumAge FROM user
lqw.select("avg(age) as avgAge");
//SELECT avg(age) as avgAge FROM user
List<Map<String, Object>> userList = userDao.selectMaps(lqw);

分组查询(group by)

注意:分组查询一定是要配合聚合函数的

  • 聚合与分组查询,无法使用lambda表达式来完成
  • MP只是对MyBatis的增强,如果MP实现不了,可以直接在DAO接口中使用MyBatis的方式实现
1
2
3
4
QueryWrapper<User> lqw = new QueryWrapper<User>();
lqw.select("count(*) as count,tel");
lqw.groupBy("tel");
List<Map<String, Object>> list = userDao.selectMaps(lqw);

groupBy为分组,最终的sql语句为:

1
SELECT count(*) as count,tel FROM user GROUP BY tel

查询条件

除了 lt() 和 gt() 这两个方法外,MP的查询条件有很多,使用方法参见条件构造器

  • 等值匹配(eq):eq(): equal的缩小,相当于 =
  • 范围匹配(> 、 = 、between)
  • 模糊匹配(like):
    • like():前后加百分号,如 %J%
    • likeLeft():左边加百分号,如 %J
    • likeRight():后面加百分号,如 J%
  • 空判定(null)
  • 包含性匹配(in)
  • 分组(group)
  • 排序(order)
    • orderBy排序
      • condition:条件,true则添加排序,false则不添加排序
      • isAsc:是否为升序,true升序,false降序
      • columns:排序字段,可以有多个
    • **orderByAsc/Desc(单个column):**按照指定字段进行升序/降序
    • **orderByAsc/Desc(多个column):**按照多个字段进行升序/降序
    • orderByAsc/Desc
      • condition:条件,true添加排序,false不添加排序
      • 多个columns:按照多个字段进行排序

MP的查询结果形式:

  • selectList:查询结果为多个或者单个
  • **selectOne:**查询结果为单个
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 1.等值查询
LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<User>();
lqw.eq(User::getName, "Jerry").eq(User::getPassword, "jerry");
// SELECT id,name,password,age,tel FROM user WHERE (name = ? AND password = ?)
User loginUser = userDao.selectOne(lqw);
// 2.范围查询
lqw.between(User::getAge, 10, 30);
// SELECT id,name,password,age,tel FROM user WHERE (age BETWEEN ? AND ?)
List<User> userList = userDao.selectList(lqw);
// 3.模糊查询
lqw.likeLeft(User::getName, "J");
// SELECT id,name,password,age,tel FROM user WHERE (name LIKE ?)
List<User> userList = userDao.selectList(lqw);
// 4.排序查询
/**
* condition :条件,返回boolean,当condition为true,进行排序,如果为false,则不排序
* isAsc:是否为升序,true为升序,false为降序
* columns:需要操作的列
*/
lwq.orderBy(true,false, User::getId);
userDao.selectList(lwq);

映射匹配兼容性

属性起别名、可见性、权限、表明和实体类名不匹配。

问题1:表字段与编码属性设计不同步

表的列名和模型类的属性名发生不一致,就会导致数据封装不到模型对象

解决:@TableField字段注解里的value属性起别名

问题2:编码中添加了数据库中未定义的属性

当模型类中多了一个数据库表不存在的字段,就会导致生成的sql语句中在select的时候查询了数据库不存在的字段,程序运行就会报错,错误信息为:Unknown column ‘多出来的字段名称’ in ‘field list’

解决:@TableField注解里的exist属性设置其是否在数据库存在,设置为false则不存在,生成sql查询时,不会再查询该字段。

问题3:采用默认查询开放了更多的字段查看权限

查询表中所有的列的数据时可能把敏感数据查询到返回给前端,这时候就需要限制哪些字段默认不要进行查询。

解决:@TableField注解里的select属性,设置属性是否参与查询,该属性设置默认是否需要查询该字段的值,true(默认值)表示默认查询该字段,false表示默认不查询该字段。

问题4:表名与编码开发设计不同步

表的名称和模型类的名称不一致,导致查询失败,因为mybatisplus的语句中没有表名,不像sql能select * form xxx,只能根据实体类名和表明匹配。这个时候通常会报如下错误信息:Table ‘databaseName.tableNaem’ doesn’t exist,译为数据库中的表不存在。

解决方案一:是使用MP提供的另外一个注解@TableName来设置表与模型类之间的对应关系。

解决方法二:配置统一给实体类名加前缀:

1
2
3
4
mybatis-plus:
global-config:
db-config:
table-prefix: tbl_

@TableField

名称 @TableField
类型 属性注解
位置 模型类属性定义上方
作用 设置当前属性对应的数据库表中的字段关系
相关属性 value(默认):设置数据库表字段名称
exist:设置属性在数据库表字段中是否存在,默认为true,此属性不能与value合并使用
select:设置属性是否参与查询,此属性与select()映射配置不冲突

@TableName

名称 @TableName
类型 类注解
位置 模型类定义上方
作用 设置当前类对应于数据库表关系
相关属性 value(默认):设置数据库表名称

DML编程控制

id生成策略控制

前面我们在新增的时候留了一个问题,就是新增成功后,主键ID是一个很长串的内容,我们更想要的是按照数据库表字段进行自增长。

ID该如何选择:

  • 不同的表应用不同的id生成策略
    • 日志:自增(1,2,3,4,……)
    • 购物订单:特殊规则(FQ23948AK3843)
    • 外卖单:关联地区日期等信息(10 04 20200314 34 91)
    • 关系表:可省略id

@TableId

名称 @TableId
类型 属性注解
位置 模型类中用于表示主键的属性定义上方
作用 设置当前类中主键属性的生成策略
相关属性 value(默认):设置数据库表主键名称 type:设置主键属性的生成策略,值查照IdType的枚举值

五种id策略代码实现

概述:

  • AUTO策略:数据库默认自增策略

  • NONE: 不设置id生成策略

  • INPUT:用户手工输入id,如果id为null会报错

  • **ASSIGN_ID:雪花算法生成id(可兼容数值型与字符串型)**,mp默认id策略

  • ASSIGN_UUID:以UUID生成算法作为id生成策略

  • 其他的几个策略均已过时,都将被ASSIGN_ID和ASSIGN_UUID代替掉。

注意:除了INPUT策略,其他策略即使指定id为null会自动生成,不为null会用指定的id。

分布式ID是什么?

  • 当数据量足够大的时候,一台数据库服务器存储不下,这个时候就需要多台数据库服务器进行存储
  • 比如订单表就有可能被存储在不同的服务器上
  • 如果用数据库表的自增主键,因为在两台服务器上所以会出现冲突
  • 这个时候就需要一个全局唯一ID,这个ID就是分布式ID。
  1. AUTO策略:数据库默认自增策略,使用该策略的时候一定要确保对应的数据库表设置了ID主键自增,否则无效。

    1
    @TableId(type = IdType.AUTO) // 给实体类id属性,设置生成策略为AUTO
  2. 设置生成策略为INPUT,这种ID生成策略,需要将表的自增策略删除掉,添加数据手动设置ID。

    1
    2
    3
    4
    5
    @TableId(type = IdType.INPUT)
    private Long id;
    // 使用 设置主键ID的值
    User user = new User();
    user.setId(666L);
  3. 设置生成策略为ASSIGN_ID,这种生成策略,不需要手动设置ID,如果手动设置ID,则会使用自己设置的值。

    1
    @TableId(type = IdType.ASSIGN_ID)
  4. 设置生成策略为ASSIGN_UUID,主键的类型应该改成String,表字段类型设置为varchar,长度要大于32,因为UUID生成的主键为32位,如果长度小的话就会导致插入失败。

    1
       

    雪花算法(SnowFlake),是Twitter官方给出的算法实现 是用Scala写的。其生成的结果是一个64bit大小整数,它的结构如下图:

    • 1bit,不用,因为二进制中最高位是符号位,1表示负数,0表示正数。生成的id一般都是用整数,所以最高位固定为0。
    • 41bit-时间戳,用来记录时间戳,毫秒级
    • 10bit-工作机器id,用来记录工作机器id,其中高位5bit是数据中心ID其取值范围0-31,低位5bit是工作节点ID其取值范围0-31,两个组合起来最多可以容纳1024个节点
    • 序列号占用12bit,每个节点每毫秒0开始不断累加,最多可以累加到4095,一共可以产生4096个ID

ID生成策略对比

  • NONE: 不设置id生成策略,MP不自动生成,约等于INPUT,所以这两种方式都需要用户手动设置,但是手动设置第一个问题是容易出现相同的ID造成主键冲突,为了保证主键不冲突就需要做很多判定,实现起来比较复杂
  • **AUTO:**数据库ID自增,这种策略适合在数据库服务器只有1台的情况下使用,不可作为分布式ID使用
  • ASSIGN_UUID:可以在分布式的情况下使用,而且能够保证唯一,但是生成的主键是32位的字符串,长度过长占用空间而且还不能排序,查询性能也慢
  • ASSIGN_ID:可以在分布式的情况下使用,生成的是Long类型的数字,可以排序性能也高,但是生成的策略和服务器时间有关,如果修改了系统时间就有可能导致出现重复主键

配置方法设置id策略和实体类前缀tbl_

模型类主键策略设置

但是如果要在项目中的每一个模型类上都需要使用相同的生成策略,只需要在配置文件中添加如下内容,就能让所有的模型类都可以使用该主键ID策略:

1
2
3
4
mybatis-plus:
global-config:
db-config:
id-type: assign_id

配置方法设置数据库表与模型类的映射关系

MP会默认将模型类的类名名首字母小写作为表名使用,假如数据库表的名称都以tbl_开头,那么我们就需要将所有的模型类上添加**@TableName**,简化方式为在配置文件中配置如下内容:

1
2
3
4
mybatis-plus:
global-config:
db-config:
table-prefix: tbl_

批量操作

根据传入的ID集合批量操作

1
2
int deleteBatchIds(@Param(Constants.COLLECTION) Collection<? extends Serializable> idList);
List<T> selectBatchIds(@Param(Constants.COLLECTION) Collection<? extends Serializable> idList);

逻辑删除,@TableLogic

  • 物理删除:业务数据从数据库中丢弃,执行的是delete操作。
  • 逻辑删除:为数据设置是否可用状态字段,删除时设置状态字段为不可用状态,数据保留在数据库中,执行的是update操作。
  1. 修改数据库表添加deleted列

  2. 实体类设置逻辑删除成员

    1. 单个实体类注解@TableLogic

      标识新增的字段为逻辑删除字段,使用注解@TableLogic,value属性是默认值,delval是删除后修改的值

      1
      2
      @TableLogic(value="0",delval="1")
      private Integer deleted;
    2. yml全局配置

      1
      2
      3
      4
      5
      6
      7
      8
      9
      mybatis-plus:
      global-config:
      db-config:
      #逻辑删除字段名
      logic-delete-field: deleted
      #逻辑删除字面值:未删除为0
      logic-not-delete-value: 0
      #逻辑删除字面值:删除为1
      logic-delete-value: 1
  3. 执行查询操作默认给sql语句后面加where deleted=0

    MP的逻辑删除会将所有的查询都添加一个未被删除的条件,也就是已经被删除的数据是不应该被查询出来的。

@TableLogic

名称 @TableLogic
类型 属性注解
位置 模型类中用于表示删除字段的属性定义上方
作用 标识该字段为进行逻辑删除的字段
相关属性 value:逻辑未删除值
delval:逻辑删除值

乐观锁

乐观锁通过版本号控制事务的并发。

  1. 给数据加版本号
  2. 保存数据时判断版本号是否取出时的版本,如果是则说明数据没有改动,直接保存并且版本号加1,否则回退。

悲观锁:就是锁定你想要使用的资源,其他请求想要使用这个资源就必须排队,等这个资源释放了才能继续。

乐观锁使用案例:

业务并发现象带来的问题:秒杀。假如有100个商品或者票在出售,为了能保证每个商品或者票只能被一个人购买,如何保证不会出现超买或者重复卖。

  • 第一个想到的就是锁,锁在一台服务器中是可以解决的,但是如果在多台服务器下锁就没有办法控制,比如12306有两台服务器在进行卖票,在两台服务器上都添加锁的话,那也有可能会导致在同一时刻有两个线程在进行卖票,还是会出现并发问题
  • 接下来介绍的这种方式是针对于小型企业的解决方案,因为数据库本身的性能就是个瓶颈,如果对其并发量超过2000以上的就需要考虑其他的解决方案了。

实现思路

  • 数据库表中添加version列,比如默认值给1
  • 第一个线程要修改数据之前,取出记录时,获取当前数据库中的version=1
  • 第二个线程要修改数据之前,取出记录时,获取当前数据库中的version=1
  • 第一个线程执行更新时,set version = newVersion where version = oldVersion
    • newVersion = version+1 [2]
    • oldVersion = version [1]
  • 第二个线程执行更新时,set version = newVersion where version = oldVersion
    • newVersion = version+1 [2]
    • oldVersion = version [1]
  • 假如这两个线程都来更新数据,第一个和第二个线程都可能先执行
    • 假如第一个线程先执行更新,乐观锁发现version依然为1,就把version改为2,第二个线程再更新的时候,set version = 2 where version = 1,此时乐观锁发现数据库表version已经为2,所以第二个线程会修改失败
    • 假如第二个线程先执行更新,会把version改为2,第一个线程再更新的时候,set version = 2 where version = 1,此时数据库表的数据version已经为2,所以第一个线程会修改失败
    • 不管谁先执行都会确保只能有一个线程更新数据,这就是MP提供的乐观锁的实现原理分析。

代码实现

  1. 数据库表添加列,默认值1。列名可以任意,比如使用version,给列设置默认值为1

  2. 在模型类中添加对应的属性,@Version。根据添加的字段列名,在模型类中添加对应的属性值version,并注解@Version

  3. 添加乐观锁的拦截器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @Configuration
    public class MpConfig {
    @Bean
    public MybatisPlusInterceptor mpInterceptor() {
    // 1.定义Mp拦截器
    MybatisPlusInterceptor mpInterceptor = new MybatisPlusInterceptor();
    // 2.添加乐观锁拦截器
    mpInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
    // 分页拦截器
    //mpInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
    return mpInterceptor;
    }
    }
  4. 先查询version再更新操作

    要想实现乐观锁,首先第一步应该是拿到表中的version,然后拿version当条件在将version加1更新回到数据库表中,所以我们在修改的时候,需要对其进行查询

    1
    2
    3
    4
    5
    // 1.先通过要修改的数据id将当前数据查询出来
    User user = userDao.selectById(3L);
    // 2.将要修改的属性逐一设置进去
    user.setName("Jock888");
    userDao.updateById(user);

注意:必须先查询再修改,修改后系统自动给version属性加1。手动加和不查不加都是步行的。如果手动加version,提交后系统还会加一次1.


快速开发

代码生成器实现

  1. 创建一个Maven项目,导入对应的jar包mybatis-plus,druid,lombok,代码生成器mybatis-plus-generator和模板引擎velocity-engine-core依赖。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.5.1</version>
    </parent>
    <groupId>com.itheima</groupId>
    <artifactId>mybatisplus_04_generator</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <properties>
    <java.version>1.8</java.version>
    </properties>
    <dependencies>
    <!--spring webmvc-->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!--mybatisplus-->
    <dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.4.1</version>
    </dependency>
    <!--druid-->
    <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.16</version>
    </dependency>
    <!--mysql-->
    <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
    </dependency>
    <!--test-->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
    </dependency>
    <!--lombok-->
    <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.12</version>
    </dependency>
    <!--代码生成器-->
    <dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-generator</artifactId>
    <version>3.4.1</version>
    </dependency>
    <!--velocity模板引擎-->
    <dependency>
    <groupId>org.apache.velocity</groupId>
    <artifactId>velocity-engine-core</artifactId>
    <version>2.3</version>
    </dependency>
    </dependencies>
    <build>
    <plugins>
    <plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    </plugin>
    </plugins>
    </build>
    </project>
  2. 编写引导类

    1
    2
    3
    4
    5
    6
    @SpringBootApplication
    public class MybatisplusGeneratorApplication {
    public static void main(String[] args) {
    SpringApplication.run(MybatisplusGeneratorApplication.class, args);
    }
    }
  3. 创建代码生成类CodeGenerator

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    public class CodeGenerator {
    public static void main(String[] args) {
    // 1.获取代码生成器的对象
    AutoGenerator autoGenerator = new AutoGenerator();
    // 创建DataSourceConfig 对象设置数据库相关配置
    DataSourceConfig dataSource = new DataSourceConfig();
    dataSource.setDriverName("com.mysql.cj.jdbc.Driver");
    dataSource.setUrl("jdbc:mysql://localhost:3306/mybatisplus_db?serverTimezone=UTC");
    dataSource.setUsername("root");
    dataSource.setPassword("root");
    autoGenerator.setDataSource(dataSource);
    // 设置全局配置
    GlobalConfig globalConfig = new GlobalConfig();
    globalConfig.setOutputDir(System.getProperty("user.dir")
    + "/mybatisplus_04_generator/src/main/java");
    // 设置代码生成位置
    globalConfig.setOpen(false); // 设置生成完毕后是否打开生成代码所在的目录
    globalConfig.setAuthor("黑马程序员"); // 设置作者
    globalConfig.setFileOverride(true); // 设置是否覆盖原始生成的文件
    globalConfig.setMapperName("%sDao"); // 设置数据层接口名,%s为占位符,指代模块名称
    globalConfig.setIdType(IdType.ASSIGN_ID); // 设置Id生成策略
    autoGenerator.setGlobalConfig(globalConfig);
    // 设置包名相关配置
    PackageConfig packageInfo = new PackageConfig();
    packageInfo.setParent("com.aaa"); // 设置生成的包名,与代码所在位置不冲突,二者叠加组成完整路径
    packageInfo.setEntity("domain"); // 设置实体类包名
    packageInfo.setMapper("dao"); // 设置数据层包名
    autoGenerator.setPackageInfo(packageInfo);
    // 策略设置
    StrategyConfig strategyConfig = new StrategyConfig();
    strategyConfig.setInclude("tbl_user"); //设置当前参与生成的表名,参数为可变参数
    strategyConfig.setTablePrefix("tbl_"); //设置数据库表的前缀名称,模块名 = 数据库表名-前缀名。
    // 如:User = tbl_user-tbl_
    strategyConfig.setRestControllerStyle(true); //设置是否启用Rest风格
    strategyConfig.setVersionFieldName("version"); //设置乐观锁字段名
    strategyConfig.setLogicDeleteFieldName("deleted"); //设置逻辑删除字段名
    strategyConfig.setEntityLombokModel(true); //设置是否启用lombok
    autoGenerator.setStrategy(strategyConfig);
    // 2.执行代码生成器
    autoGenerator.execute();
    }
    }

    可以参考代码生成器

  4. 运行生成器类的main方法,运行成功后会在当前项目中生成很多代码,代码包含controller,service,mapper和entity

MP中Service的CRUD

IService和ServiceImpl<UserDao, User>

1
2
3
4
5
6
7
8
9
10
11
public interface UserService{
public List<User> findAll();
}
@Service
public class UserServiceImpl implements UserService{
@Autowired
private UserDao userDao;
public List<User> findAll(){
return userDao.selectList(null);
}
}

MP提供了一个Service接口和实现类,分别是:**IService和ServiceImpl**,后者是对前者的一个具体实现。自定义Service可修改为:

1
2
3
public interface UserService extends IService<User>{}
@Service
public class UserServiceImpl extends ServiceImpl<UserDao, User> implements UserService{}

修改以后的好处是,MP已经帮我们把业务层的一些基础的增删改查都已经实现了,可以直接进行使用。

在MP封装的Service层都有哪些方法可以用,可以查看官方文档持久层接口,这些提供的方法大家可以参考官方文档进行学习使用,方法的名称可能有些变化,但是方法对应的参数和返回值基本类似。


ActiveRecord简介

概念

ActiveRecord属于ORM(对象关系映射)层,由Rails最早提出,遵循标准的ORM模型:表映射到记录,记录映射到对象,字段映射到对象属性。配合遵循的命名和配置惯例,能够很大程度的快速实现模型的操作,而且简洁易懂。

主要思想是:

  • 每一个数据库表对应创建一个类,类的每一个对象实例对应于数据库中表的一行记录;通常表的每个字段在类中都有相应的Field;
  • ActiveRecord同时负责把自己持久化,在ActiveRecord中封装了对数据库的访问,即CURD;
  • ActiveRecord是一种领域模型(Domain Model),封装了部分业务逻辑;

增删改查

在MP中,开启AR非常简单,只需要将**实体对象继承Model<实体类>**即可。

1
2
3
class User extends Model<User>{
// fields...
}

注意:

  • 之后增删改查就可以不用自动注入dao接口,直接实体类对象就能调用mp的各种方法。
  • 但dao还不能删除,因为ar底层还是dao。

查询

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
void test() {
User user = new User();
System.out.println(user.selectById(2L)); //或者给user设置id后user.selectById()
}
// 或者给user设置id后user.selectById()
User user = new User();
QueryWrapper<User> userQueryWrapper = new QueryWrapper<>();
userQueryWrapper.le("age","20");
List<User> users = user.selectList(userQueryWrapper);
for (User user1 : users) {
System.out.println(user1);
}

新增

1
2
3
4
5
6
7
8
User user = new User();
user.setName("刘备");
user.setAge(30);
user.setPassword("123456");
user.setUserName("liubei");
user.setEmail("liubei@itcast.cn");
boolean insert = user.insert();
System.out.println(insert);

删除

1
2
3
4
User user = new User();
user.setId(7L);
boolean delete = user.deleteById();
System.out.println(delete);

修改

1
2
3
4
5
User user = new User();
user.setId(8L);
user.setAge(35);
boolean update = user.updateById();
System.out.println(update);

Mybtis和Mybatis-Plus区别

MyBatis-Plus 是在 MyBatis 基础上增强的工具包,让开发更便捷、高效。MyBatis-Plus官网地址。

核心区别总结

特性 MyBatis MyBatis-Plus
配置复杂度 需要手写大量 XML 或注解 极简配置,自动生成 SQL
CRUD 操作 手写 Mapper 方法 + SQL 内置通用 CRUD 方法
分页功能 需要手写分页逻辑或第三方插件 内置分页插件,开箱即用
条件构造器 无,需要手写 where 条件 内置 Lambda 条件构造器
代码生成 无 提供代码生成器(Code Generator)
主键策略 需要手动配置 内置多种主键生成策略
乐观锁 自己实现 提供内置乐观锁插件
审计字段(如创建时间、修改时间) 自行维护 提供自动填充功能
性能分析 需要额外工具 内置 SQL 性能分析插件
兼容性 灵活、手动控制高 完全兼容 MyBatis,可随时退回

配置复杂度

MyBatis:

  • 需要创建 Mapper 接口 + Mapper.xml 文件
  • SQL 都需要手动写,配置较繁琐
  • 实体类、字段和表结构需要手动映射(resultMap 或 @Results)
1
2
3
4
<!-- MyBatis 的 select 示例 -->
<select id="selectById" resultType="User">
SELECT * FROM user WHERE id = #{id}
</select>

MyBatis-Plus:

  • 只需配置数据库连接、扫描 Mapper 包
  • 不需要写 XML,自动完成 SQL 拼接
  • 实体类字段与表字段名称一致可自动映射
1
2
// 简洁调用
User user = userMapper.selectById(1L);

CRUD 操作

MyBatis:

  • 所有 CRUD 方法都需要手写
  • 如果实体类字段多,SQL 写起来冗长,易出错

MyBatis-Plus:

  • 提供 BaseMapper<T>,自动拥有 20+ 个通用 CRUD 方法
  • 例如:
    • selectById, insert, updateById, deleteById
    • 批量插入、分页查询、条件查询等也支持
1
2
// 新增
int insert (T t)
  • T:泛型,新增用来保存新增数据
  • int:返回值,新增成功后返回1,没有新增成功返回的是0
1
2
3
4
5
6
7
8
9
// 根据 ID 删除
int deleteById(Serializable id);
// 根据多个ID 批量删除
int deleteBatchIds(@Param(Constants.COLLECTION) Collection<? extends Serializable> idList);
// 根据 columnMap 批量删除
int deleteByMap(@Param(Constants.COLUMN_MAP) Map<String, Object> columnMap);
// 不常用
// 根据 entity 条件,删除记录
//int delete(@Param(Constants.WRAPPER) Wrapper<T> wrapper);

Serializable:参数类型

  • 思考:参数类型为什么是一个序列化类?

    String和Number是Serializable的子类,Number又是Float,Double,Integer等类的父类,能作为主键的数据类型都已经是Serializable的子类,MP使用Serializable作为参数类型,就类似java中Object接收任何数据类型一样。

  • int:返回值类型,数据删除成功返回1,未删除数据返回0。

1
2
3
4
5
6
// 根据 ID 选择修改
boolean updateById(T t);
// 根据ID 批量更新
boolean updateBatchById(Collection<T> entityList);
// 根据 whereWrapper 条件,更新记录
boolean update(T updateEntity, Wrapper<T> whereWrapper);
  • T:泛型,需要修改的数据内容,注意因为是根据ID进行修改,所以传入的对象中需要有ID属性值
  • int:返回值,修改成功后返回1,未修改数据返回0

根据条件更改:

1
2
3
4
5
6
7
8
User user = new User();
user.setAge(22); //更新的字段
// 更新的条件
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("id", 6);
//执行更新操作
int result = this.userMapper.update(user, wrapper);
System.out.println("result = " + result);

说明:修改的时候,只修改实体对象中有值的字段。

1
2
3
4
5
6
7
8
// 根据 ID 查询
T getById(Serializable id);
// 根据 Wrapper,查询一条记录。结果集,如果是多个会抛出异常,随机取一条加上限制条件 wrapper.last("LIMIT 1")
T getOne(Wrapper<T> queryWrapper);
// 根据 Wrapper,查询一条记录
T getOne(Wrapper<T> queryWrapper, boolean throwEx);
// 根据 Wrapper,查询一条记录
Map<String, Object> getMap(Wrapper<T> queryWrapper);
  • Serializable:参数类型,主键ID的值
  • T:根据ID查询只会返回一条数据
1
2
// 查询所有
List<T> selectList(Wrapper<T> queryWrapper)
  • Wrapper:用来构建条件查询的条件,目前我们没有可直接传为Null
  • List:因为查询的是所有,所以返回的数据是一个集合

分页功能

MyBatis:

  • 需要自己拼接分页 SQL(LIMIT/OFFSET)
  • 或接入第三方分页插件如 PageHelper

MyBatis-Plus:

  • 提供 分页插件,配置一次后,使用非常方便
  • 统一使用 Page<T> 对象传参
1
IPage<T> selectPage(IPage<T> page, Wrapper<T> queryWrapper)
  • IPage:用来构建分页查询条件
  • Wrapper:译为包装器,封装器。用来构建条件查询的条件,目前我们没有可直接传为Null
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 调用方法传入参数获取返回值
Page<User> page = new Page<>(1, 10); // 页码1,每页10条
Page<User> result = userMapper.selectPage(page, null);
// 或者
@Override
public PageUtils queryPage(Map<String, Object> params, Long categoryId) {
String key = (String) params.get("key");
LambdaQueryWrapper<AttrGroupEntity> wrapper = new LambdaQueryWrapper<>();
// 先根据检索查
if(StringUtils.isNotEmpty(key)){
//wrapper.eq(AttrGroupEntity::getAttrGroupId,key).or().like(AttrGroupEntity::getAttrGroupName,key);//也可以
wrapper.and(
obj -> obj.eq(AttrGroupEntity::getAttrGroupId,key)
.or().like(AttrGroupEntity::getAttrGroupName,key)
);
}
if(categoryId!=0) {
wrapper.eq(AttrGroupEntity::getCatelogId,categoryId);
}
IPage<AttrGroupEntity> page = this.page(
new Query<AttrGroupEntity>().getPage(params),
wrapper
);
return new PageUtils(page);
}

在config包下创建分页拦截器类:分页拦截器类代码是通用的,可以直接复制粘贴。可以查看官方文档类配置

配置拦截器MP,并将其配置成Spring管理的bean对象即可。需要用到MybatisPlusInterceptor 对象的addInnerInterceptor方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 注解为配置类@Configuration,也可以在引导类@Import({MybatisPlusConfig.class})
@Configuration
public class MybatisPlusConfig {
// 被Spring容器管理
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
//1 创建mp拦截器对象MybatisPlusInterceptor
MybatisPlusInterceptor mpInterceptor=new MybatisPlusInterceptor();
//2 添加内置拦截器,参数为分页内置拦截器对象PaginationInnerInterceptor
mpInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return mpInterceptor;
}
}

条件构造器

MyBatis:条件查询时需要手写 SQL 中的 where 子句

MyBatis-Plus:

  • 提供 QueryWrapper 和 LambdaQueryWrapper
  • 可链式编程、支持条件组合、动态拼接
1
2
3
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getAge, 20).like(User::getName, "张");
List<User> users = userMapper.selectList(wrapper);

代码生成

MyBatis:没有自带代码生成工具,通常需要借助 MyBatis Generator 插件,且配置复杂

MyBatis-Plus:

  • 自带 Code Generator,支持通过数据库自动生成:
    • 实体类、Mapper 接口、XML、Service、Controller 等
1
2
3
4
// 快速生成一整套文件,只需配置数据库和模板路径
AutoGenerator generator = new AutoGenerator();
// 配置略...
generator.execute();

主键策略

MyBatis:插入数据时需要手动指定主键或配置主键返回策略(如 useGeneratedKeys)

MyBatis-Plus:

  • 支持多种主键生成方式:
    • 自增、UUID、雪花算法(默认使用雪花)
  • 只需在实体类中加注解即可
1
2
@TableId(type = IdType.ASSIGN_ID) // 使用雪花算法生成主键
private Long id;

乐观锁

MyBatis:需要手动实现版本字段逻辑和 SQL 拼接

MyBatis-Plus:提供内置插件支持乐观锁,自动对 version 字段进行比较和更新

1
2
@Version
private Integer version; // 更新时会自动加入 version=... 的判断

审计字段(如创建时间、修改时间)

MyBatis:需要手动在 insert/update 语句中维护 createTime, updateTime

MyBatis-Plus:

  • 提供自动填充功能,支持插入/更新时自动填充字段

    1
    2
    3
    4
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
  • 需要配置 MetaObjectHandler,一次性设置全局规则

性能分析

MyBatis:需要自己接入日志框架或 SQL 监控工具(如 P6Spy)

MyBatis-Plus:内置 SQL 执行分析插件(dev/test 环境下很实用)

1
2
// 开启性能分析插件
interceptor.addInnerInterceptor(new PerformanceInterceptor());

兼容性

MyBatis:灵活度高,自由度大,但需要手动控制 SQL

MyBatis-Plus:

  • 100% 兼容原生 MyBatis
  • 可以只使用通用功能,特殊需求时照样写 XML、注解 SQL

使用场景建议

  • MyBatis:适合 SQL 极度复杂、强定制化的项目,想完全控制 SQL。
  • MyBatis-Plus:适合大多数通用业务,追求开发效率、简洁的项目,尤其适合中后台管理系统。