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方法,运行成功后会在当前项目中生成很多代码,代码包含controllerservicemapperentity

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接口和实现类,分别是:**IServiceServiceImpl**,后者是对前者的一个具体实现。自定义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:

  • 提供 QueryWrapperLambdaQueryWrapper
  • 可链式编程、支持条件组合、动态拼接
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:适合大多数通用业务,追求开发效率、简洁的项目,尤其适合中后台管理系统。