Mybatis-Plus
MyBatis-Plus和MyBatis是共生的,而非替代品。MyBatis-Plus只做增强不做改变,引入它不会对现有工程产生影响。其会在启动时自动注入基本CRUD(增删改查),性能基本无损耗,直接面向对象操作,使得开发人员能够更加方便地进行数据库操作。
官网地址:Mybatis-Plus
参考文章:Mybtis和Mybatis-Plus区别、MyBatisPlus基础
推荐文章:MyBatis Plus 讲解
Mybatis-Plus 简介
MP架构

MP的特性
- 无侵入:只做增强不做改变,不会对现有工程产生影响
 - 强大的 CRUD 操作:内置通用 Mapper,少量配置即可实现单表CRUD 操作
 - 支持 Lambda:编写查询条件无需担心字段写错
 - 支持主键五种自动生成策略
 - 内置分页插件、代码生成器、全局拦截插件、sql注入剥离器(支持sql注入剥离,防止SQL注入攻击)
 
快速入门
导入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包
 
加MP的相关配置信息
resources默认生成的是properties配置文件,可以将其替换成yml文件,并在文件中配置数据库连接的相关信息:
application.yml1
2
3
4
5
6
7
8spring:
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
7spring:
datasource:
druid:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/ssm_db?serverTimezone=UTC
username: root
password: rootmp在实体类的注解:
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方法
// 一般数据库表名tbl_user,这里注解@TableName("tbl_user"),就可以对应上表名
// 毕竟mp的语句里没有指定表名,都是在数据库中搜索和首字母小的的实体类名对应的表的。
public class User {
// 设置主键自增策略为auto,mp默认自增策略是ASSIGN_ID,分布式、雪花算法。自增策略也可以在yml中全局配置。
private Long id;
private String name;
// value属性起别名,select设置该字段是否参与查询,针对于一些密码等隐私数据不希望被查出来
private String password;
private Integer age;
private String tel;
// exist属性设置是否在数据库中存在该字段
private String online;
// 乐观锁注解版本,需要搭配乐观拦截器
private Integer version;
// 逻辑删除,本质是更新,数据库内该字段默认是0,通过标记为1来判定删除。
private Integer delete;
}Lombok常见的注解有:
@Setter:为模型类的属性提供setter方法@Getter:为模型类的属性提供getter方法@ToString:为模型类的属性提供toString方法@EqualsAndHashCode:为模型类的属性提供equals和hashcode方法@Data:是个组合注解,包含Setter、Getter、ToString、EqualsAndHashCode@NoArgsConstructor:提供一个无参构造函数@AllArgsConstructor:提供一个包含所有参数的构造函数**创建Dao接口,继承 基础mapper:BaseMapper
**。继承之后dao类多出了很多方法如selectById…… 编写引导类
1
2
3
4
5
6
7
//@MapperScan("com.itheima.dao")
public class Mybatisplus01QuickstartApplication {
public static void main(String[] args) {
SpringApplication.run(Mybatisplus01QuickstartApplication.class, args);
}
}Dao接口要想被容器扫描到,有两种解决方案:
- 在Dao接口上添加@Mapper注解,并且确保Dao处在引导类所在包或其子包中。需要在每一Dao接口中添加注解
 - 在引导类上添加@MapperScan注解,其属性为所要扫描的Dao所在包。只需要写一次,则指定包下的所有Dao接口都能被扫描到,@Mapper就可以不写。
 
