MyBatis Plus

摘要:MyBatis-Plus 是基于 MyBatis 框架的一个增强工具,主要目的是简化 MyBatis 的开发过程,提供更加简洁、方便的 CRUD 操作。


目录

[TOC]

MyBatis Plus

MyBatis-Plus 是基于 MyBatis 框架的一个增强工具,主要目的是简化 MyBatis 的开发过程,提供更加简洁、方便的 CRUD 操作。

  • 在保留 MyBatis 强大功能的基础上,通过封装和优化一些常见操作来提高开发效率

功能特性

提供了许多开箱即用的功能,部分核心特性包括:

  1. 无侵入设计:只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑。不会改变 MyBatis 原有的 API 和使用方式,可以自由选择 MyBatis 和 MyBatis-Plus 的功能。
  2. 强大的 CRUD 操作:内置通用 Mapper、通用 Service,仅仅通过少量配置即可实现单表大部分 CRUD 操作,更有强大的条件构造器,满足各类使用需求
    • 自动生成 CRUD 代码:通过 BaseMapperServiceImpl 接口,提供了一系列 CRUD 操作的方法,如 insertdeleteupdateselect,减少了重复的 SQL 编写工作。
  3. 条件构造器:如 QueryWrapper,可以通过链式编程方式轻松构建复杂的查询条件。
  4. 分页查询、性能优化、以及支持多种数据。
  5. 损耗小:启动即会自动注入基本 CURD,性能基本无损耗,直接面向对象操作
  6. 支持 Lambda 形式调用:通过 Lambda 表达式,方便的编写各类查询条件,无需再担心字段写错
  7. 支持主键自动生成:支持多达 4 种主键策略(内含分布式唯一 ID 生成器 - Sequence),可自由配置,完美解决主键问题
  8. 内置代码生成器:采用代码或者 Maven 插件可快速生成 Mapper 、 Model 、 Service 、 Controller 层代码,支持模板引擎,更有超多自定义配置等您来使用
  9. 内置分页插件:基于 MyBatis 物理分页,开发者无需关心具体操作,配置好插件之后,写分页等同于普通 List 查询
    • 分页插件支持多种数据库:支持 MySQL、MariaDB、Oracle、DB2、H2、HSQL、SQLite、Postgre、SQLServer 等多种数据库

参考:mybatis plus 常用知识汇总(保姆级教程!~)

快速开始

引入依赖

pom.xml 文件中,引入相关依赖。

  • 相比来说,将 mybatis-spring-boot-starter 替换成 mybatis-plus-boot-starter
1
2
3
4
5
6
<!-- 实现对 MyBatis Plus 的自动化配置 -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.2.0</version>
</dependency>

Java 配置类

创建 Application.java 类,配置 @MapperScan 注解,扫描对应 Mapper 接口所在的包路径。

1
2
3
4
5
// MyBatisPlusConfig.java

@MapperScan(basePackages = "cn.iocoder.springboot.lab12.mybatis.mapper")
public class MyBatisPlusConfig {
}

应用配置文件

resources/application.yaml 配置文件。

  • mybatis 替换成 mybatis-plus 配置项目。实际上,如果老项目在用 mybatis-spring-boot-starter 的话,直接将 mybatis 修改成 mybatis-plus 即可。
  • 相比 mybatis 配置项来说,mybatis-plus 增加了更多配置项,也因此无需在配置 mybatis-config.xml 配置文件。更多的 MyBatis-Plus 配置项,可以看看 MyBatis-Plus 使用配置
  • 配置 logging 的原因是,方便看到 MyBatis-Plus 自动生成的 SQL 。生产环境下,记得关闭。
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
spring:
  # datasource 数据源配置内容
  datasource:
    url: jdbc:mysql://47.112.193.81:3306/testb5f4?useSSL=false&useUnicode=true&characterEncoding=UTF-8
    driver-class-name: com.mysql.jdbc.Driver
    username: testb5f4
    password: F4df4db0ed86@11

# mybatis-plus 配置内容
mybatis-plus:
  configuration:
    map-underscore-to-camel-case: true # 虽然默认为 true ,但是还是显示去指定下。
  global-config:
    db-config:
      id-type: auto # ID 主键自增
      logic-delete-value: 1 # 逻辑已删除值(默认为 1)
      logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
  mapper-locations: classpath*:mapper/*.xml
  type-aliases-package: cn.iocoder.springboot.lab12.mybatis.dataobject

# logging
logging:
  level:
    # dao 开启 debug 模式,mybatis 输入 sql
    cn:
      iocoder:
        springboot:
          lab12:
            mybatis:
              mapper: debug

自动生成

创建数据库表

三种方式不用纠结,对于使用的人来说不分优劣,只分好用不好用。

  1. AutoGenerator:官方 Java 配置代码。可以快速生成 Entity、Mapper、Mapper XML、Service、Controller等各个模块的代码,极大的提升了开发效率。

  2. MyBatis Plus 插件:简洁易用

  3. MyBatisX-Generator 插件

  4. Mybatis-plus Code Generator 插件

  5. EasyCode 插件,最全面

AutoGenerator

自带的,通过 Java 程序自动生成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
FastAutoGenerator.create("url", "username", "password")
        .globalConfig(builder -> builder
                .author("Baomidou")
                .outputDir(Paths.get(System.getProperty("user.dir")) + "/src/main/java")
                .commentDate("yyyy-MM-dd")
        )
        .packageConfig(builder -> builder
                .parent("com.baomidou.mybatisplus")
                .entity("entity")
                .mapper("mapper")
                .service("service")
                .serviceImpl("service.impl")
                .xml("mapper.xml")
        )
        .strategyConfig(builder -> builder
                .entityBuilder()
                .enableLombok()
        )
        .templateEngine(new FreemarkerTemplateEngine())
        .execute();
生成方式

代码生成器目前支持两种生成方式:

  1. DefaultQuery (元数据查询)
    • 优点: 根据通用接口读取数据库元数据相关信息,对数据库通用性较好。
    • 缺点: 依赖数据库厂商驱动实现。
    • 备注: 默认方式,部分类型处理可能不理想。
  2. SQLQuery (SQL查询)
    • 优点: 需要根据数据库编写对应表、主键、字段获取等查询语句。
    • 缺点: 通用性不强,同数据库厂商不同版本可能会存在兼容问题(例如,H2数据库只支持1.X版本)。
    • 备注: 后期不再维护。

如果是已知数据库(无版本兼容问题),请继续按照原有的SQL查询方式继续使用,示例代码如下:

1
2
3
4
5
6
7
8
9
// MYSQL 示例 切换至SQL查询方式,需要指定好 dbQuery 与 typeConvert 来生成
FastAutoGenerator.create("url", "username", "password")
                .dataSourceConfig(builder ->
                        builder.databaseQueryClass(SQLQuery.class)
                                .typeConvert(new MySqlTypeConvert())
                                .dbQuery(new MySqlQuery())
                )

                // Other Config ...

元数据查询目前有如下问题:

  1. 不支持使用 NotLike 的方式反向生成表。

  2. 无法读取表注释,解决方法:

    • MySQL链接增加属性 remarks=true&useInformationSchema=true
    • Oracle链接增加属性 remarks=true 或者 remarksReporting=true(某些驱动版本)
    • SqlServer:驱动不支持
  3. 部分 PostgreSQL 类型处理不佳(如 json、jsonb、uuid、xml、money 类型),解决方法:

    • 转换成自定义的类型配合自定义 TypeHandler 来处理。
    • 扩展 typeConvertHandler 来处理(3.5.3.3 后增加了 typeName 获取)。
  4. MySQL 下 tinyint 字段转换问题:

    • 当字段长度为 1 时,无法转换成 Boolean 字段,建议在指定数据库连接时添加 &tinyInt1isBit=true

    • 当字段长度大于 1 时,默认转换成 Byte,如果想继续转换成 Integer,可使用如下代码:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
        FastAutoGenerator.create("url", "username", "password")
                .dataSourceConfig(builder ->
                        builder.typeConvertHandler((globalConfig, typeRegistry, metaInfo) -> {
                            // 兼容旧版本转换成Integer
                            if (JdbcType.TINYINT == metaInfo.getJdbcType()) {
                                return DbColumnType.INTEGER;
                            }
                            return typeRegistry.getColumnType(metaInfo);
                        })
                );
      
依赖

由于代码生成器用到了模板引擎,请自行引入喜好的模板引擎。

MyBatis-Plus Generator 支持如下模板引擎:

  • VelocityTemplateEngine(Default)
  • FreemarkerTemplateEngine
  • BeetlTemplateEngine
  • EnjoyTemplateEngine

如果还想使用或适配其他模板引擎,可自行继承 AbstractTemplateEngine 并参考其他模板引擎实现自定义。

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
    <!--mysql数据库驱动-->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.35</version>
    </dependency>
	<!--通过注解消除实际开发中的样板式代码-->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>

    <!--mybatis-plus启动器-->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.3.0</version>
    </dependency>
    <!--代码生成器-->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-generator</artifactId>
        <version>3.3.0</version>
    </dependency>

    <dependency>
        <groupId>io.springfox</groupId>
        <artifactId>springfox-swagger-ui</artifactId>
        <version>2.9.2</version>
    </dependency>
    <dependency>
        <groupId>io.springfox</groupId>
        <artifactId>springfox-swagger2</artifactId>
        <version>2.9.2</version>
    </dependency>
使用

可以通过以下两种形式使用代码生成器。

快速生成

在 CodeGenerator 中的 main 方法中直接添加生成器代码,并进行相关配置,然后直接运行即可生成代码。

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
// CodeGenerator.java
public static void main(String[] args) {

    FastAutoGenerator.create("url", "username", "password")
        .globalConfig(builder -> {
            builder.author("baomidou") // 设置作者
                .enableSwagger() // 开启 swagger 模式
                .outputDir("D://"); // 指定输出目录
        })

        .dataSourceConfig(builder ->
         	builder.typeConvertHandler((globalConfig, typeRegistry, metaInfo) -> {
                int typeCode = metaInfo.getJdbcType().TYPE_CODE;
                if (typeCode == Types.SMALLINT) {
                    // 自定义类型转换
                    return DbColumnType.INTEGER;
                }
                return typeRegistry.getColumnType(metaInfo);
            })
        )

        .packageConfig(builder ->
        	builder.parent("com.baomidou.mybatisplus.samples.generator") // 设置父包名
                   .moduleName("system") // 设置父包模块名
                   .pathInfo(Collections.singletonMap(OutputFile.xml, "D://")) // 设置mapperXml生成路径
         )

        .strategyConfig(builder ->
        	builder.addInclude("t_simple") // 设置需要生成的表名
                   .addTablePrefix("t_", "c_") // 设置过滤表前缀
         )

        .templateEngine(new FreemarkerTemplateEngine()) // 使用Freemarker引擎模板,默认的是Velocity引擎模板

        .execute();

}
交互式生成

交互式生成在运行之后,会提示输入相应的内容,等待配置输入完整之后就自动生成相关代码。

如果需要更多例子可查看 test 包下面的 samples。

配置

请移步至 代码生成器配置 查看。

BaseDO 类
  1. baseEntity:用来写一些公共字段。例如:create_time(创建时间)、creator(创建人)、update_time、updator、logic_delete 等。
  2. baseController:用来写基础公共的控制。
1
2
3
4
5
6
7
package com..common.dao;

import java.io.Serializable;

//public class BaseEntity implements Serializable {
public abstract class BaseDO implements Serializable, TransPojo {
}
BaseMapper

MyBatisPlus 插件

选中Other菜单,会出现Config Database(配置数据库)和Code Generator(代码生成)

  • 生成 Controller、Service、Entity

MyBatisX-Generator 插件

  • mapper 和 xml 可以来回跳转
  • mybatis.xml、mapper.xml 提示
  • mapper 和 xml 支持类似 jpa 的自动提示(参考 MybatisCodeHelperPro)
  • 集成 mybatis 生成器 GUI(从免费的 mybatis 插件复制)
    • 直接用数据库连接,不用另外配置。
    • Lombok 注解不是特别友好

MyBatisplus Code Generator

Mybatisplus 代码生成器,介绍了各种代码生成器的优点。本着约定大于配置的理念。

关键词:Mybatis Plus、Maven、Spring Boot、Lombok、Mysql、Freemarker、XMind、Excel 等。

提供两种形式:Windows 桌面工具 exe 和 IDEA 插件。

打开方式:工具 -> Mybatisplus 代码生成器 或 Ctrl + Alt + 0

功能:

  1. 一键生成 Mybatis Plus 代码,傻瓜式功能选择。
  2. Freemarker 代码自定义模板配置。
  3. 工程化:支持 Maven 和 Spring Boot 工程化生成。
  4. 多数据源管理。
  5. 配置可以是数据持久化,用户操作记忆。
  6. 一键导出数据库表的思维导图
  7. 一键导出 Excel 设计文档到数据库表。

EasyCode 插件

EasyCode是基于IntelliJ IDEA Ultimate版开发的一个代码生成插件,主要通过自定义模板(基于velocity)来生成各种想要的代码。

  • 通常用于生成Entity、Dao、Service、Controller。
  • 如果动手能力强还可以用于生成HTML、JS、PHP等代码。理论上来说只要是与数据有关的代码都是可以生成的。

  • 直接用数据库连接,不用另外配置。
  • 自定义模板:点击 File->Settings->Easy Code->Template Setting。
    • 比如想在生成的 dao 层代码中,额外添加一个不需要任何条件,获取所有数据的getAll()方法(默认的生成模版中没有这个方法)。
    • 需要编写自定义模版
  • 支持导出配置。

实体类

  • 实体类放在 dal.dataobject 包下,以 DO 结尾;mapper 数据库访问类放在 dal.mysql 包下,以 Mapper 结尾。

BaseDO

BaseDO是所有数据库实体的父类

  • abstract class
  • createTime + creator 字段,创建人相关信息。
  • updater + updateTime 字段,创建人相关信息。
  • 增加了 deleted 字段,并添加了@TableLogic注解,设置该字段为逻辑删除的标记。
  • LocalDateTime 日期时间
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
// 抽象类
@Data
@JsonIgnoreProperties(value = "transMap") // 由于 Easy-Trans 会添加 transMap 属性,避免 Jackson 在 Spring Cache 反序列化报错
public abstract class BaseDO implements Serializable, TransPojo {

    /**
     * 创建时间,插入时调用 DefaultDBFillFieldHandler.inserFill 自动填充
     */
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
    /**
     * 最后更新时间,插入 or 更新时
     */
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
    
    /**
     * 创建者,目前使用 AdminUserDO / MemberUserDO / SysUser 的 id 编号
     * 使用 String 类型的原因是,未来可能会存在非数值的情况,留好拓展性。
     */
    @TableField(fill = FieldFill.INSERT)
    private String creator;
    /**
     * 更新者
     */
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private String updater;
    
    /**
     * 是否删除
     */
    @TableLogic
    private Boolean deleted;

     /**
     * 把 creator、createTime、updateTime、updater 都清空,
     * 避免前端直接传递 creator 之类的字段,直接就被更新了
     */
    public void clean(){
        this.creator = null;
        this.createTime = null;
        this.updater = null;
        this.updateTime = null;
    }
}

