在这篇介绍性文章中,我们探讨 MyBatis-Plus - 流行的 MyBatis 框架的扩展,它包含许多开发人员友好的、在数据库上执行 CRUD 操作的方法。
MyBatis是一种流行的开源持久性框架,它提供了 JDBC 和 Hibernate 的替代方案。
在本文中,我们将讨论 MyBatis 的一个扩展,称为MyBatis-Plus,它具有许多方便的功能,可提供快速开发和更高的效率。
MyBatis-Plus 设置
Maven 依赖
首先,让我们在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 依赖项,因此我们还需要将spring-boot-starter依赖项添加到pom.xml中。
或者,我们可以在使用 Spring Boot 2 时添加以下依赖项:
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.7</version> </dependency>
|
接下来,我们将把 H2 依赖项添加到内存数据库的pom.xml中,以验证 MyBatis-Plus 的特性和能力。
<dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <version>2.3.230</version> </dependency>
|
同样,在这里找到最新版本的 Maven 依赖项。我们也可以使用MySQL进行集成。
客户端
一旦我们的设置准备就绪,让我们创建具有一些属性(如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; // getters and setters ... }
|
在这里,我们使用了 MyBatis-Plus 的自解释注释,如@TableName和@TableId ,以便与底层数据库中的客户端表快速集成。
客户端映射器
然后,我们将为客户端实体创建映射器接口 - ClientMapper,它扩展了 MyBatis-Plus 提供的 BaseMapper 接口:
@Mapper public interface ClientMapper extends BaseMapper<Client> { }
|
BaseMapper接口为 CRUD 操作提供了许多默认方法,如insert()、selectOne()、updateById()、insertOrUpdate()、deleteById()和deleteByIds() 。
客户端服务
接下来,让我们创建扩展IService接口的ClientService服务接口:
public interface ClientService extends IService<Client> { }
|
IService接口封装了CRUD操作的默认实现,并使用BaseMapper接口提供简单易维护的基本数据库操作。
客户端服务实现
最后,我们将创建ClientServiceImpl类:
@Service public class ClientServiceImpl extends ServiceImpl<ClientMapper, Client> implements ClientService { @Autowired private ClientMapper clientMapper; }
|
它是客户端实体的服务实现,注入了ClientMapper依赖项。
CRUD 操作
1. 创建
现在我们已经准备好所有实用程序接口和类,让我们使用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..mybatisplus的日志记录级别设置为DEBUG,我们就可以在保存客户端对象时观察到以下日志:
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接口生成的日志显示了插入查询及其参数以及最终插入到数据库中的行数。
2. 读
接下来,让我们看看一些方便的读取方法,如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 框架还附带一些方便的包装类,如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()方法来利用包装类让我们直观地编写复杂的查询。
3. 更新
然后,我们来看看执行更新的几种方法:
Client client = clientService.getById(2); client.setEmail("test@jdon.com"); clientService.updateById(client); assertEquals("testl@jdon.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: test@jdon.com(String), 2(Long) 16:07:57.441 [main] DEBUG c.b.m.mapper.ClientMapper.updateById - <== Updates: 1
|
类似地,我们可以使用LambdaUpdateWrapper类来更新客户端对象:
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);
|
一旦客户端对象更新,我们就使用QueryWrapper类来确认操作。
4. 删除
类似地,我们可以使用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
|
与更新日志类似,se 日志显示删除查询及其参数和从数据库中删除的总行数。
4. 额外功能
让我们讨论一下 MyBatis-Plus 中作为 MyBatis 扩展提供的一些方便的功能。
1. 批量操作
首先是能够批量执行常见的 CRUD 操作,从而提高性能和效率:
Client client2 = new Client(); client2.setFirstName("Harry"); Client client3 = new Client(); client3.setFirstName("Ron"); Client client4 = new Client(); client4.setFirstName("Hermione"); // create in batches 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()等方法来批量对对象集合执行保存、更新或删除操作。
2. 分页
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类对记录进行分页。例如,让我们获取第二页,其中包含三个结果:
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
|
同样,se 日志显示从数据库中选择的带有参数和总行数的选择查询。
3. 流式查询
MyBatis-Plus 通过selectList()、selectByMap()和selectBatchIds()等方法提供对流式查询的支持,让我们能够处理大数据并满足性能目标。
例如,让我们检查一下通过ClientService接口提供的selectList()方法:
clientService.getBaseMapper() .selectList(Wrappers.emptyWrapper(), resultContext -> assertNotNull(resultContext.getResultObject()));
|
在这里,我们使用getResultObject()方法从数据库中获取每条记录。
同样,我们有getResultCount()方法来返回正在处理的结果的数量,以及stop()方法来停止结果集的处理。
4. 自动填充
MyBatis-Plus 作为一个相当有主见和智能的框架,还支持自动填充插入和更新操作的字段。
例如,我们可以使用@TableField注释在插入新记录时设置creationDate ,在发生更新时设置lastModifiedDate :
public class Client { // ... @TableField(fill = FieldFill.INSERT) private LocalDateTime creationDate; @TableField(fill = FieldFill.UPDATE) private LocalDateTime lastModifiedDate; // getters and setters ... }
|
现在,MyBatis-Plus 将在每次插入和更新查询时自动填充creation_date和last_modified_date列。
5. 逻辑删除
MyBatis-Plus 框架提供了一种简单有效的策略,让我们通过在数据库中标记来逻辑地删除记录。
我们可以通过在已删除的属性上使用@TableLogic注释来启用该功能:
@TableName("client") public class Client { // ... @TableLogic private Integer deleted; // getters and setters ... }
|
现在,框架将在执行数据库操作时自动处理记录的逻辑删除。
因此,让我们删除客户端对象并尝试读取相同的内容:
clientService.removeById(harry); assertNull(clientService.getById(harry.getId()));
|
观察以下日志以检查更新查询,该查询将已删除属性的值设置为1 ,并在数据库上运行选择查询时使用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
|
上述配置让我们可以使用删除和活动值来更改删除字段的名称。
6. 代码生成器
MyBatis-Plus 提供自动代码生成功能,以避免手动创建实体、映射器和服务接口等冗余代码。
首先,让我们将最新的 mybatis-plus-generator依赖项添加到我们的pom.xml中:
<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.jdon.mybatisplus").service("ClientService")) .templateEngine(new FreemarkerTemplateEngine()) .execute();
|
因此,当上述程序运行时,它将在com..mybatisplus包中生成输出文件:
List<String> codeFiles = Arrays.asList("src/main/java/com/jdon/mybatisplus/entity/Client.java", "src/main/java/com/jdon/mybatisplus/mapper/ClientMapper.java", "src/main/java/com/jdon/mybatisplus/service/ClientService.java", "src/main/java/com/jdon/mybatisplus/service/impl/ClientServiceImpl.java"); for (String filePath : codeFiles) { Path path = Paths.get(filePath); assertTrue(Files.exists(path)); }
|
在这里,我们断言自动生成的类/接口(如Client、ClientMapper、ClientService和ClientServiceImpl )存在于相应的路径中。
7. 自定义 ID 生成器
MyBatis-Plus 框架允许使用IdentifierGenerator接口实现自定义 ID 生成器。
例如,让我们创建TimestampIdGenerator类并实现IdentifierGenerator接口的nextId()方法来返回系统的纳秒:
@Component public class TimestampIdGenerator implements IdentifierGenerator { @Override public Long nextId(Object entity) { return System.nanoTime(); } }
|
现在,我们可以使用timestampIdGenerator bean 创建设置自定义 ID 的客户端对象:
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
|
参数中显示的id的长值是系统时间,单位为纳秒。
8. 数据库迁移
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表来保存在模式上执行的 DDL 语句的历史记录:
CREATE TABLE IF NOT EXISTS <code>ddl_history</code> (<code>script</code> varchar(500) NOT NULL COMMENT '脚本',<code>type</code> varchar(30) NOT NULL COMMENT '类型',<code>version</code> varchar(30) NOT NULL COMMENT '版本',PRIMARY KEY (<code>script</code>)) COMMENT = 'DDL 版本' alter table client add column address varchar(255) alter table client add column deleted int default 0
|
注意:此功能适用于大多数数据库,如 MySQL 和 PostgreSQL,但不适用于 H2。