userDao注入的时候下面有红线提示的原因是什么?
UserDao是一个接口,不能实例化对象,只有在服务器启动IOC容器初始化后,由框架创建DAO接口的代理对象来注入。服务器并未启动,代理对象也未创建,IDEA查找不到对应的对象注入就会提示报红。一旦服务启动,就能注入其代理对象,故该提示不影响正常运行。
跟之前整合MyBatis相比,不需要在DAO接口中编写方法和SQL语句了,只需要继承BaseMapper接口即可。整体来说简化很多。
DQL编程控制
条件查询的Wrapper包装器类
MyBatisPlus将书写复杂的SQL查询条件进行了封装,使用编程的形式完成查询条件的组合。包装器Wrapper
- 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 | 
构建条件查询
查询包装器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 < ?)
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的时候泛型不能省。
LambdaQueryWrapper(推荐)
1
2
3
4LambdaQueryWrapper<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  | LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<User>();  | 
条件查询null值处理
针对问题:区间条件查询,例如某宝筛选价格范围,用户只填最高价,最低价为null不算在查询条件里。
解决办法:新建实体类继承原实体类,多出属性范围,使用lqw.lt(null!=uq.getAge2(),User::getAge, uq.getAge2());判定。
后台如果想接收前端的两个数据,该如何接收?
一般使用的参数类只有一个age属性,接收页面上的两个值有两个解决方案:
添加属性age2,这种做法可以但是会影响到原模型类的属性内容(不推荐)
在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()**。
使用Lambda(推荐)
1
2
3LambdaQueryWrapper<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
不用Lambda(不推荐)
1
2
3QueryWrapper<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  | QueryWrapper<User> lqw = new QueryWrapper<User>();  | 
分组查询(group by)
注意:分组查询一定是要配合聚合函数的
- 聚合与分组查询,无法使用lambda表达式来完成
 - MP只是对MyBatis的增强,如果MP实现不了,可以直接在DAO接口中使用MyBatis的方式实现
 
1  | QueryWrapper<User> lqw = new QueryWrapper<User>();  | 
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:按照多个字段进行排序
 
 
 - orderBy排序
 
MP的查询结果形式:
- selectList:查询结果为多个或者单个
 - **selectOne:**查询结果为单个
 
1  | // 1.等值查询  | 
映射匹配兼容性
属性起别名、可见性、权限、表明和实体类名不匹配。
问题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  | mybatis-plus:  | 
@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。
 
AUTO策略:数据库默认自增策略,使用该策略的时候一定要确保对应的数据库表设置了ID主键自增,否则无效。
1
// 给实体类id属性,设置生成策略为AUTO
设置生成策略为INPUT,这种ID生成策略,需要将表的自增策略删除掉,添加数据手动设置ID。
1
2
3
4
5
private Long id;
// 使用 设置主键ID的值
User user = new User();
user.setId(666L);设置生成策略为ASSIGN_ID,这种生成策略,不需要手动设置ID,如果手动设置ID,则会使用自己设置的值。
1
设置生成策略为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  | mybatis-plus:  | 
配置方法设置数据库表与模型类的映射关系
MP会默认将模型类的类名名首字母小写作为表名使用,假如数据库表的名称都以tbl_开头,那么我们就需要将所有的模型类上添加**@TableName**,简化方式为在配置文件中配置如下内容:
1  | mybatis-plus:  | 
批量操作
根据传入的ID集合批量操作
1  | int deleteBatchIds( Collection<? extends Serializable> idList);  | 
逻辑删除,@TableLogic
- 物理删除:业务数据从数据库中丢弃,执行的是delete操作。
 - 逻辑删除:为数据设置是否可用状态字段,删除时设置状态字段为不可用状态,数据保留在数据库中,执行的是update操作。
 
修改数据库表添加
deleted列实体类设置逻辑删除成员
单个实体类注解
@TableLogic标识新增的字段为逻辑删除字段,使用注解
@TableLogic,value属性是默认值,delval是删除后修改的值1
2
private Integer deleted;yml全局配置
1
2
3
4
5
6
7
8
9mybatis-plus:
global-config:
db-config:
#逻辑删除字段名
logic-delete-field: deleted
#逻辑删除字面值:未删除为0
logic-not-delete-value: 0
#逻辑删除字面值:删除为1
logic-delete-value: 1
执行查询操作默认给sql语句后面加where deleted=0
MP的逻辑删除会将所有的查询都添加一个未被删除的条件,也就是已经被删除的数据是不应该被查询出来的。
@TableLogic
| 名称 | @TableLogic | 
|---|---|
| 类型 | 属性注解 | 
| 位置 | 模型类中用于表示删除字段的属性定义上方 | 
| 作用 | 标识该字段为进行逻辑删除的字段 | 
| 相关属性 | value:逻辑未删除值 delval:逻辑删除值  | 
乐观锁
乐观锁通过版本号控制事务的并发。
- 给数据加版本号
 - 保存数据时判断版本号是否取出时的版本,如果是则说明数据没有改动,直接保存并且版本号加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。列名可以任意,比如使用