对应的 SQL 字段如下:

1
2
3
4
5
`creator` 		varchar(64) 	CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '创建者',
`create_time` 	datetime 		NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` 		varchar(64) 	CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '更新者',
`update_time` 	datetime 		NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` 		bit(1) 			NOT NULL DEFAULT b'0' COMMENT '是否删除',

UserDO

创建 DO/UserDO.java 类 。相比 「2.5 UserDO」 来说,主要差别有:

  • extends BaeDO:继承基类
  • 实体类的注释要完整,特别是哪些字段是关联(外键)、枚举、冗余等。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// UserDO.java
@Data
@EqualsAndHashCode(callSuper = true)
@TableName(value = "user")
public class UserDO extends BaeDO {

    /**
     * 用户编号
     */
    private Long id;

    private String username;
    private String password;

    // ... 无需 setting/getting 方法
}

对应的创建表的 SQL 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
CREATE TABLE `user` (
  	`id` 			int(11) 	NOT NULL 		AUTO_INCREMENT COMMENT '用户编号',
  	`username` 		varchar(64) COLLATE utf8mb4_bin 	DEFAULT NULL COMMENT '账号',
  	`password` 		varchar(32) COLLATE utf8mb4_bin 	DEFAULT NULL COMMENT '密码',

    `creator` 		varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci 	DEFAULT '' COMMENT '创建者',
    `create_time` 	datetime 	NOT NULL 	DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `updater` 		varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '更新者',
    `update_time` 	datetime 	NOT NULL 	DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    `deleted` 		bit(1) 		NOT NULL 	DEFAULT b'0' COMMENT '是否逻辑删除。0-未删除;1-删除',
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_username` (`username`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
ProductSkuDO
  • property:DO 静态类,对应 SQL properties varchar(512) null comment '属性数组,JSON 格式',
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
74
75
76
77
78
79
80
81
82
83
84
package cn..module.product.dal.dataobject.sku;

/**
 * 商品 SKU DO
 *
 * @author 芋道源码
 */
@TableName(value = "product_sku", autoResultMap = true)
@KeySequence("product_sku_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ProductSkuDO extends BaseDO {

    /**
     * 商品 SKU 编号,自增
     */
    @TableId
    private Long id;
    /**
     * SPU 编号
     *
     * 关联 {@link ProductSpuDO#getId()}
     */
    private Long spuId;
    /**
     * 属性数组,JSON 格式
     */
    @TableField(typeHandler = JacksonTypeHandler.class)
    private List<Property> properties;
    /**
     * 商品价格,单位:分
     */
    private Integer price;
    
    ...

    // ========== 营销相关字段 =========

    // ========== 统计相关字段 =========
    /**
     * 商品销量
     */
    private Integer salesCount;

    /**
     * 商品属性
     */
    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public static class Property {

        /**
         * 属性编号
         * 关联 {@link ProductPropertyDO#getId()}
         */
        private Long propertyId;
        /**
         * 属性名字
         * 冗余 {@link ProductPropertyDO#getName()}
         *
         * 注意:每次属性名字发生变化时,需要更新该冗余
         */
        private String propertyName;

        /**
         * 属性值编号
         * 关联 {@link ProductPropertyValueDO#getId()}
         */
        private Long valueId;
        /**
         * 属性值名字
         * 冗余 {@link ProductPropertyValueDO#getName()}
         *
         * 注意:每次属性值名字发生变化时,需要更新该冗余
         */
        private String valueName;

    }
}

自动映射规则

主要涉及如何将数据库表和字段自动映射到 Java 实体类及其属性。

  1. @TableName:表名与实体类名的映射
  2. @TableId:主键的自动映射
  3. @TableField:字段名与属性名的映射

@TableName

增加了 @TableName 注解:设置了 UserDO 对应的表名是 user 。毕竟,要使用 MyBatis-Plus 给自动生成 CRUD 操作。

  • 默认规则:MyBatis-Plus 默认使用实体类名作为数据库表名的前缀。比如,如果你的实体类名为 User,那么它会映射到名为 user 的数据库表。

  • 自定义规则:可以使用 @TableName 注解来指定自定义的表名。例如:

    1
    2
    3
    4
      @TableName("sys_user")
      public class User {
          // 属性和方法
      }
    
@KeySequence

在数据库设计中,主键的生成方式多种多样,而序列(Sequence)是一种常见的生成主键的方式。

用于标识实体类中的主键字段,并指定使用哪个数据库的序列来生成主键。

  • 通过在实体类字段上添加 @KeySequence 注解,可以简单地实现基于序列的主键生成,无需手动处理序列的获取和使用。

  • value 属性: 用于指定使用的数据库序列的名称。

1
2
3
4
5
@TableName("infra_api_access_log")
@KeySequence(value = "infra_api_access_log_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
@Data
@AllArgsConstructor
public class ApiAccessLogDO extends BaseDO {

@TableId

主键

id 主键编号,推荐使用 Long 型自增,原因是:

  • 自增,保证数据库是按顺序写入,性能更加优秀。
  • Long 型,避免未来业务增长,超过 Int 范围。

对应的 SQL 字段如下:

1
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号',

项目的 id 默认采用数据库自增的策略,如果希望使用 Snowflake 雪花算法,可以修改 application.yaml 配置文件,将配置项 mybatis-plus.global-config.db-config.id-type 修改为 ASSIGN_ID

@TableId

@TableId:专门用在主键上的注解,如果数据库中的主键字段名和实体中的属性名,不一样且不是驼峰之类的对应关系。

  • 可以在实体中表示主键的属性上加@Tableid注解,并指定value属性值为表中主键的字段名,即可以对应上。

在 MyBatis-Plus 中,@TableId 注解用于标识实体类中的主键字段。

  • 有两个主要属性:valuetype。分别用于指定字段名和主键生成策略。
@TableId 注解属性
  1. value:

    • 用途:指定数据库表中的主键列名。它的值应该是数据库表中实际的列名。

    • 示例:如果数据库表中的主键列名是user_id,则在实体类中可以这样配置:

      1
      2
      1
      2
        @TableId(value = "user_id", type = IdType.ASSIGN_UUID)
        private String userId;
      
  2. type:

    • 用途:指定主键生成策略。MyBatis-Plus 提供了多种主键生成策略,IdType 枚举类定义了这些策略。

    • 常用的生成策略

      • IdType.AUTO:数据库自动生成(通常是自增长 ID)。
      • IdType.INPUT:用户输入 ID(即需要手动设置)。
      • IdType.ASSIGN_ID:由 MyBatis-Plus 生成的 ID(通常是 UUID)。
      • IdType.ASSIGN_UUID生成 UUID(字符串类型的唯一 ID)。
    • 示例:如果想使用 UUID 作为主键,可以使用 IdType.ASSIGN_UUID

      1
      2
      1
      2
        @TableId(value = "user_id", type = IdType.ASSIGN_UUID)
        private String userId;
      

@TableField

表字段填充,is fill

  • 默认规则:字段名和属性名默认是直接映射的。例如,数据库中的 user_name 字段会映射到实体类中的 userName 属性。

  • 驼峰命名规则:MyBatis-Plus 默认启用了驼峰命名转换。即,数据库中的下划线命名(如 user_name)会被自动转换为 Java 属性的驼峰命名(如 userName)。

  • 自定义字段映射:可以使用@TableField注解来指定自定义的字段名。例如:

    1
    2
      @TableField("user_name")
      private String userName;
    

@TableField的其他用法:

描述 备注
value 数据表中的字段名 驼峰命名方式,该值可无
update 预处理 set 字段自定义注入  
condition 预处理 WHERE 实体条件自定义运算规则  
el 详看注释说明  
exist 是否为数据库表字段 默认 true 存在,false 不存在
strategy 字段验证 默认 非 null 判断,查看 com.baomidou.mybatisplus.enums.FieldStrategy
fill 字段自动填充标记 FieldFill, 配合自动填充使用 。DEFAULT、INSERT、UPDATE、INSERT_UPDATE。
value 属性字段映射

比如说数据库中字段为last_name,而实体类的属性为lastName

前提是在全局策略配置中将驼峰命名关闭。

1
<property name="dbColumnUnderline" value="false"></property>
  • 关于MyBatisPlus中进行通用CRUD全局策略配置参照:https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/details/89425049

这时就可以在实体类上添加:

1
@TableField(value="last_name")
sql语句的关键词

如果表中字段存在 sql语句的关键词 ,比如 desc , 那么需要按照下面的写法, 否则mybatis plus 拼接含有 desc字段 的sql语句时会报错。

1
2
@TableField("`desc`") 
private String desc;
select = false

@TableField(select = false)

作用:用于指定某个字段在执行 SQL 查询时不参与查询,即在 SELECT 语句中不包含该字段。

用途:通常用于那些只需要在数据库操作中存在,但不需要在查询结果中显示的字段。

  • 例如,逻辑删除字段、内部使用的字段等。
1
2
@TableField(select = false)
private Integer age;
exist 属性

@TableField(exist = false)

作用:用于指定某个字段不对应数据库中的任何字段,即在数据库表中不存在该字段

用途:通常用于那些只在 Java 实体中存在的字段,但在数据库表中没有相应的字段。例如,用于计算或临时存储的数据。

1
2
@TableField(exist = false)
private Integer age;

又比如在实体类中有一个属性为remark,但是在数据库中没有这个字段

但是在执行插入操作时,给实体类的remark属性赋值了,那么可以通过在实体类的remark属性上添加:

1
2
@TableField(exist=false)
 private String remark;
fill 属性自动填充

DefaultDBFieldHandler (opens new window)基于 MyBatis 自动填充机制,实现 BaseDO 通用字段的自动设置。

DefaultDBFieldHandler 自动填充

typeHandler 字段类型处理器

MyBatis Plus 提供 TypeHandler 字段类型处理器,用于 JavaType 与 JdbcType 之间的转换。

  • @Builder
  • 使用时,需要设置实体的 @TableName 注解的 @autoResultMap = true
  • @TableField
复杂字段类型转换

常用的字段类型处理器有:

  • JacksonTypeHandler:通用的 Jackson 实现 JSON 字段类型处理器。如 JavaBean 转为 JSON。

字段处理器的示例

字段加密

EncryptTypeHandler (opens new window),基于 Hutool AES (opens new window)实现字段的加密与解密。

例如说,数据源配置 (opens new window)password 密码需要实现加密存储,则只需要在该字段上添加 EncryptTypeHandler 处理器

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
@TableName(value = "infra_data_source_config", autoResultMap = true) // 添加 autoResultMap = true
public class DataSourceConfigDO extends BaseDO {

    // ... 省略其它字段
    /**
     * 密码
     */
    @TableField(typeHandler = EncryptTypeHandler.class) // 添加 EncryptTypeHandler 处理器
    private String password;

}

另外,在 application.yaml 配置文件中,可使用 mybatis-plus.encryptor.password 设置加密密钥。

字段加密后,只允许使用精准匹配,无法使用模糊匹配。

1
2
3
4
5
6
7
8
9
10
@Test // 测试使用 password 查询,可以查询到数据
public void testSelectPassword() {
    // mock 数据
    DataSourceConfigDO dbDataSourceConfig = randomPojo(DataSourceConfigDO.class);
    dataSourceConfigMapper.insert(dbDataSourceConfig);// @Sql: 先插入出一条存在的数据

    // 调用
    DataSourceConfigDO result = dataSourceConfigMapper.selectOne(DataSourceConfigDO::getPassword,
            EncryptTypeHandler.encrypt(dbDataSourceConfig.getPassword())); // 重点:需要使用 EncryptTypeHandler 去加密查询字段!!!
}

逻辑删除

所有表通过 deleted 字段来实现逻辑删除,值为 0 表示未删除,值为 1 表示已删除。

  • 可见 application.yaml 配置文件的 logic-delete-value: 1logic-not-delete-value: 0 配置项。当然,也可以通过注解的 valuedelval 来定义未删除和删除。

  • 具体关于 MyBatis-Plus 的逻辑删除功能,看下 逻辑删除 部分的文档。

逻辑删除的配置

自动拼接 WHERE deleted = 0

① 所有 SELECT 查询,都会自动拼接 WHERE deleted = 0 查询条件,过滤已经删除的记录。

  • 如果要 SELECT 被删除的记录,只能通过在 XML 或者 @SELECT 来手写 SQL 语句。例如:
1
2
3
4
5
@Mapper
public interface RoleMapper extends BaseMapperX<RoleDO> {
    @Select("SELECT id FROM system_role WHERE update_time > #{maxUpdateTime} LIMIT 1")
    RoleDO selectExistsByUpdateTimeAfter(Date maxUpdateTime);
}
额外增加 delete_time 字段

② 建立唯一索引时,需要额外增加 delete_time 字段(初始值为0,表示未被逻辑删除),添加到唯一索引字段中,避免唯一索引冲突。例如说,system_users 使用 username 作为唯一索引:

  • 未添加前:先逻辑删除了一条 username = yudao 的记录,然后又插入了一条 username = yudao 的记录时,会报索引冲突的异常。
  • 已添加后:先逻辑删除了一条 username = yudao 的记录并更新 delete_time 为当前时间,然后又插入一条 username = yudao 并且 delete_time 为 0 的记录,不会导致唯一索引冲突。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 插入字典类型
DictTypeDO dictType = BeanUtils.toBean(createReqVO, DictTypeDO.class);
dictType.setDeletedTime(LocalDateTimeUtils.EMPTY); // 唯一索引,避免 null 值
dictTypeMapper.insert(dictType);


public class LocalDateTimeUtils {

    /**
     * 空的 LocalDateTime 对象,主要用于 DB 唯一索引的默认值
     */
    public static LocalDateTime EMPTY = buildTime(1970, 1, 1);
    
    // 创建指定时间
    public static LocalDateTime buildTime(int year, int month, int day) {
        return LocalDateTime.of(year, month, day, 0, 0, 0);
    }
}

Easy-Trans 数据翻译

easy-trans是一款用于做数据翻译的代码辅助插件,利用mybatis plus/jpa/等ORM框架的能力自动查表,让开发者可以快速的把id/字典码 翻译为前端需要展示的数据;

  • 能减少sql的注入。

为什么实现 {@link TransPojo} 接口?

  • 因为使用 Easy-Trans TransType.SIMPLE 模式,集成 MyBatis Plus 查询。
适用场景
  1. 我有一个id,但是需要给客户展示他的 title/name 但是又不想自己手动做表关联查询

  2. 我有一个字典码 sex 和 一个字典值0 希望能翻译成 给客户展示。

  3. 我有一组 user id 比如 1,2,3 希望能展示成 张三,李四,王五 给客户

  4. 我有一个枚举,枚举里有一个title字段,想给前端展示title的值 给客户

  5. 我有一个唯一键(比如手机号,身份证号码,但是非其他表id字段),但是需要给客户展示他的title/name 但是又不想自己手动做表关联查询

easy trans 支持的五种类型
  1. 字典翻译(TransType.DICTIONARY):需要使用者把字典信息刷新到DictionaryTransService 中进行缓存,使用字典翻译的时候取缓存数据源

  2. 简单翻译(TransType.SIMPLE):比如有userId需要userName或者userPo给前端,原理是组件使用MybatisPlus/JPA的API自动进行查询,把结果放到TransMap中。
    • 无需自己实现数据源(推荐),适用于根据id翻译name/title等 。此数据源需要配合easy_trans_mybatis_plus_extend或者easy_trans_jpa_extend一起使用。
  3. 跨微服务翻译(TransType.RPC):比如订单和用户是2个微服务,但是要在订单详情里展示订单的创建人的用户名,需要用到RPC翻译。

    1. 原理是订单微服务使用restTemplate调用用户服务的一个统一的接口,把需要翻译的id传过去
    2. 然后用户微服务使用MybatisPlus/JPA的API自动进行查询把结果给订单微服务,然后订单微服务拿到数据后进行翻译。
    3. 当然使用者只是需要一个注解,这些事情都是由组件自动完成的。
  4. 自动翻译(TransType.AUTO):还是id翻译name场景,但是使用者如果想组件调用自己写的方法而不通过Mybatis Plus/JPA 的API进行数据查询,就可以使用AutoTrans。

  5. 枚举翻译(TransType.ENUM):比如要把SEX.BOY 翻译为男,可以用枚举翻译。
依赖
1
2
3
4
5
<dependency>
    <groupId>com.fhs-opensource</groupId>
    <artifactId>easy-trans-spring-bootstarter</artifactId>
    <version>2.0.12</version>
</dependency>
配置
1
2
3
# VO 转换(数据翻译)相关
easy-trans:
  is-enable-global: false # 【默认禁用,对性能确认压力大】启用全局翻译(拦截所有 SpringMVC ResponseBody 进行自动翻译 )。如果对于性能要求很高可关闭此配置,或通过 @IgnoreTrans 忽略某个接口
用法
  1. 实现 AutoTransable 接口
  2. 实现 TransPojo 接口:代表这个类需要被翻译或者被当作翻译的数据源
  3. 在需要翻译的字段上添加 @Trans 注解即可。
AutoTransable 接口

只有实现了这个接口的才能自动翻译。

为什么要赋值粘贴到 -common 包下?

  • 因为 AutoTransable 属于 easy-trans-service 下,无法方便的在 -module-xxx-api 模块下使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

/**
 * @since  2020-05-19 10:26:15
 */
public interface AutoTransable<V extends VO> {

    /**
     * 根据 ids 查询数据列表
     * @param ids 编号数组
     * @return 数据列表
     */
    default List<V> selectByIds(List<? extends Object> ids){
        return new ArrayList<>();
    }
    ....
}
@Trans 注解

@Trans(type = TransType.SIMPLE,target = Users.class,fields = "userName")

  1. type表示翻译类型 – 简单翻译

  2. target表示要翻译出来的结果字段在哪个表中(对应的实体类)

  3. fields表示对应翻译的是哪个字段

  4. ref—-将翻译出来的数据映射到本实体类的某个属性

    refs—-将翻译出来的数据映射到本实体类的多个属性上

    alias—-别名,解决翻译结果字段名重名问题

准备一张设备表device和一张用户表users,其中device表中的 user_id 字段关联了users表中的id字段。

  • 这里的target表示将 userId 翻译为 Users 表的field字段"userName", "phone"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Device.java

@Data
//实现TransPojo  接口,代表这个类需要被翻译或者被当作翻译的数据源
public class Device extends BaseEntity implements TransPojo {
    
    private Long id;
    private String deviceName;
    
	//SIMPLE 翻译,用于关联其他的表进行翻译,userName和phone 为 Users 的字段
    @Trans(type = TransType.SIMPLE, target = Users.class, fields = {"userName", "phone"})
    private Long userId;
 
}

User 表

1
2
3
4
5
6
7
8
9
10
11
// User.java

@Data
//实现TransPojo  接口,代表这个类需要被翻译或者被当作翻译的数据源
public class Users extends BaseEntity implements TransPojo {
    
    private Long id;
 
    private String userName;
    private String phone;
}
@TransMethodResult

@TransMethodResult 注解:用于将翻译结果映射到结果集中。

  • 一般情况下,由于easy-trans框架是将结果集映射到前端的,所以当后端需要得到结果集进行查询,导出等操作时值为null,所以需要在调用方法时就将结果映射,需要使用该接口。
1
2
3
4
5
6
7
8
9
10
11
//DeviceController.java
    
	@Autowired
    private DeviceService deviceService;
    
    @GetMapping
    @TransMethodResult  //用于将翻译的结果映射到transMap中展示
    public R<List<Device>> getDeviceList() {
        //list()是Mybais plus提供的方法
        List<Device> deviceList = deviceService.list();
    }
查询结果

如下:

  • 发现userId已经成功地被翻译成了userName和phone,并将翻译的结果封装在了transMap中。

a401f84cf39d1d3b3b7e5db51ef8ebb5.png

平级模式

如果想让userName、phone在json中和userId同级展示,可以使用平铺模式:

在application.xml中开局平铺模式

1
2
easy-trans:
  is-enable-tile: true #启用平铺模式
  • 此时再看结果,发现 userName、phone和userId是同级

Mapper 接口

  • 默认配置下,MyBatis Mapper XML 需要写在各 yudao-module-xxx-server 模块的 resources/mapper 目录下。
  • 简单的单表查询,优先在 Mapper 中通过 default 方法实现。
  • 不要在 Controller、Service 中,直接进行 MyBatis Plus 操作。建议封装到对应的 Mapper 中,这样会更加简洁干净可管理。否则会导致:
    1. 会导致 Service 中的代码越来越乱,无法聚焦业务逻辑。逻辑里遍布了各种查询,无法统一管理实际有哪些查询条件。
    2. Service 会存在很多相同且重复的 SELECT 查询逻辑,无法更好的实现 SELECT 查询的复用
  • Mapper 的 SELECT 查询方法的命名,采用 Spring Data 的 “Query methods” (opens new window)策略,方法名使用 selectBy查询条件 规则。
  示例
错误 img
正确 img

BaseMapper 接口

cn.iocoder.mybatis.mapper 包路径下,创建 UserMapper 接口。

UserMapper (通过继承 BaseMapper<User>),可以自动生成常规的 CRUD 操作,立即拥有了所有的 CRUD 操作方法。可以在服务层中直接使用这些方法,无需再编写任何 SQL 语句。

  • 另外,BaseMapperX (opens new window)接口,继承 MyBatis Plus 的 BaseMapper 接口,提供更强的 CRUD 操作能力

  • Mybatis Plus 通过 lambda 表达式获取数据库对应的列名。如,UserDO::getName

更多 BaseMapper 提供的接口方法,可看看 《MyBatis-Plus 文档 —— CRUD 接口》

BaseMapper 提供的常用方法有:

  1. 四个 CRUD 方法:
    1. #insert(UserDO user)
    2. #updateById(UserDO user)
    3. #deleteById(@Param("id") Integer id)
    4. #selectById(@Param("id") Integer id)
  2. 条件查询:

QueryWrapper 是 MyBatis-Plus 提供的一个工具类,用于构建查询条件

  • selectList 方法根据条件查询所有符合条件的记录。
1
2
3
4
5
6
7
8
9
10
11
// 构建查询条件
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("username", "john_doe");  // 设置条件:用户名为 john_doe
 
// 执行查询
List<User> users = userMapper.selectList(queryWrapper);
 
// 输出查询结果
for (User u : users) {
    System.out.println(u.getUsername());
}
BaseMapperX 接口

在 MyBatis Plus 的 BaseMapper 的基础上拓展,提供更多的能力。

封装的方法有:

  • selectPage()
    • selectJoinPage()
  • selecctOne()
    • selectFirstOne()
  • selectCount()
  • selectList()
  • insertBatch():批量插入,适合大量数据插入
  • updateBatch()
  • delete()
    • deleteBatch()

如,selectOne 方法,使用指定条件,查询单条记录。

  1. BaseMapperX 中封装 QueryWrapper
  2. MemberUserMapper 继承 BaseMapperX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public interface BaseMapperX<T> extends MPJBaseMapper<T> {
    
	default T selectOne(String field, Object value) {
        return selectOne(new QueryWrapper<T>().eq(field, value));
    }
    
    // 使用 LambdaQueryWrapper 调用,如 selectOne(MemberUserDO::getMobile, mobile)
    default T selectOne(SFunction<T, ?> field, Object value) {
        return selectOne(new LambdaQueryWrapper<T>().eq(field, value));
    }
}

@Mapper
public interface MemberUserMapper extends BaseMapperX<MemberUserDO> {

    default MemberUserDO selectByMobile(String mobile) {
        return selectOne(MemberUserDO::getMobile, mobile);
    }

    default List<MemberUserDO> selectListByNicknameLike(String nickname) {
        return selectList(new LambdaQueryWrapperX<MemberUserDO>()
                .likeIfPresent(MemberUserDO::getNickname, nickname));
    }
}

常见条件查询有:

SelectOne

  1. 对于#selectByUsername(@Param("username") String username)方法,使用了QueryWrapper<T>构造相对灵活的条件,这样一些动态 SQL 就无需在 XML 中编写。
    • 建议 1 :使用 QueryWrapper 拼接动态条件(如用#selectList(Wrapper<T> queryWrapper) 等方法)。
    • 建议 2 :因为 QueryWrapper 暂时不支持一些类似 <if /> 等 MyBatis 的 OGNL 表达式,可以通过继承 QueryWrapper 类,封装 QueryWrapperX 类。
    • 更多 QueryWrapper 提供的拼接方法,可以看 《MyBatis-Plus 文档 —— 条件构造器》
  2. 对于#selectPageByCreateTime(IPage<UserDO> page, @Param("createTime") Date createTime)方法,是额外添加的,用于演示 MyBatis-Plus 提供的分页插件。
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
// UserMapper.java

@Repository
public interface UserMapper extends BaseMapper<UserDO> {

    default UserDO selectByUsername(@Param("username") String username) {
        return selectOne(new QueryWrapper<UserDO>().eq("username", username));
    }

    // 实际也可以使用 MyBatis-Plus 的 QueryWrapper 很方便的实现,
    // 这里仅仅是为了演示在 MyBatis-Plus 混合使用 XML 。
    List<UserDO> selectByIds(@Param("ids") Collection<Integer> ids);

    default IPage<UserDO> selectPageByCreateTime(IPage<UserDO> page, @Param("createTime") Date createTime) {
        return selectPage(page,
                new QueryWrapper<UserDO>().gt("create_time", createTime));
    }
}

@Mapper
public interface ProductSkuMapper extends BaseMapperX<ProductSkuDO> {

    @Select("SELECT * FROM product_sku WHERE id = #{id}")
    ProductSkuDO selectByIdIncludeDeleted(@Param("id") Long id);
    
    ...
UserMapper.xml

resources/mapper 路径下,创建 UserMapper.xml 配置文件。

  • 是不是一下子,瘦了!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.iocoder.springboot.lab12.mybatis.mapper.UserMapper">

    <sql id="FIELDS">
        id, username, password, create_time
    </sql>

    <select id="selectByIds" resultType="UserDO">
        SELECT
            <include refid="FIELDS" />
        FROM users
        WHERE id IN
            <foreach item="id" collection="ids" separator="," open="(" close=")" index="">
                #{id}
            </foreach>
    </select>

</mapper>
简单测试

创建 UserMapperTest 测试类,来测试一下简单的 UserMapper 的每个操作。

  • 多了一个分页的单元测试方法。
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
// UserMapperTest.java

@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
public class UserMapperTest {

    @Autowired
    private UserMapper userMapper;

    @Test
    public void testInsert() {
        // UUID.randomUUID()
        UserDO user = new UserDO().setUsername(UUID.randomUUID().toString())
                .setPassword("nicai").setCreateTime(new Date())
                .setDeleted(0); // 一般情况下,是否删除,可以全局枚举下。
        userMapper.insert(user);
    }

    @Test
    public void testUpdateById() {
        UserDO updateUser = new UserDO().setId(1)
                .setPassword("wobucai");
        userMapper.updateById(updateUser);
    }

    @Test
    public void testDeleteById() {
        userMapper.deleteById(2);
    }


    @Test
    public void testSelectByUsername() {
        userMapper.selectByUsername("yunai");
    }

    @Test
    public void testSelectByIds() {
        List<UserDO> users = userMapper.selectByIds(Arrays.asList(1, 3));
        System.out.println("users:" + users.size());
    }

    @Test
    public void testSelectPageByCreateTime() {
        IPage<UserDO> page = new Page<>(1, 10);
        Date createTime = new Date(2018 - 1990, Calendar.FEBRUARY, 24); // 临时 Demo ,实际不建议这么写
        page = userMapper.selectPageByCreateTime(page, createTime);
        System.out.println("users:" + page.getRecords().size());
    }

}

selectCount

#selectCount(...) (opens new window)方法,使用指定条件,查询记录的数量。

selectCount 示例

selectList

#selectList(...) (opens new window)方法,使用指定条件,查询多条记录。

mybatis_011

selectByIds

select(List)ByIds

  • 通过 Collection ids 获取 List。
1
2
3
4
5
6
7
8
//ProductSkuServiceImpl.java

// 更新 SPU 库存
List<ProductSkuDO> skus = productSkuMapper.selectByIds(
    convertSet(updateStockReqDTO.getItems(), ProductSkuUpdateStockReqDTO.Item::getId));
Map<Long, Integer> spuStockIncrCounts = ProductSkuConvert.INSTANCE.convertSpuStockMap(
    updateStockReqDTO.getItems(), skus);
productSpuService.updateSpuStock(spuStockIncrCounts);

selectPage 分页

见下

insertBatch

#insertBatch(...)方法,遍历数组,逐条插入数据库中,适合少量数据插入,或者对性能要求不高的场景。

为什么不使用 insertBatchSomeColumn 批量插入?

  • 只支持 MySQL 数据库。其它 Oracle 等数据库使用会报错,可见 InsertBatchSomeColumn (opens new window)说明。
  • 未支持多租户。插入数据库时,多租户字段不会进行自动赋值。

insertBatch 示例

批量插入

绝大多数场景下,推荐使用 MyBatis Plus 提供的 IService 的 #saveBatch() (opens new window)方法。示例 PermissionServiceImpl如下:

  • XxxBatchInsertMapper

saveBatch 示例

MPJBaseMapper 连表 JOIN

MyBatis Plus Join 的基础接口,提供连表 Join 能力。

1
2
public interface MPJBaseMapper<T> extends BaseMapper<T>, JoinMapper<T> {
}
多表查询

尽量避免数据库的连表(多表)查询,而是采用多次查询 + Java 内存拼接的方式替代。

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
@Tag(name = "管理后台 - 用户")
@RestController
@RequestMapping("/system/user")
@Validated
public class UserController {

    @Resource
    private AdminUserService userService;
    @Resource
    private DeptService deptService;
    
	@GetMapping("/page")
    @Operation(summary = "获得用户分页列表")
    @PreAuthorize("@ss.hasPermission('system:user:query')")
    public CommonResult<PageResult<UserRespVO>> getUserPage(@Valid UserPageReqVO pageReqVO) {
        // 获得拼接需要的数据,获得用户分页列表
        PageResult<AdminUserDO> pageResult = userService.getUserPage(pageReqVO);
        if (CollUtil.isEmpty(pageResult.getList())) {
            return success(new PageResult<>(pageResult.getTotal()));
        }
        // 拼接结果返回,拼接数据
        Map<Long, DeptDO> deptMap = deptService.getDeptMap(
                convertList(pageResult.getList(), AdminUserDO::getDeptId));
        return success(new PageResult<>(UserConvert.INSTANCE.convertList(pageResult.getList(), deptMap),
                pageResult.getTotal()));
    }
}

UserController 示例

条件构造器

Mybatis Plus 通过 lambda 表达式获取数据库对应的列名。如,UserDO::getName

MyBatis-Plus 提供了强大的条件构造器,使得在查询数据库时可以灵活地构建条件,而无需手动编写复杂的 SQL 语句。

  • 主要通过 Wrapper 接口、及其常用实现类 QueryWrapperLambdaQueryWrapper实现条件查询

Wrapper 接口

Wrapper 是 MyBatis-Plus 提供的条件构造器接口,用于构建动态 SQL。

  • 有多个实现类,其中最常用的是 QueryWrapperLambdaQueryWrapper
  • 常用于复杂查询,比如 selectPage 查询方法。

QueryWrapper

QueryWrapper 是 MyBatis-Plus 提供的一个通用条件构造器,用于以非 Lambda 表达式的方式构建查询条件。

常用方法:

  • eq: 等于
  • ne: 不等于
  • gt、ge: 大于、大于等于
  • lt、le: 小于、 小于等于
  • in: 在指定范围内
  • inSql: 允许使用子查询的结果集作为 IN 条件的范围
  • between: 在两者之间
  • like: 模糊查询
  • or、and: 或、并且条件
  • isNull、isNotNull: 判断字段是否为 NULL
  • orderByAsc、orderByDesc: 升序排序、 降序排序

示例:

1
2
3
4
5
6
7
8
9
10
11
12
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper
    .eq("name", "张三")  // name 等于 张三。 eq也可以写三个参数, 第一个参数是boolean, false 表示这个条件不起作用, true 表示起作用
    .ge("age", 18)       // age 大于等于 18
    .like("email", "gmail.com")  // email 包含 gmail.com
    .orderByDesc("create_time"); // 按 create_time 降序排列
 
List<User> users = userMapper.selectList(queryWrapper);

//查询学生
QueryWrapper<Student> queryWrapper = new QueryWrapper();
queryWrapper.lambda().eq(Student::getName, 老王);

LambdaQueryWrapper

LambdaQueryWrapperQueryWrapperLambda 版本,用于在构建条件时避免使用字符串来指定字段,增加了类型安全性。

  • 使用字段的 Lambda 表达式来构建条件。
  • 通过方法引用的方式来使用实体字段名,避免直接写数据库表字段名时的错写名字。

三种方式:

  1. LambdaQueryWrapper<T>方式
  2. QueryWrapper<实体>().lambda()方式
  3. Wrappers.<实体>lambdaQuery()方式

示例:

1
2
3
4
5
6
7
8
LambdaQueryWrapper<User> lambdaQuery = new LambdaQueryWrapper<>();
lambdaQuery
    .eq(User::getName, "张三")  // name 等于 张三
    .ge(User::getAge, 18)       // age 大于等于 18
    .like(User::getEmail, "gmail.com")  // email 包含 gmail.com
    .orderByDesc(User::getCreateTime); // 按 create_time 降序排列
 
List<User> users = userMapper.selectList(lambdaQuery);

LambdaQueryWrapper 条件构造器

LambdaQueryWrapperX

继承 MyBatis Plus 的条件构造器,拓展了 LambdaQueryWrapperX (opens new window)QueryWrapperX (opens new window)类(重写父类方法,方便链式调用)。

  • 主要是增加 xxxIfPresent 方法,用于判断值不存在的时候,不要拼接到条件中。
  • apply()
  • setSql():
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
default Long selectCountByTagId(Long tagId) {
    return selectCount(new LambdaQueryWrapperX<MemberUserDO>()
            .apply("FIND_IN_SET({0}, tag_ids)", tagId));
}

/**
     * 更新用户积分(增加)
     *
     * @param id        用户编号
     * @param incrCount 增加积分(正数)
     */
    default void updatePointIncr(Long id, Integer incrCount) {
        Assert.isTrue(incrCount > 0);
        LambdaUpdateWrapper<MemberUserDO> lambdaUpdateWrapper = new LambdaUpdateWrapper<MemberUserDO>()
                .setSql(" point = point + " + incrCount)
                .eq(MemberUserDO::getId, id);
        update(null, lambdaUpdateWrapper);
    }

xxxIfPresent 方法

具体的使用示例如下:

LambdaQueryWrapperX 使用示例

MPJLambdaWrapper

Join QueryWrapper

UpdateWrapperLambdaUpdateWrapper

这两个类分别是用于构建更新条件的构造器,功能与 QueryWrapperLambdaQueryWrapper 类似,但用于 UPDATE 操作。

示例:

1
2
3
4
5
6
UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();
updateWrapper
    .eq("name", "张三")
    .set("age", 30);  // 将年龄更新为 30
 
userMapper.update(null, updateWrapper);

具体用法可参考 Mybatis plus 官网 在这里插入图片描述

分页实现

以 [系统管理 -> 租户管理 -> 租户列表] 菜单为例子,讲解它的分页 + 搜索的实现。

前端分页实现

Vue 界面

界面 tenant/index.vue (opens new window)相关的代码如下:

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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
<template>
    <!-- 搜索工作栏 -->
    <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
      <el-form-item label="租户名" prop="name">
        <el-input v-model="queryParams.name" placeholder="请输入租户名" clearable @keyup.enter.native="handleQuery"/>
      </el-form-item>
      <el-form-item label="联系人" prop="contactName">
        <el-input v-model="queryParams.contactName" placeholder="请输入联系人" clearable @keyup.enter.native="handleQuery"/>
      </el-form-item>
      <el-form-item label="联系手机" prop="contactMobile">
        <el-input v-model="queryParams.contactMobile" placeholder="请输入联系手机" clearable @keyup.enter.native="handleQuery"/>
      </el-form-item>
      <el-form-item label="租户状态" prop="status">
        <el-select v-model="queryParams.status" placeholder="请选择租户状态" clearable>
          <el-option v-for="dict in this.getDictDatas(DICT_TYPE.COMMON_STATUS)"
                       :key="dict.value" :label="dict.label" :value="dict.value"/>
        </el-select>
      </el-form-item>
      <el-form-item>
        <el-button type="primary" icon="el-icon-search" @click="handleQuery">搜索</el-button>
        <el-button icon="el-icon-refresh" @click="resetQuery">重置</el-button>
      </el-form-item>
    </el-form>
    
    <!-- 列表 -->
    <el-table v-loading="loading" :data="list">
        <!-- 省略每一列... -->
    </el-table>
    
    <!-- 分页组件 -->
    <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNo" :limit.sync="queryParams.pageSize" 
                @pagination="getList"/>

</template>

<script>
import { getTenantPage } from "@/api/system/tenant";

export default {
	name: "Tenant",
	components: {},
	data() {
      // 遮罩层
      return {
        // 遮罩层
        loading: true,
        // 显示搜索条件
        showSearch: true,
        // 总条数
        total: 0,
        // 租户列表
        list: [],
        // 查询参数
        queryParams: {
          pageNo: 1,
          pageSize: 10,
          // 搜索条件
          name: null,
          contactName: null,
          contactMobile: null,
          status: undefined,
        },
      }
	},
	created() {
	  this.getList();
	},
	methods: {
	  /** 查询列表 */
	  getList() {
	    this.loading = true;
	    // 处理查询参数
	    let params = {...this.queryParams};
		// 执行查询
	    getTenantPage(params).then(response => {
		  this.list = response.data.list;
		  this.total = response.data.total;
		  this.loading = false;
		});
      },
      /** 搜索按钮操作 */
      handleQuery() {
        this.queryParams.pageNo = 1;
        this.getList();
      },
      /** 重置按钮操作 */
      resetQuery() {
        this.resetForm("queryForm");
        this.handleQuery();
      }
    }
}
</script>
API 请求

请求 system/tenant.js (opens new window)相关的代码如下:

1
2
3
4
5
6
7
8
9
10
import request from '@/utils/request'

// 获得租户分页
export function getTenantPage(query) {
  return request({
    url: '/system/tenant/page',
    method: 'get',
    params: query
  })
}

后端分页实现

后端:基于 MyBatis Plus 分页功能,二次封装。

Controller 接口

TenantController (opens new window)类中,定义 /admin-api/system/tenant/page 接口。

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
@Tag(name = "管理后台 - 租户")
@RestController
@RequestMapping("/system/tenant")
public class TenantController {

    @Resource
    private TenantService tenantService;

    @GetMapping("/page")
    @Operation(summary = "获得租户分页")
    @PreAuthorize("@ss.hasPermission('system:tenant:query')")
    public CommonResult<PageResult<TenantRespVO>> getTenantPage(@Valid TenantPageReqVO pageVO) {
        PageResult<TenantDO> pageResult = tenantService.getTenantPage(pageVO);
        return success(TenantConvert.INSTANCE.convertPage(pageResult));
    }
}


@GetMapping("/page")
@Operation(summary = "获得交易订单分页")
@PreAuthorize("@ss.hasPermission('trade:order:query')")
public CommonResult<PageResult<TradeOrderPageItemRespVO>> getOrderPage(TradeOrderPageReqVO reqVO) {
    // 查询订单
    PageResult<TradeOrderDO> pageResult = tradeOrderQueryService.getOrderPage(reqVO);
    if (CollUtil.isEmpty(pageResult.getList())) {
        return success(PageResult.empty());
    }

    // 查询用户信息
    Set<Long> userIds = CollUtil.unionDistinct(convertList(pageResult.getList(), TradeOrderDO::getUserId),
                                               convertList(pageResult.getList(), TradeOrderDO::getBrokerageUserId, Objects::nonNull));
    Map<Long, MemberUserRespDTO> userMap = memberUserApi.getUserMap(userIds);
    // 查询订单项
    List<TradeOrderItemDO> orderItems = tradeOrderQueryService.getOrderItemListByOrderId(
        convertSet(pageResult.getList(), TradeOrderDO::getId));
    // 最终组合
    return success(TradeOrderConvert.INSTANCE.convertPage(pageResult, orderItems, userMap));
}

// TradeOrderQueryServiceImpl.java
@Override
public List<TradeOrderItemDO> getOrderItemListByOrderId(Collection<Long> orderIds) {
    if (CollUtil.isEmpty(orderIds)) {
        return Collections.emptyList();
    }
    return tradeOrderItemMapper.selectListByOrderId(orderIds);
}

// TradeOrderItemMapper.java
default List<TradeOrderItemDO> selectListByOrderId(Collection<Long> orderIds) {
    return selectList(TradeOrderItemDO::getOrderId, orderIds);
}

// BaseMapperX.java
default List<T> selectList(SFunction<T, ?> field, Collection<?> values) {
    if (CollUtil.isEmpty(values)) {
        return CollUtil.newArrayList();
    }
    return selectList(new LambdaQueryWrapper<T>().in(field, values));
}
分页参数 PageParam

分页请求,需要继承 PageParam (opens new window)类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Schema(description="分页参数")
@Data
public class PageParam implements Serializable {

    private static final Integer PAGE_NO = 1;
    private static final Integer PAGE_SIZE = 10;

    @Schema(description = "页码,从 1 开始", required = true, example = "1")
    @NotNull(message = "页码不能为空")
    @Min(value = 1, message = "页码最小值为 1")
    private Integer pageNo = PAGE_NO;

    @Schema(description = "每页条数,最大值为 100", required = true, example = "10")
    @NotNull(message = "每页条数不能为空")
    @Min(value = 1, message = "每页条数最小值为 1")
    @Max(value = 100, message = "每页条数最大值为 100")
    private Integer pageSize = PAGE_SIZE;

}
分页请求 VO

分页请求VO,分页条件,在子类中进行定义。以 TenantPageReqVO 举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Schema(description = "管理后台 - 租户分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class TenantPageReqVO extends PageParam {

    @Schema(description = "租户名", example = "芋道")
    private String name;

    @Schema(description = "联系人", example = "芋艿")
    private String contactName;

    @Schema(description = "联系手机", example = "15601691300")
    private String contactMobile;

    @Schema(description = "租户状态(0正常 1停用)", example = "1")
    private Integer status;

    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
    @Schema(description = "创建时间")
    private LocalDateTime[] createTime;
}
分页结果 PageResult

分页结果 PageResult (opens new window)类,代码如下:

1
2
3
4
5
6
7
8
9
10
@Schema(description = "分页结果")
@Data
public final class PageResult<T> implements Serializable {

    @Schema(description = "数据", required = true)
    private List<T> list;

    @Schema(description = "总量", required = true)
    private Long total;
}
Mapper

针对 MyBatis Plus 分页查询的二次分装,在 BaseMapperX 中实现,目的是使用项目自己的分页封装

  • 【入参】查询前,将项目的分页参数 PageParam,转换成 MyBatis Plus 的 IPage 对象
  • 【出参】查询后,将 MyBatis Plus 的分页结果 IPage,转换成项目的分页结果 PageResult

在 BaseMapperX 中:

BaseMapperX 实现

具体的使用示例,可见 TenantMapper类中,定义 selectPage 查询方法

1
2
3
4
5
6
7
8
9
10
11
12
13
@Mapper
public interface TenantMapper extends BaseMapperX<TenantDO> {

    default PageResult<TenantDO> selectPage(TenantPageReqVO reqVO) {
        return selectPage(reqVO, new LambdaQueryWrapperX<TenantDO>()
        	.likeIfPresent(TenantDO::getName, reqVO.getName()) // 如果 name 不为空,则进行 like 查询
            .likeIfPresent(TenantDO::getContactName, reqVO.getContactName())
            .likeIfPresent(TenantDO::getContactMobile, reqVO.getContactMobile())
            .eqIfPresent(TenantDO::getStatus, reqVO.getStatus()) // 如果 status 不为空,则进行 = 查询
            .betweenIfPresent(TenantDO::getCreateTime, reqVO.getBeginCreateTime(), reqVO.getEndCreateTime()) // 如果 create 不为空,则进行 between 查询
            .orderByDesc(TenantDO::getId)); // 按照 id 倒序
    }
}
IPage

IPage<实体>转 IPage<Vo>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
 * 根据用户姓名分页查询用户
 *
 * @param userQo
 * @return
 */
@Override
public IPage<UserVo> selectPageAll(UserQo userQo) {
    int page = userQo.getPage();
    int limit = userQo.getLimit();

    IPage<User> userIPage = cpWalletLogMapper.selectPage(new Page<>(page, limit),
            new LambdaQueryWrapper<User>()
                    .like(User::getName, userQo.getUserName())
                    .orderByAsc(User::getId)
    );

    return  userIPage .convert(User -> ConvertUtils.beanCopy(User, UserVo.class));
}

高级查询

基于 userMapper.selectMaps(queryWrapper)

分组查询

示例 :按 age 分组并统计人数

假设想统计各个年龄段的人数,可以使用如下代码:

1
2
3
4
5
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.select("age", "COUNT(*) as count");
queryWrapper.groupBy("age");
 
List<Map<String, Object>> result = userMapper.selectMaps(queryWrapper);

生成的 SQL:

1
SELECT age, COUNT(*) as count FROM user GROUP BY age;

聚合查询

示例 :按 age 分组并过滤统计结果(HAVING

如果只想统计人数大于 1 的年龄段,可以添加 HAVING 条件:

1
2
3
4
5
6
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.select("age", "COUNT(*) as count");
queryWrapper.groupBy("age");
queryWrapper.having("COUNT(*) > 1");
 
List<Map<String, Object>> result = userMapper.selectMaps(queryWrapper);

生成的 SQL:

1
SELECT age, COUNT(*) as count FROM user GROUP BY age HAVING COUNT(*) > 1;

排序查询

示例 1:按多字段排序,并指定是否为空

假设想按 age 升序排序,并希望将 name 为空的记录排在前面,可以使用如下代码:

1
2
3
4
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.orderByAsc("age").orderByAsc("name", true);
 
List<User> result = userMapper.selectList(queryWrapper);

生成的 SQL:

1
SELECT * FROM user ORDER BY age ASC, name ASC;
示例 2:按 age 升序和 name 降序组合排序

假设想先按年龄升序排序,再按姓名降序排序,可以使用如下代码:

1
2
3
4
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.orderByAsc("age").orderByDesc("name");
 
List<User> result = userMapper.selectList(queryWrapper);

生成的 SQL:

1
SELECT * FROM user ORDER BY age ASC, name DESC;

逻辑查询

func 方法是 MyBatis-Plus 提供的一个非常灵活的功能,它允许将一段自定义的逻辑包装到查询条件中。

  • 这对于需要根据不同的条件来动态构建查询的场景特别有用。
1. func 方法的基本用法

func 方法接收一个 Consumer,参数是 QueryWrapper(或 LambdaQueryWrapper)的一个实例。

  • 可以在这个 Consumer 中编写自定义的逻辑,并根据不同的条件来动态地添加或修改查询条件。

语法结构:

1
2
3
4
5
6
7
8
queryWrapper.func(wrapper -> {
    // 在这里编写自定义逻辑
    if (condition) {
        wrapper.eq("column", value);
    } else {
        wrapper.ne("column", value);
    }
});

主要参数:

  • Consumer<QueryWrapper>Consumer<LambdaQueryWrapper>:这是一个函数式接口,允许传入一个 Lambda 表达式或方法引用。可以在这个接口的 accept 方法中实现自己的逻辑。

实际应用场景:

  • 假设有一个用户查询接口,允许用户根据不同的条件来过滤结果,例如按 id 或按 name

  • 可以使用 func 来根据用户输入动态地构建查询条件。

1
2
3
4
5
6
7
8
9
LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.func(wrapper -> {
    if (userInput != null && userInput.isValid()) {
        wrapper.eq(User::getName, userInput.getName());
    } else {
        wrapper.ne(User::getId, 1);
    }
});
List<User> users = userMapper.selectList(lambdaQueryWrapper);
  • 示例解释:
    • 如果 userInput 非空且有效,则查询条件为 name = userInput.getName()
    • 否则,查询条件为 id != 1
2. andor 的使用

在 MyBatis-Plus 中,andor 用于在构建查询条件时处理多条件的逻辑运算。它们允许在查询中组合多个条件,以实现复杂的查询逻辑。

and 方法

and 方法:用于将多个查询条件通过逻辑“与” (AND) 连接在一起。将多个条件组合成一个大的 AND 条件,从而要求所有这些条件都必须满足。

示例

假设有一个 User 表,想查询年龄大于 20 且名字为 “Jack” 的用户。可以使用以下代码:

1
2
3
4
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.gt("age", 20).and(wrapper -> wrapper.eq("name", "Jack"));
 
List<User> users = userMapper.selectList(queryWrapper);

生成的 SQL:

1
SELECT * FROM user WHERE age > 20 AND name = 'Jack';

在这个例子中,and 方法的作用是将 .gt("age", 20).eq("name", "Jack") 这两个条件通过 AND 组合在一起。

or 方法

or 方法:用于将多个查询条件通过逻辑“或” (OR) 连接在一起。它将多个条件组合成一个大的 OR 条件,只要其中一个条件满足,就会返回符合的结果。

示例

假设有一个 User 表,想查询年龄大于 20 或名字为 “Jack” 的用户。可以使用以下代码:

1
2
3
4
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.gt("age", 20).or(wrapper -> wrapper.eq("name", "Jack"));
 
List<User> users = userMapper.selectList(queryWrapper);

生成的 SQL:

1
SELECT * FROM user WHERE age > 20 OR name = 'Jack';

在这个例子中,or 方法的作用是将 .gt("age", 20).eq("name", "Jack") 这两个条件通过 OR 组合在一起。

组合使用 andor

还可以结合使用 andor 方法,以构建更复杂的查询。

  • 例如,如果你想查询年龄大于 20 且(名字为 “Jack” 或邮箱为 “test@example.com”)的用户,可以使用以下代码:
1
2
3
4
5
6
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.gt("age", 20)
            .and(wrapper -> wrapper.eq("name", "Jack")
                                    .or().eq("email", "test@example.com"));
 
List<User> users = userMapper.selectList(queryWrapper);

生成的 SQL:

1
SELECT * FROM user WHERE age > 20 AND (name = 'Jack' OR email = 'test@example.com');

在这个例子中,andor 方法的结合使用允许在 age > 20 的基础上,增加一个组合条件 (name = 'Jack' OR email = 'test@example.com')

其他查询

apply 方法

apply 方法:允许直接在 QueryWrapper插入自定义的 SQL 片段。会被添加到 WHERE 子句的末尾。这允许在现有的查询条件基础上,添加更复杂的条件或函数。

基本用法:

1
2
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.apply("DATE_FORMAT(create_time, '%Y-%m-%d') = {0}", "2024-09-01");

在这个示例中:

  • apply 方法接受一个 SQL 片段作为第一个参数,并可以通过 {} 占位符来插入参数。
  • 这里使用了 DATE_FORMAT 函数来格式化 create_time 字段,并将其与特定的日期进行比较。
last 方法

last 方法:用于在生成的 SQL 查询的末尾添加额外的 SQL 片段。

  • 通常用于添加额外的 SQL 语句,如 ORDER BY, LIMIT, OFFSET 等,这些操作是在生成的 SQL 的最后部分进行的。

基本用法:

1
2
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.last("LIMIT 5");

在这个示例中:

  • last 方法添加了一个 LIMIT 5 子句到查询的末尾,用于限制结果集的返回行数。

示例:假设有一个 User 表,并且想要查询所有 age 大于 20 的用户,并且结果按照 id 降序排列,并且只返回前 10 条记录。

  • 可以使用 applylast 方法来实现这个需求:
1
2
3
4
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.gt("age", 20) // age > 20
       .orderByDesc("id") // 按 id 降序排序
       .last("LIMIT 10"); // 限制返回结果为前 10 条

服务层

ServiceImplIService 是 MyBatis-Plus 中用于服务层(Service Layer)的两个重要接口和类,它们帮助简化和规范了与数据库交互的业务逻辑。

IService 接口

IService 是 MyBatis-Plus 提供的一个通用服务接口。

  • 定义了一些常见的 CRUD(Create, Read, Update, Delete)操作,并将这些操作抽象成方法。
  • 这意味着,当使用 IService 接口时,无需自己手动编写这些常见的数据库操作方法。

一些常用方法:

  • boolean save(T entity): 保存一个实体类对象到数据库。
  • boolean removeById(Serializable id): 根据 ID 删除数据。
  • boolean updateById(T entity): 根据 ID 更新数据。
  • T getById(Serializable id): 根据 ID 查询数据。
  • List<T> list(): 查询所有数据。
  • Page<T> page(Page<T> page): 分页查询数据。

ServiceImpl 类

ServiceImpl 是 MyBatis-Plus 提供的一个基础实现类,实现了 IService 接口中的方法。

  • ServiceImpl 通常是被继承的,提供了具体的数据库操作方法的实现。
  • 开发者只需在自己定义的服务实现类中继承 ServiceImpl 类,就可以获得默认的 CRUD 功能。

假设有一个用户表 User,并为其定义了一个实体类 User 和一个 Mapper 接口 UserMapper

  • 可以定义一个服务接口 UserService 和一个服务实现类 UserServiceImpl
定义服务接口:
1
2
3
public interface UserService extends IService<User> {
    // 可以定义一些自定义的服务方法
}
定义服务实现类:
1
2
3
4
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
    // 可以重写 ServiceImpl 中的方法,或者定义更多的业务逻辑
}

在这个例子中,UserServiceImpl 继承了 ServiceImpl<UserMapper, User> 并实现了 UserService 接口。

  • 通过这种方式,UserServiceImpl 类可以直接使用 ServiceImpl 提供的基本 CRUD 方法。

Mapper 和 Service 中有很多的方法,具体用法可以参考 Mybatis plus 官网 在这里插入图片描述

事务处理

在使用注解定义 SQL 查询时,事务管理通常在服务层进行。

  • 可以使用 Spring 的 @Transactional 注解来管理事务。
1
2
3
4
5
6
7
8
9
10
11
@Service
public class UserService {
    @Autowired
    private UserMapper userMapper;
 
    @Transactional
    public void createUser(User user) {
        userMapper.insert(user);
        // 其他逻辑...
    }
}

MapStruct

简介

选型

MyBtatis从数据库中查询的数据映射到domain的实体类上,有时候需要将domain的实体类映射给前端的VO类,用于展示。

因此,可以借助框架或是工具来实现对象的转换,例如说:

  • Hutool 里的BeanUtils.copyProperties()
    • 但是只能转换类中字段名和类型都一样的字段。
    • 而且 由于采用的是反射,实际上当重复调用时效率比较低。(实际测试在生成 次数为1000000时需要1.6秒,而使用MapStruct仅需要69毫秒)。
  • Spring BeanUtils
  • Apache BeanUtils
  • Dozer
  • Orika
  • MapStruct:通过创建一个 MapStruct Mapper 接口,并定义一个转换接口方法,后续交给 MapStruct 自动生成对象转换的代码即可。
  • ModelMapper
  • JMapper

MapStruct

MapStruct解决的问题:手动创建bean映射器非常耗时。 该库可以自动生成Bean映射器类。

MapStruct是一个开源的基于Java的代码生成器,用于创建实现 Java Bean之间转换的扩展映射器。

  • 使用 MapStruct,只需要(通过@Mapper@Mapping 注解)创建接口,而该库会通过注解在编译过程中自动创建具体的映射实现,
  • 大大减少了通常需要手工编写的样板代码的数量。

用于各个对象实体间的相互转换

  • 例如数据库底层实体 转为页面对象,Model 转为 DTO,DTO 转为其他中间对象、VO 等等,相关转换代码为编译时自动产生的新文件和代码。
  • 大部分属性都是相同的,只有少部分的不同。
  • 两个对象之间相同属性名的会被自动转换。指定特殊情况时,需要通过注解在抽象方法上说明不同属性之间的转换。

转换方法一般均为抽象方法,所以这一类文件一般使用 接口类,或者抽象类均可,官方的介绍一般均使用了接口类文件来完成。

优点

  • 使用纯 Java 方法代替 Java 反射机制快速执行。
  • 编译时类型安全:只能映射彼此的对象和属性,不能映射一个 Order 实体到一个 Customer DTO 中等等。
  • 如果无法映射实体或属性,则在编译时清除错误报告

依赖

1
2
3
4
5
<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct-processor</artifactId>
    <version>1.2.0.Final</version>
 </dependency>

映射

基本映射用法

加入 MapStruct 的转换相关的注解。

  1. Convert 接口类@Mapper:当前类认为是要执行 MapStruct 相关操作的类。标记这个接口作为一个映射接口,并且是编译时MapStruct处理器的入口。
    • 注意这里定义的是抽象类,实际上使用接口类也可以。
  2. 在接口中定义了convert()toDto()方法:该方法接收一个Doctor实例为参数,并返回一个DoctorDto实例。MapStruct 会把 Doctor实例映射到一个DoctorDto实例。
    1. 默认情况下,当源对象与 目标对象拥有一样的属性时会自动转换。
    2. 通过添加参数,MapStruct 会自动在返回的 POJO 实例中加入。
    3. 名称不一样时,在方法上@Mapping:为指定某些特殊映射的注解,
      • source 为入参,源对象的属性名target 为目标对象的属性。二者没有顺序之分。
  3. 通过 XxxMapper.INSTANCE. 入口调用方法:如果要将Doctor实例映射到一个DoctorDto实例,可以这样写:
1
DoctorDto doctorDto = DoctorMapper.INSTANCE.toDto(doctor);
@Mapper
@Mapping
  • qualifiedByName 属性,可以自定义转换方法。
1
2
3
4
5
6
7
8
@Named("convertAreaIdToAreaName")
default String convertAreaIdToAreaName(Integer areaId) {
    return AreaUtils.format(areaId);
}

// 将地区编号转为地区名
@Mapping(source = "areaId", target = "areaName", qualifiedByName = "convertAreaIdToAreaName")
DeliveryPickUpStoreSimpleRespVO convert02(DeliveryPickUpStoreDO bean);
Convert 接口
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
@Mapper
public interface UserMapper { // UserMapperConvert

    UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);

    @Mapping(source = "describe", target = "des")
    PersonVO transToViewObject(PersionDTO persionDTO);
    
    //DTO中没有openid,通过参数在VO中加入
    AppAuthLoginRespVO convert(OAuth2AccessTokenRespDTO bean, String openid);
}

@Mapper
public interface DoctorMapper {
    
    DoctorMapper INSTANCE = Mappers.getMapper(DoctorMapper.class);
    
    //不同字段间映射,不同属性名称
    //Doctor中的specialty字段对应于DoctorDto类的 specialization 。
    @Mapping(source = "doctor.specialty", target = "specialization")
    //多个源类
    @Mapping(source = "education.degreeName", target = "degree")
    DoctorDto toDto(Doctor doctor); 
    DoctorDto convertToDto(Doctor doctor)
}
多个源类

@Mapping 注解还支持多个对象转换为一个对象。示例如下图:

 复杂示例

构建

当构建/编译应用程序时,MapStruct插件会识别出DoctorMapper接口并为其生成一个实现类

  • 这段代码中创建了一个DoctorMapper类型的实例INSTANCE,在生成对应的实现代码后,这就是我们调用的“入口”。
    • INSTANCE:是为了在外面调用该方法, 接口中的属性默认为静态属性所以可以直接调用到。
    • 自动生成的接口的实现可以通过Mapper的class对象获取。按照惯例,接口中会声明一个成员变量INSTANCE,从而让客户端可以访问Mapper接口的实现。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class DoctorMapperImpl implements DoctorMapper {
    @Override
    public DoctorDto toDto(Doctor doctor) {
        if ( doctor == null ) {
            return null;
        }
        DoctorDtoBuilder doctorDto = DoctorDto.builder();

        doctorDto.id(doctor.getId());
        doctorDto.name(doctor.getName());

        return doctorDto.build();
    }
}

DoctorMapperImpl类中包含一个toDto()方法,将Doctor属性值映射到DoctorDto的属性字段中。

注意:可能注意到了上面实现代码中的DoctorDtoBuilder。因为builder代码往往比较长,为了简洁起见,这里省略了builder模式的实现代码。

  • 如果类中包含Builder,MapStruct会尝试使用它来创建实例;
  • 如果没有的话,MapStruct将通过new关键字进行实例化

子对象映射

多数情况下,POJO中不会包含基本数据类型,其中往往会包含其它类。比如说,一个Doctor类中会有多个患者类。

  • 通过 @Mapping 注解指定。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Mapper
public interface PatientMapper {
    PatientMapper INSTANCE = Mappers.getMapper(PatientMapper.class);
    PatientDto toDto(Patient patient);
}

@Mapper(uses = {PatientMapper.class})
public interface DoctorMapper {

    DoctorMapper INSTANCE = Mappers.getMapper(DoctorMapper.class);

    @Mapping(source = "doctor.patientList", target = "patientDtoList")
    @Mapping(source = "doctor.specialty", target = "specialization")
    DoctorDto toDto(Doctor doctor);
}

更新现有实例

有时,我们希望用DTO的最新值更新一个模型中的属性,对目标对象(例子中是DoctorDto)使用@MappingTarget注解,就可以更新现有的实例。

数据类型转换

数据类型映射

MapStruct支持sourcetarget属性之间的数据类型转换。还提供了基本类型及其相应的包装类之间的自动转换。

自动类型转换适用于:

  • 基本类型及其对应的包装类之间。比如, intIntegerfloatFloatlongLongbooleanBoolean 等。
  • 任意基本类型与任意包装类之间。如 intlongbyteInteger 等。
  • 所有基本类型及包装类与String之间。如 booleanStringIntegerStringfloatString 等。
  • 枚举和String之间。
  • Java大数类型(java.math.BigIntegerjava.math.BigDecimal) 和Java基本类型(包括其包装类)与String之间。
  • 其它情况详见MapStruct官方文档
数字格式转换

在进行日期转换的时候,可以通过dateFormat标志指定日期的格式。

除此之外,对于数字的转换,也可以使用numberFormat指定显示格式:

枚举映射

为了在这些枚举项之间建立桥梁,可以使用@ValueMappings注解,可以包含多个@ValueMapping注解。

  • 这里,将source设置为三个具体枚举项之一,并将target设置为CARD

集合映射

List映射
1
2
3
4
@Mapper
public interface DoctorMapper {
    List<DoctorDto> map(List<Doctor> doctor);
}
Set和Map映射
集合映射策略
目标集合实现类型

进阶操作

依赖注入

到目前为止,我们一直在通过getMapper()方法访问生成的映射器:

1
DoctorMapper INSTANCE = Mappers.getMapper(DoctorMapper.class);

但是,如果使用的是Spring,只需要简单修改映射器配置,就可以像常规依赖项一样注入映射器

修改 DoctorMapper 以支持Spring框架:

1
2
@Mapper(componentModel = "spring")
public interface DoctorMapper {}

@Mapper注解中添加(componentModel = "spring"):是为了告诉MapStruct,在生成映射器实现类时,希望它能支持通过Spring的依赖注入来创建。

  • 这样,生成的 DoctorMapperImpl 会带有 @Component 注解,
  • 就不需要在接口中添加 INSTANCE 字段了。
1
2
@Component
public class DoctorMapperImpl implements DoctorMapper {}

只要被标记为@Component,Spring就可以把它作为一个bean来处理,就可以在其它类(如控制器)中通过@Autowire注解来使用它:

1
2
3
4
5
@Controller
public class DoctorController() {
    @Autowired
    private DoctorMapper doctorMapper;
}

如果你不使用Spring,MapStruct也支持Java CDI

1
2
@Mapper(componentModel = "cdi")
public interface DoctorMapper {}

添加默认值

@Mapping 注解有两个很实用的标志就是常量 constant 和默认值 defaultValue

  • 无论source如何取值,都将始终使用常量值;
  • 如果source取值为null,则会使用默认值。

添加表达式

添加自定义方法

创建自定义映射器

@BeforeMapping、@AfterMapping

为了进一步控制和定制化,可以定义 @BeforeMapping@AfterMapping方法。

  • 显然,这两个方法是在每次映射之前和之后执行的。
  • 也就是说,在最终的实现代码中,会在两个对象真正映射之前和之后添加并执行这两个方法。

映射异常处理

映射配置

继承配置
继承逆向配置
0%