文章目录
前言
在使用Mybatis时,最先开始的事情就是实体类对应的增删改查代码的编写,而且还不能省略,你不知道下个需求需不需要这个方法。几乎每个表都需要编写一套最基本的增删改查方法,主要就是 DAO 接口和 mapper.xml 文件的编写,如果表中的字段进行了修改,那么实体类,mapper 文件甚至 DAO 接口都要进行修改, 这样比较麻烦。
虽然有 MyBatis Generator 这样的插件在,可以自动生成,但是会覆盖自定义的方法。有没有类似 JPA 那样不用编写sql语句的框架库呢?
有的,MyBatis-Plus [简称 MP)是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。MP 专注于单表,可以实现让你不用写SQL, 只需要简单配置就可以 CRUD 操作,从而节省大量时间。
MP可以实现单表操作不需要编写 sql 语句,但是如果多表 join 的话,还是需要自己编写 sql 语句。
MP的愿景是成为 MyBatis 最好的搭档,就像 魂斗罗中的 1P、2P,基友搭配,效率翻倍。
一、Pom依赖
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.2</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
二、简单使用
2.1 配置
application.yml
spring:
datasource:
driver-class-name: org.h2.Driver
url: jdbc:h2:mem:test
sql:
init:
platform: h2
# 项目启动后自动执行的DDL语句
schema-locations: classpath:db/schema.sql
# 项目启动后自动执行的DML语句
data-locations: classpath:db/data.sql
h2:
console:
enabled: true #开启web console功能
mybatis-plus:
#配置sql打印日志
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
database-id: h2
db/scheme.sql
DROP TABLE IF EXISTS `user_account`;
CREATE TABLE `user_account` ( id bigint, username varchar(30), age int, email varchar(50), primary key (id));
db/data.sql
INSERT INTO `user_account`(id, username, age, email) VALUES
(1, 'Eli', 18, 'Eli@example.com'),
(2, 'Jack', 10, 'Jack@example.com'),
(3, 'Tom', 28, 'Tom@example.com'),
(4, 'Sandy', 21, 'Sandy@example.com'),
(5, 'Billie', 24, 'Billie@example.com');
2.2 类
实体类 UserAccount
@Data
@Builder
//@TableName("user_account")
public class UserAccount {
private Long id;
private String username;
private Integer age;
private String email;
}
DAO 接口 UserAccountDao
@Mapper
public interface UserAccountDao extends BaseMapper<UserAccount> {
}
启动类
@SpringBootApplication
@MapperScan
public class MybatisplusdemoApplication {
public static void main(String[] args) {
SpringApplication.run(MybatisplusdemoApplication.class, args);
}
}
2.3 CRUD
@SpringBootTest
class MybatisplusdemoApplicationTests {
private static final Logger logger = LoggerFactory.getLogger(MybatisplusdemoApplicationTests.class);
@Autowired
private UserAccountDao userAccountDao;
@Test
void crudTest() {
UserAccount userAccount = UserAccount.builder().id(6L).username("张三").age(20).email("zs@163.com").build();
userAccountDao.insert(userAccount);
UserAccount zs = userAccountDao.selectOne(new LambdaQueryWrapper<UserAccount>().eq(UserAccount::getUsername, "张三"));
logger.info("{}", zs);
assertThat(zs, is(userAccount));
LambdaQueryWrapper<UserAccount> id6Wrapper = new LambdaQueryWrapper<UserAccount>().eq(UserAccount::getId, 6L);
LambdaQueryWrapper<UserAccount> id1Wrapper = new LambdaQueryWrapper<UserAccount>().eq(UserAccount::getId, 1L);
userAccount.setAge(30);
userAccountDao.updateById(userAccount);
assertThat(userAccountDao.selectOne(id6Wrapper).getAge(), is(30));
UserAccount update = UserAccount.builder().id(1L).username("李四").build();
userAccountDao.update(update, id1Wrapper);
assertThat(userAccountDao.selectOne(id1Wrapper).getUsername(), is("李四"));
assertThat(userAccountDao.selectOne(id1Wrapper).getEmail(), is(notNullValue()));
List<UserAccount> userAccounts = userAccountDao.selectList(null);
assertThat(userAccounts, is(hasSize(6)));
LambdaQueryWrapper<UserAccount> ageMoreThan20Wrapper = new LambdaQueryWrapper<UserAccount>().ge(UserAccount::getAge, 20);
List<UserAccount> userAccounts2 = userAccountDao.selectList(ageMoreThan20Wrapper);
assertThat(userAccounts2, is(hasSize(4)));
userAccountDao.deleteById(userAccount.getId());
assertThat(userAccountDao.selectOne(id6Wrapper), is(nullValue()));
}
}
三、高级特性
3.1 分页
分页需要加上分页插件
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return interceptor;
}
@Test
void pageTest() {
LambdaQueryWrapper<UserAccount> ageMoreThan20Wrapper = new LambdaQueryWrapper<UserAccount>()
.ge(UserAccount::getAge, 20)
.orderBy(true, true, UserAccount::getId);
Page<UserAccount> userAccountPage = userAccountDao.selectPage(Page.of(2, 2), ageMoreThan20Wrapper);
List<UserAccount> records = userAccountPage.getRecords();
assertThat(records, is(hasSize(1)));
}
3.2 逻辑删除
开启逻辑删除功能后,查找更新自动会排除掉已逻辑删除的字段. 如果想真实删除,可以使用原生 MyBatis 功能实现
mybatis-plus:
global-config:
db-config:
logic-delete-field: flag # 全局逻辑删除的实体字段名(since 3.3.0,可以不用在实体类字段上加上@TableLogic注解)
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
@Data
@Builder
//@TableName("user_account")
public class UserAccount {
private Long id;
private String username;
private Integer age;
private String email;
private Integer flag;
}
DROP TABLE IF EXISTS `user_account`;
CREATE TABLE `user_account` ( id bigint, username varchar(30), age int, email varchar(50), flag int default 0, primary key (id));
@Test
void logicDeleteTest() {
userAccountDao.deleteById(1L);
List<UserAccount> userAccounts = userAccountDao.selectList(null);
assertThat(userAccounts, hasSize(4));
}
3.3 密码加密
v3.3.2 开始支持,可以用来加密yml中数据库配置 url, username, password
spring:
datasource:
driver-class-name: org.h2.Driver
url: mpw:a9yJMQVA0N44amxF8sMVNKMo732YV7JfLJUuv8Ke82k=
# url: jdbc:h2:mem:test
jar 包启动需要加上参数 --mpw.key=5d1bbd7c53c59da2
@SpringBootTest(args = "--mpw.key=5d1bbd7c53c59da2")
class MybatisplusdemoApplicationTests {
@Test
void logicDeleteTest() {
userAccountDao.deleteById(1L);
List<UserAccount> userAccounts = userAccountDao.selectList(null);
assertThat(userAccounts, hasSize(4));
String randomKey = AES.generateRandomKey();
String encrypt = AES.encrypt("jdbc:h2:mem:test", randomKey);
logger.info("{}, {}", randomKey, encrypt);
}
}
3.4 自定义ID生成器
添加了 @TableId(type = IdType.ASSIGN_ID) 或者 ASSIGN_UUID 即可生成 ID
从 MybatisParameterHandler 源码可以看出
默认ASSIGN_ID使用https://gitee.com/yu120/sequence,ASSIGN_UUID 使用UUID(不含中划线)
也可以自定义生成器,需要实现 IdentifierGenerator 接口
@Data
@Builder
//@TableName("user_account")
public class UserAccount {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private String username;
private Integer age;
private String email;
private Integer flag;
}
@TableId(type = IdType.Auto) 可实现ID自增,需要在建表时指定,否则无效
db2 主键自增设置如下,实际操作时发现db2设置主键自增后,insert就不能insert id了
id bigint generated always as identity (INCREMENT BY +1)
3.5 自动填充
MP 支持类似更新时间,创建时间属性的自动填充
DROP TABLE IF EXISTS `user_account`;
CREATE TABLE `user_account` ( id bigint, username varchar(30), age int,
email varchar(50), flag int default 0, create_time datetime, update_time datetime, primary key (id));
@Component
public class DateTimeMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now()); // 起始版本 3.3.0(推荐使用)
}
@Override
public void updateFill(MetaObject metaObject) {
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); // 起始版本 3.3.0(推荐)
}
}
@Data
@Builder
//@TableName("user_account")
public class UserAccount {
private Long id;
private String username;
private Integer age;
private String email;
private Integer flag;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.UPDATE)
private LocalDateTime updateTime;
}
@Test
void fillTest() {
UserAccount userAccount = UserAccount.builder().id(6L).username("王五").age(25).email("12@qq.com").build();
userAccountDao.insert(userAccount);
logger.info("{}", userAccount);
assertThat(userAccount.getCreateTime(), is(notNullValue()));
userAccount.setAge(10);
userAccountDao.updateById(userAccount);
logger.info("{}", userAccount);
assertThat(userAccount.getUpdateTime(), is(notNullValue()));
}
四、源码探秘
本节只探究 MybatisPlus BaseMapper 方法实现和 LambdaQueryWrapper , 毕竟这两部分使用的最多,源码版本为最新版 v3.5.2
4.1 BaseMapper
在使用 MyBatisPlus 时,只需要将DAO接口继承 BaseMapper ,就可以使用框架自带的 CRUD 方法了,很是方便,所以想了解它是如何实现的。
众所周知,Mybatis 是通过解析XML文件(新版本也可用注解),用动态代理来实现接口中的方法,而·MyBatisPlus不需要xml和注解也可以直接使用,下面从源码开始调试探秘
-
从 MapperProxyFactory 开始,这是 MyBatis 动态代理类,猜测 MyBatisPlus 对 MyBatis 增强的话,一定会在生成代理类之前将 BaseMapper 中方法的 Sql 语句生成好
-
发现 MyBatisPlus 从 Mybatis 复制过来 MybatisMapperProxyFactory,发现 MybatisMapperRegistry , MybatisConfiguration 都继承的是 Mybatis 里的类,这几个类都是 MyBatis 流程中很重要的类,那了解 MyBatis 的话,就可以很好过一下这个流程
-
首先是 MybatisMapperRegistry.addMapper ,这里会将代理类工厂包装自定义 DAO 接口,然后添加到 knownMappers ,然后通过 MybatisMapperAnnotationBuilder.parse 开始解析
-
MybatisMapperAnnotationBuilder.parse 会解析 MyBatis 自带的一些注解,而 BaseMapper 自带的方法会通过 sql 注入的方式实现,在 parserInjector 这个方法处理,并且 parserInjector 这个方法默认调用了 DefaultSqlInjector.inspectInject 方法
-
DefaultSqlInjector.inspectInject 实际调用的是父类 AbstractSqlInjector.inspectInject 方法,会对DefaultSqlInjector.getMethodList 进行注入
-
通过AbstractMethod的各个子类(包含主键默认的话16个)进行注入,以 delete 为例,会生成xml脚本
-
最终会将生成的语句加入到 Mybatis 容器中
4.2 LambdaQueryWrapper
LambdaQueryWrapper 的父类的父类是 AbstractWrapper, 它用来构建 sql 的 where 部分, 具体如何构建详细信息可以查看 https://baomidou.com/pages/10c804/#abstractwrapper
下面是继承 BaseMapper 的 UserAccountDao 中 selectList 生成的 mybatis xml 脚本 ,可以看出 LambdaQueryWrapper 是如何作用于最终SQL生成
BaseMapper @Param(Constants.WRAPPER) Wrapper<T> queryWrapper
Constants 接口中 String WRAPPER = “ew”,下表的 ew 代表 Wrapper 类
<script>
<if test="ew != null and ew.sqlFirst != null">
${ew.sqlFirst}
</if>
SELECT
<choose>
<when test="ew != null and ew.sqlSelect != null">
${ew.sqlSelect}
</when>
<otherwise>id,username,age,email,flag,create_time,update_time</otherwise>
</choose>
FROM user_account
<where>
<choose>
<when test="ew != null">
<if test="ew.entity != null">
<if test="ew.entity.id != null">id=#{ew.entity.id}</if>
<if test="ew.entity['username'] != null"> AND username=#{ew.entity.username}</if>
<if test="ew.entity['age'] != null"> AND age=#{ew.entity.age}</if>
<if test="ew.entity['email'] != null"> AND email=#{ew.entity.email}</if>
<if test="ew.entity['createTime'] != null"> AND create_time=#{ew.entity.createTime}</if>
<if test="ew.entity['updateTime'] != null"> AND update_time=#{ew.entity.updateTime}</if>
</if>
AND flag=0
<if test="ew.sqlSegment != null and ew.sqlSegment != '' and ew.nonEmptyOfNormal">
AND ${ew.sqlSegment}
</if>
<if test="ew.sqlSegment != null and ew.sqlSegment != '' and ew.emptyOfNormal">
${ew.sqlSegment}
</if>
</when>
<otherwise>flag=0</otherwise>
</choose>
</where>
<if test="ew != null and ew.sqlComment != null">
${ew.sqlComment}
</if>
</script>