1、简介 {#1简介}
MyBatis 是一个流行的开源持久性框架,提供了 JDBC 和 Hibernate 的替代方案。
本文将带你了解 MyBatis 的一个扩展,名为 MyBatis-Plus,它具有许多方便的功能,可以大大地提高我们的开发效率。
2、MyBatis-Plus 整合 {#2mybatis-plus-整合}
2.1、Maven 依赖 {#21maven-依赖}
首先,在 pom.xml
中添加以下 Maven 依赖。
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.7</version>
</dependency>
最新版本的 Maven 依赖可在 此处 找到。由于这是基于 Spring Boot 3 的 Maven 依赖,我们还需要在 pom.xml
中添加 spring-boot-starter 依赖。
如果使用的是 Spring Boot 2,则需要添加以下依赖:
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.7</version>
</dependency>
接着,在 pom.xml
中添加 H2 内存数据库依赖,用于验证 MyBatis-Plus 的特性和功能。
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.3.230</version>
</dependency>
同样,H2 的最新版本可以在 这里 找到。除了 H2 外,我们还可以使用 MySQL 等其他关系型数据库。
2.2、Client {#22client}
依赖添加完毕后,创建 Client
实体,其中包含一些属性,如 id
、firstName
、lastName
和 email
:
@TableName("client")
public class Client {
@TableId(type = IdType.AUTO)
private Long id;
private String firstName;
private String lastName;
private String email;
// Getter / Setter 省略
}
如上,使用 MyBatis-Plus 的 @TableName
和 @TableId
注解实体类和 id
字段,与底层数据库中的 client
表创建映射。
2.3、ClientMapper {#23clientmapper}
然后,为 Client
实体创建 mapper 接口 ClientMapper
,它继承了 MyBatis-Plus 提供的 BaseMapper
接口:
@Mapper
public interface ClientMapper extends BaseMapper<Client> {
}
BaseMapper
接口为 CRUD 操作提供了大量默认方法,如 insert()
、selectOne()
、updateById()
、insertOrUpdate()
、deleteById()
和 deleteByIds()
。
2.4、ClientService {#24clientservice}
接下来,创建继承 IService
接口的 ClientService
接口:
public interface ClientService extends IService<Client> {
}
IService
接口封装了 CRUD 操作的默认实现,并使用 BaseMapper
接口提供简单且可维护的基本数据库操作。
2.5、ClientServiceImpl {#25clientserviceimpl}
最后,创建 ClientServiceImpl
类:
@Service
public class ClientServiceImpl extends ServiceImpl<ClientMapper, Client> implements ClientService {
@Autowired
private ClientMapper clientMapper;
}
它是 Client
实体的 Service 实现,注入了 ClientMapper
依赖。
3、CRUD 操作 {#3crud-操作}
3.1、创建 {#31创建}
使用 ClientService
接口来创建 Client
对象:
Client client = new Client();
client.setFirstName("Anshul");
client.setLastName("Bansal");
client.setEmail("anshul.bansal@example.com");
clientService.save(client); // 保存对象到数据库
assertNotNull(client.getId());
将 com.baeldung.mybatisplus
包的日志记录级别设置为 DEBUG
后,可以在保存 client
对象时看到以下日志记录:
16:07:57.404 [main] DEBUG c.b.m.mapper.ClientMapper.insert - ==> Preparing: INSERT INTO client ( first_name, last_name, email ) VALUES ( ?, ?, ? )
16:07:57.414 [main] DEBUG c.b.m.mapper.ClientMapper.insert - ==> Parameters: Anshul(String), Bansal(String), anshul.bansal@example.com(String)
16:07:57.415 [main] DEBUG c.b.m.mapper.ClientMapper.insert - <== Updates: 1
ClientMapper
接口生成的日志显示了 insert
SQL 的参数和最终插入数据库的行数。
3.2、读取 {#32读取}
接下来,看看一些方便的查询方法,如 getById()
和 list()
:
assertNotNull(clientService.getById(2));
assertEquals(6, clientService.list())
同样,我们可以在日志中看到以下 SELECT
语句:
16:07:57.423 [main] DEBUG c.b.m.mapper.ClientMapper.selectById - ==> Preparing: SELECT id,first_name,last_name,email,creation_date FROM client WHERE id=?
16:07:57.423 [main] DEBUG c.b.m.mapper.ClientMapper.selectById - ==> Parameters: 2(Long)
16:07:57.429 [main] DEBUG c.b.m.mapper.ClientMapper.selectById - <== Total: 1
16:07:57.437 [main] DEBUG c.b.m.mapper.ClientMapper.selectList - ==> Preparing: SELECT id,first_name,last_name,email FROM client
16:07:57.438 [main] DEBUG c.b.m.mapper.ClientMapper.selectList - ==> Parameters:
16:07:57.439 [main] DEBUG c.b.m.mapper.ClientMapper.selectList - <== Total: 6
此外,MyBatis-Plus 框架还提供了一些方便的 Wrapper 类,如 QueryWrapper
、LambdaQueryWrapper
和 QueryChainWrapper
:
Map<String, Object> map = Map.of("id", 2, "first_name", "Laxman");
QueryWrapper<Client> clientQueryWrapper = new QueryWrapper<>();
clientQueryWrapper.allEq(map);
assertNotNull(clientService.getBaseMapper().selectOne(clientQueryWrapper));
LambdaQueryWrapper<Client> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(Client::getId, 3);
assertNotNull(clientService.getBaseMapper().selectOne(lambdaQueryWrapper));
QueryChainWrapper<Client> queryChainWrapper = clientService.query();
queryChainWrapper.allEq(map);
assertNotNull(clientService.getBaseMapper().selectOne(queryChainWrapper.getWrapper()));
如上,使用 ClientService
接口的 getBaseMapper()
来获取 Wrapper 类,以更加直观地方式编写复杂的查询。
3.3、更新 {#33更新}
然后,来看看执行更新的几种方法:
Client client = clientService.getById(2);
client.setEmail("anshul.bansal@baeldung.com");
clientService.updateById(client);
assertEquals("anshul.bansal@baeldung.com", clientService.getById(2).getEmail());
控制台输出的日志如下:
16:07:57.440 [main] DEBUG c.b.m.mapper.ClientMapper.updateById - ==> Preparing: UPDATE client SET email=? WHERE id=?
16:07:57.441 [main] DEBUG c.b.m.mapper.ClientMapper.updateById - ==> Parameters: anshul.bansal@baeldung.com(String), 2(Long)
16:07:57.441 [main] DEBUG c.b.m.mapper.ClientMapper.updateById - <== Updates: 1
同样,我们可以使用 LambdaUpdateWrapper
类来更新 Client
对象:
LambdaUpdateWrapper<Client> lambdaUpdateWrapper = new LambdaUpdateWrapper<>();
lambdaUpdateWrapper.set(Client::getEmail, "x@e.com");
assertTrue(clientService.update(lambdaUpdateWrapper));
QueryWrapper<Client> clientQueryWrapper = new QueryWrapper<>();
clientQueryWrapper.allEq(Map.of("email", "x@e.com"));
assertThat(clientService.list(clientQueryWrapper).size()).isGreaterThan(5);
更新 client
对象后,使用 QueryWrapper
类来确认更新操作是否成功。
3.4、删除 {#34删除}
同样,我们可以使用 removeById()
或 removeByMap()
方法删除记录:
clientService.removeById(1);
assertNull(clientService.getById(1));
Map<String, Object> columnMap = new HashMap<>();
columnMap.put("email", "x@e.com");
clientService.removeByMap(columnMap);
assertEquals(0, clientService.list().size());
删除操作的日志如下:
21:55:12.938 [main] DEBUG c.b.m.mapper.ClientMapper.deleteById - ==> Preparing: DELETE FROM client WHERE id=?
21:55:12.938 [main] DEBUG c.b.m.mapper.ClientMapper.deleteById - ==> Parameters: 1(Long)
21:55:12.938 [main] DEBUG c.b.m.mapper.ClientMapper.deleteById - <== Updates: 1
21:57:14.278 [main] DEBUG c.b.m.mapper.ClientMapper.delete - ==> Preparing: DELETE FROM client WHERE (email = ?)
21:57:14.286 [main] DEBUG c.b.m.mapper.ClientMapper.delete - ==> Parameters: x@e.com(String)
21:57:14.287 [main] DEBUG c.b.m.mapper.ClientMapper.delete - <== Updates: 5
与更新日志类似,日志显示了 delete
查询的参数和从数据库中删除的总行数。
4、其他特性 {#4其他特性}
来看看 MyBatis-Plus 为 MyBatis 扩展的一些功能。
4.1、批处理 {#41批处理}
首先是批处理,可以批量执行常见的 CRUD 操作,从而提高性能和效率:
Client client2 = new Client();
client2.setFirstName("Harry");
Client client3 = new Client();
client3.setFirstName("Ron");
Client client4 = new Client();
client4.setFirstName("Hermione");
// 批创建
clientService.saveBatch(Arrays.asList(client2, client3, client4));
assertNotNull(client2.getId());
assertNotNull(client3.getId());
assertNotNull(client4.getId());
批量插入的日志如下:
16:07:57.419 [main] DEBUG c.b.m.mapper.ClientMapper.insert - ==> Preparing: INSERT INTO client ( first_name ) VALUES ( ? )
16:07:57.419 [main] DEBUG c.b.m.mapper.ClientMapper.insert - ==> Parameters: Harry(String)
16:07:57.421 [main] DEBUG c.b.m.mapper.ClientMapper.insert - ==> Parameters: Ron(String)
16:07:57.421 [main] DEBUG c.b.m.mapper.ClientMapper.insert - ==> Parameters: Hermione(String)
此外,还提供了 updateBatchById()
、saveOrUpdateBatch()
和 removeBatchByIds()
等方法,用于批量更新、批量保存或更新、批量删除。
4.2、分页查询 {#42分页查询}
MyBatis-Plus 框架提供了一种直观的分页查询方式。
我们只需要将 MyBatisPlusInterceptor
类声明为 Spring Bean,并添加数据库对应的 PaginationInnerInterceptor
拦截器类:
@Configuration
public class MyBatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.H2));
return interceptor;
}
}
然后,就可以使用 Page
类进行分页查询。例如,检索第 2 页,每页 3 条记录:
Page<Client> page = Page.of(2, 3);
clientService.page(page, null).getRecords();
assertEquals(3, clientService.page(page, null).getRecords().size());
分页查询的日志如下:
16:07:57.487 [main] DEBUG c.b.m.mapper.ClientMapper.selectList - ==> Preparing: SELECT id,first_name,last_name,email FROM client LIMIT ? OFFSET ?
16:07:57.487 [main] DEBUG c.b.m.mapper.ClientMapper.selectList - ==> Parameters: 3(Long), 3(Long)
16:07:57.488 [main] DEBUG c.b.m.mapper.ClientMapper.selectList - <== Total: 3
如上,日志显示了 select
查询,以及总记录数。
4.3、流式查询 {#43流式查询}
MyBatis-Plus 通过 selectList()
、selectByMap()
和 selectBatchIds()
等方法为流式查询提供了支持,用于高效处理大数据。
首先来看看 ClientService
接口提供的 selectList()
方法:
clientService.getBaseMapper()
.selectList(Wrappers.emptyWrapper(), resultContext ->
assertNotNull(resultContext.getResultObject()));
如上,使用 getResultObject()
方法从数据库中获取每一条记录。
同样,还有 getResultCount()
方法和 stop()
方法,前者用于返回正在处理的结果数,后者用于停止对结果集的处理。
4.4、自动填充 {#44自动填充}
MyBatis-Plus 还支持在 insert
和 update
操作时自动填充字段。
例如,可以使用 @TableField
注解在插入新记录时设置 creationDate
,在更新时设置 lastModifiedDate
:
public class Client {
// ...
@TableField(fill = FieldFill.INSERT)
private LocalDateTime creationDate;
@TableField(fill = FieldFill.UPDATE)
private LocalDateTime lastModifiedDate;
// Getter / Setter 省略
}
现在,MyBatis-Plus 会在每次 insert
和 update
查询时自动填充 creation_date
和 last_modified_date
列。
4.5、逻辑删除 {#45逻辑删除}
MyBatis-Plus 框架提供了一种简单高效的策略,通过在数据库中标记记录来实现逻辑删除。
我们可以通过在 deleted
属性上使用 @TableLogic
注解来启用该功能:
@TableName("client")
public class Client {
// ...
@TableLogic
private Integer deleted;
// Getter / Setter 省略
}
现在,框架会在执行数据库操作时自动处理逻辑删除的记录。
先根据 ID 删除 Client
对象,然后再根据 ID 检索:
clientService.removeById(harry);
assertNull(clientService.getById(harry.getId()));
输出日志如下,删除操作(本质上是 update
操作)将 deleted
属性的值设置为 1
,在检索的时候自动添加了 deleted=0
的条件,避免检索出逻辑删除了的记录:
15:38:41.955 [main] DEBUG c.b.m.mapper.ClientMapper.deleteById - ==> Preparing: UPDATE client SET last_modified_date=?, deleted=1 WHERE id=? AND deleted=0
15:38:41.955 [main] DEBUG c.b.m.mapper.ClientMapper.deleteById - ==> Parameters: null, 7(Long)
15:38:41.957 [main] DEBUG c.b.m.mapper.ClientMapper.deleteById - <== Updates: 1
15:38:41.957 [main] DEBUG c.b.m.mapper.ClientMapper.selectById - ==> Preparing: SELECT id,first_name,last_name,email,creation_date,last_modified_date,deleted FROM client WHERE id=? AND deleted=0
15:38:41.957 [main] DEBUG c.b.m.mapper.ClientMapper.selectById - ==> Parameters: 7(Long)
15:38:41.958 [main] DEBUG c.b.m.mapper.ClientMapper.selectById - <== Total: 0
此外,还可以通过 application.yml
修改默认配置:
mybatis-plus:
global-config:
db-config:
logic-delete-field: deleted # 控制逻辑删除的字段
logic-delete-value: 1 # 已删除状态的字段值
logic-not-delete-value: 0 # 未删除状态的字段值
通过上述配置,我们可以更改逻辑删除字段的名称以及对应的逻辑值。
4.6、代码生成 {#46代码生成}
MyBatis-Plus 提供自动代码生成功能,可避免手动创建实体、mapper 和 service 接口等冗余代码。
首先,在 pom.xml
中添加 最新版本的 mybatis-plus-generator
依赖:
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.5.7</version>
</dependency>
此外,还需要 Velocity 或 Freemarker 等模板引擎的支持。
然后,使用 MyBatis-Plus 的 FastAutoGenerator
类,将 FreemarkerTemplateEngine
类设置为模板引擎,连接底层数据库、扫描所有现有表并生成代码:
FastAutoGenerator.create("jdbc:h2:file:~/mybatisplus", "sa", "")
.globalConfig(builder -> {
builder.author("anshulbansal")
.outputDir("../tutorials/mybatis-plus/src/main/java/")
.disableOpenDir();
})
.packageConfig(builder -> builder.parent("com.baeldung.mybatisplus").service("ClientService"))
.templateEngine(new FreemarkerTemplateEngine())
.execute();
上述程序运行时,会在 com.baeldung.mybatisplus
包中输出生成的文件:
List<String> codeFiles = Arrays.asList("src/main/java/com/baeldung/mybatisplus/entity/Client.java",
"src/main/java/com/baeldung/mybatisplus/mapper/ClientMapper.java",
"src/main/java/com/baeldung/mybatisplus/service/ClientService.java",
"src/main/java/com/baeldung/mybatisplus/service/impl/ClientServiceImpl.java");
for (String filePath : codeFiles) {
Path path = Paths.get(filePath);
assertTrue(Files.exists(path));
}
如上,断言自动生成的类/接口(如 Client
、ClientMapper
、ClientService
和 ClientServiceImpl
)存在于相应的路径中。
4.7、自定义 ID 生成器 {#47自定义-id-生成器}
MyBatis-Plus 框架允许通过实现 IdentifierGenerator
接口来自定义 ID 生成器。
创建 TimestampIdGenerator
类,并实现 IdentifierGenerator
接口的 nextId()
方法,返回 System
的纳秒数作为 ID:
@Component
public class TimestampIdGenerator implements IdentifierGenerator {
@Override
public Long nextId(Object entity) {
return System.nanoTime();
}
}
现在,我们可以使用 timestampIdGenerator
Bean 创建设置了自定义 ID 的 Client
对象:
Client client = new Client();
client.setId(timestampIdGenerator.nextId(client));
client.setFirstName("Harry");
clientService.save(client);
assertThat(timestampIdGenerator.nextId(harry)).describedAs(
"Since we've used the timestampIdGenerator, the nextId value is greater than the previous Id")
.isGreaterThan(harry.getId());
日志如下,显示了由 TimestampIdGenerator
类生成的自定义 ID 值:
16:54:36.485 [main] DEBUG c.b.m.mapper.ClientMapper.insert - ==> Preparing: INSERT INTO client ( id, first_name, creation_date ) VALUES ( ?, ?, ? )
16:54:36.485 [main] DEBUG c.b.m.mapper.ClientMapper.insert - ==> Parameters: 678220507350000(Long), Harry(String), null
16:54:36.485 [main] DEBUG c.b.m.mapper.ClientMapper.insert - <== Updates: 1
参数中显示的 long id
值是系统时间(纳秒)。
4.8、数据库迁移 {#48数据库迁移}
MyBatis-Plus 提供了自动处理 DDL 迁移的机制。
我们只需继承 SimpleDdl
类并覆盖 getSqlFiles()
方法,返回包含数据库迁移语句的 SQL 文件路径列表:
@Component
public class DBMigration extends SimpleDdl {
@Override
public List<String> getSqlFiles() {
return Arrays.asList("db/db_v1.sql", "db/db_v2.sql");
}
}
底层 IdDL
接口创建了 ddl_history
表,用于保存对 schema 执行的 DDL 语句的历史记录:
CREATE TABLE IF NOT EXISTS `ddl_history` (`script` varchar(500) NOT NULL COMMENT '脚本',`type` varchar(30) NOT NULL COMMENT '类型',`version` varchar(30) NOT NULL COMMENT '版本',PRIMARY KEY (`script`)) COMMENT = 'DDL 版本'
alter table client add column address varchar(255)
alter table client add column deleted int default 0
注意:此功能适用于 MySQL 和 PostgreSQL 等大多数数据库,但不适用于 H2。
5、总结 {#5总结}
本文介绍了 MyBatis-Plus 框架,它是 MyBatis 框架的扩展,它基于 MyBatis 提供了很多额外的功能,除了基本的 CRUD 操作外,还提供了批处理、流式查询、分页、逻辑删除、审计、代码生成、数据库迁移等方便的功能。
Ref:https://www.baeldung.com/mybatis-plus-introduction