MyBatis Plus 简介

1、简介

MyBatis 是一个流行的开源持久性框架,提供了 JDBC 和 Hibernate 的替代方案。

本文将带你了解 MyBatis 的一个扩展,名为 MyBatis-Plus,它具有许多方便的功能,可以大大地提高我们的开发效率。

2、MyBatis-Plus 整合

2.1、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 依赖,我们还需要在 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

依赖添加完毕后,创建 Client 实体,其中包含一些属性,如 idfirstNamelastNameemail

@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

然后,为 Client 实体创建 mapper 接口 ClientMapper,它继承了 MyBatis-Plus 提供的 BaseMapper 接口:

@Mapper
public interface ClientMapper extends BaseMapper<Client> {
}

BaseMapper 接口为 CRUD 操作提供了大量默认方法,如 insert()selectOne()updateById()insertOrUpdate()deleteById()deleteByIds()

2.4、ClientService

接下来,创建继承 IService 接口的 ClientService 接口:

public interface ClientService extends IService<Client> {
}

IService 接口封装了 CRUD 操作的默认实现,并使用 BaseMapper 接口提供简单且可维护的基本数据库操作。

2.5、ClientServiceImpl

最后,创建 ClientServiceImpl 类:

@Service
public class ClientServiceImpl extends ServiceImpl<ClientMapper, Client> implements ClientService {
    @Autowired
    private ClientMapper clientMapper;
}

它是 Client 实体的 Service 实现,注入了 ClientMapper 依赖。

3、CRUD 操作

3.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.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、读取

接下来,看看一些方便的查询方法,如 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 类,如 QueryWrapperLambdaQueryWrapperQueryChainWrapper

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、更新

然后,来看看执行更新的几种方法:

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、删除

同样,我们可以使用 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、其他特性

来看看 MyBatis-Plus 为 MyBatis 扩展的一些功能。

4.1、批处理

首先是批处理,可以批量执行常见的 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、分页查询

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、流式查询

MyBatis-Plus 通过 selectList()selectByMap()selectBatchIds() 等方法为流式查询提供了支持,用于高效处理大数据。

首先来看看 ClientService 接口提供的 selectList() 方法:

clientService.getBaseMapper()
  .selectList(Wrappers.emptyWrapper(), resultContext -> 
    assertNotNull(resultContext.getResultObject()));

如上,使用 getResultObject() 方法从数据库中获取每一条记录。

同样,还有 getResultCount() 方法和 stop() 方法,前者用于返回正在处理的结果数,后者用于停止对结果集的处理。

4.4、自动填充

MyBatis-Plus 还支持在 insertupdate 操作时自动填充字段。

例如,可以使用 @TableField 注解在插入新记录时设置 creationDate,在更新时设置 lastModifiedDate

public class Client {
    // ...

    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime creationDate;

    @TableField(fill = FieldFill.UPDATE)
    private LocalDateTime lastModifiedDate;

    // Getter / Setter 省略
}

现在,MyBatis-Plus 会在每次 insertupdate 查询时自动填充 creation_datelast_modified_date 列。

4.5、逻辑删除

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、代码生成

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>

此外,还需要 VelocityFreemarker 等模板引擎的支持。

然后,使用 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));
}

如上,断言自动生成的类/接口(如 ClientClientMapperClientServiceClientServiceImpl)存在于相应的路径中。

4.7、自定义 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、数据库迁移

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、总结

本文介绍了 MyBatis-Plus 框架,它是 MyBatis 框架的扩展,它基于 MyBatis 提供了很多额外的功能,除了基本的 CRUD 操作外,还提供了批处理、流式查询、分页、逻辑删除、审计、代码生成、数据库迁移等方便的功能。


Ref:https://www.baeldung.com/mybatis-plus-introduction