version,给列设置默认值为1在模型类中添加对应的属性,@Version。根据添加的字段列名,在模型类中添加对应的属性值version,并注解@Version
添加乐观锁的拦截器
1
2
3
4
5
6
7
8
9
10
11
12
13
public class MpConfig {
public MybatisPlusInterceptor mpInterceptor() {
// 1.定义Mp拦截器
MybatisPlusInterceptor mpInterceptor = new MybatisPlusInterceptor();
// 2.添加乐观锁拦截器
mpInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
// 分页拦截器
//mpInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return mpInterceptor;
}
}先查询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.
快速开发
代码生成器实现
创建一个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
<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>编写引导类
1
2
3
4
5
6
public class MybatisplusGeneratorApplication {
public static void main(String[] args) {
SpringApplication.run(MybatisplusGeneratorApplication.class, args);
}
}创建代码生成类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
42public 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();
}
}可以参考代码生成器
运行生成器类的main方法,运行成功后会在当前项目中生成很多代码,代码包含
controller,service,mapper和entity
MP中Service的CRUD
IService和ServiceImpl<UserDao, User> 
1  | public interface UserService{  | 
MP提供了一个Service接口和实现类,分别是:**IService和ServiceImpl**,后者是对前者的一个具体实现。自定义Service可修改为:
1  | public interface UserService extends IService<User>{}  | 
修改以后的好处是,MP已经帮我们把业务层的一些基础的增删改查都已经实现了,可以直接进行使用。
在MP封装的Service层都有哪些方法可以用,可以查看官方文档持久层接口,这些提供的方法大家可以参考官方文档进行学习使用,方法的名称可能有些变化,但是方法对应的参数和返回值基本类似。
ActiveRecord简介
概念
ActiveRecord属于ORM(对象关系映射)层,由Rails最早提出,遵循标准的ORM模型:表映射到记录,记录映射到对象,字段映射到对象属性。配合遵循的命名和配置惯例,能够很大程度的快速实现模型的操作,而且简洁易懂。
主要思想是:
- 每一个数据库表对应创建一个类,类的每一个对象实例对应于数据库中表的一行记录;通常表的每个字段在类中都有相应的Field;
 - ActiveRecord同时负责把自己持久化,在ActiveRecord中封装了对数据库的访问,即CURD;
 - ActiveRecord是一种领域模型(Domain Model),封装了部分业务逻辑;
 
增删改查
在MP中,开启AR非常简单,只需要将**实体对象继承Model<实体类>**即可。
1  | class User extends Model<User>{  | 
注意:
- 之后增删改查就可以不用自动注入dao接口,直接实体类对象就能调用mp的各种方法。
 - 但dao还不能删除,因为ar底层还是dao。
 
查询
1  | 
  | 
新增
1  | User user = new User();  | 
删除
1  | User user = new User();  | 
修改
1  | User user = new User();  | 
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  | <!-- MyBatis 的 select 示例 -->  | 
MyBatis-Plus:
- 只需配置数据库连接、扫描 Mapper 包
 - 不需要写 XML,自动完成 SQL 拼接
 - 实体类字段与表字段名称一致可自动映射
 
1  | // 简洁调用  | 
CRUD 操作
MyBatis:
- 所有 CRUD 方法都需要手写
 - 如果实体类字段多,SQL 写起来冗长,易出错
 
MyBatis-Plus:
- 提供 
BaseMapper<T>,自动拥有 20+ 个通用 CRUD 方法 - 例如:
selectById,insert,updateById,deleteById- 批量插入、分页查询、条件查询等也支持
 
 
1  | // 新增  | 
- T:泛型,新增用来保存新增数据
 - int:返回值,新增成功后返回1,没有新增成功返回的是0
 
1  | // 根据 ID 删除  | 
Serializable:参数类型
思考:参数类型为什么是一个序列化类?
String和Number是Serializable的子类,Number又是Float,Double,Integer等类的父类,能作为主键的数据类型都已经是Serializable的子类,MP使用Serializable作为参数类型,就类似java中Object接收任何数据类型一样。
int:返回值类型,数据删除成功返回1,未删除数据返回0。
1  | // 根据 ID 选择修改  | 
- T:泛型,需要修改的数据内容,注意因为是根据ID进行修改,所以传入的对象中需要有ID属性值
 - int:返回值,修改成功后返回1,未修改数据返回0
 
根据条件更改:
1  | User user = new User();  | 
说明:修改的时候,只修改实体对象中有值的字段。
1  | // 根据 ID 查询  | 
- Serializable:参数类型,主键ID的值
 - T:根据ID查询只会返回一条数据
 
1  | // 查询所有  | 
- 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  | // 调用方法传入参数获取返回值  | 
在config包下创建分页拦截器类:分页拦截器类代码是通用的,可以直接复制粘贴。可以查看官方文档类配置
配置拦截器MP,并将其配置成Spring管理的bean对象即可。需要用到MybatisPlusInterceptor 对象的addInnerInterceptor方法。
1  | // 注解为配置类@Configuration,也可以在引导类@Import({MybatisPlusConfig.class})  | 
条件构造器
MyBatis:条件查询时需要手写 SQL 中的 where 子句
MyBatis-Plus:
- 提供 
QueryWrapper和LambdaQueryWrapper - 可链式编程、支持条件组合、动态拼接
 
1  | LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();  | 
代码生成
MyBatis:没有自带代码生成工具,通常需要借助 MyBatis Generator 插件,且配置复杂
MyBatis-Plus:
- 自带 
Code Generator,支持通过数据库自动生成:- 实体类、Mapper 接口、XML、Service、Controller 等
 
 
1  | // 快速生成一整套文件,只需配置数据库和模板路径  | 
主键策略
MyBatis:插入数据时需要手动指定主键或配置主键返回策略(如 useGeneratedKeys)
MyBatis-Plus:
- 支持多种主键生成方式:
- 自增、UUID、雪花算法(默认使用雪花)
 
 - 只需在实体类中加注解即可
 
1  | // 使用雪花算法生成主键  | 
乐观锁
MyBatis:需要手动实现版本字段逻辑和 SQL 拼接
MyBatis-Plus:提供内置插件支持乐观锁,自动对 version 字段进行比较和更新
1  | 
  | 
审计字段(如创建时间、修改时间)
MyBatis:需要手动在 insert/update 语句中维护 createTime, updateTime
MyBatis-Plus:
提供自动填充功能,支持插入/更新时自动填充字段
1
2
3
4
private LocalDateTime createTime;
private LocalDateTime updateTime;需要配置
MetaObjectHandler,一次性设置全局规则
性能分析
MyBatis:需要自己接入日志框架或 SQL 监控工具(如 P6Spy)
MyBatis-Plus:内置 SQL 执行分析插件(dev/test 环境下很实用)
1  | // 开启性能分析插件  | 
兼容性
MyBatis:灵活度高,自由度大,但需要手动控制 SQL
MyBatis-Plus:
- 100% 兼容原生 MyBatis
 - 可以只使用通用功能,特殊需求时照样写 XML、注解 SQL
 
使用场景建议
- MyBatis:适合 SQL 极度复杂、强定制化的项目,想完全控制 SQL。
 - MyBatis-Plus:适合大多数通用业务,追求开发效率、简洁的项目,尤其适合中后台管理系统。
 




