MyBatis-Plus 入门教程之二
大纲
- MyBatis-Plus 入门教程之一
- MyBatis-Plus 入门教程之二
- MyBatis-Plus 入门教程之三
- MyBatis-Plus 入门教程之四
- MyBatis-Plus 入门教程之五
- MyBatis-Plus 入门教程之六
- MyBatis-Plus 入门教程之七
前言
版本说明
本文的教程内容是基于 MyBatis-Plus 3.5.2 版本编写的,若你使用的是 2.x 或其他版本,可能会有部分知识点、案例代码不兼容,一切以 MyBatis-Plus 官方文档为准。
MyBatis-Plus 常用注解
提示
MyBatis-Plus 3.x 注解类包的源码:👉 mybatis-plus-annotation
@TableName
- 描述:表名注解,标识实体类对应的表
- 使用位置:实体类
1 |
|
| 属性 | 类型 | 必须指定 | 默认值 | 描述 |
|---|---|---|---|---|
| value | String | 否 | "" | 表名 |
| schema | String | 否 | "" | Schema |
| keepGlobalPrefix | boolean | 否 | false | 是否保持使用全局的 tablePrefix 的值(当全局 tablePrefix 生效时) |
| resultMap | String | 否 | "" | SQL 映射文件中 resultMap 的 id(用于满足特定类型的实体类对象绑定) |
| autoResultMap | boolean | 否 | false | 是否自动构建 resultMap 并使用(如果设置 resultMap 则不会进行 resultMap 的自动构建与注入) |
| excludeProperty | String[] | 否 | {} | 需要排除的属性名(@since 3.3.1 version) |
关于 autoResultMap 的说明
MyBatis-Plus 会自动构建一个 resultMap 并注入到 MyBatis 里(一般用不上),请注意以下内容:
- 因为 MyBatis-Plus 的底层是 MyBatis,所以 MyBatis-Plus 只是帮开发者注入了常用 CRUD 到 MyBatis 里,注入之前是动态的(根据开发者的 Entity 字段以及注解变化而变化),但是注入之后是静态的(等同于 XML 配置中的内容)
- 对于
typeHandler属性,MyBatis 只支持写在两个地方- 第一个地方:定义在
resultMap里,作用于查询结果的封装 - 第二个地方:定义在
insert和update语句的#{property}中的property后面(例:#{property, typehandler=xxx.xxx.xxx}),并且只作用于当前设置值 - 除了以上两种直接指定
typeHandler的形式,MyBatis 还支持在全局配置文件中自定义typeHandler包的配置,原理是根据开发者的property类型去找其对应的typeHandler并使用。
- 第一个地方:定义在
@TableId
- 描述:主键注解
- 使用位置:实体类主键属性
1 |
|
| 属性 | 类型 | 必须指定 | 默认值 | 描述 |
|---|---|---|---|---|
| value | String | 否 | "" | 主键字段名 |
| type | Enum | 否 | IdType.NONE | 指定主键类型 |
IdType
- 描述:主键类型
| 值 | 描述 |
|---|---|
| AUTO | 数据库 ID 自增,该类型必须确保数据库表的主键字段设置了 ID 自增,否则无法生效 |
| NONE | 无状态,该类型为未设置主键类型(注解里等于跟随全局,全局里约等于 INPUT) |
| INPUT | 用户自行设置主键值,该类型可以通过自己注册自动填充插件进行填充 |
| ASSIGN_ID | 分配 ID,主键类型为 Number [Long 和 Integer] 或 String(@since 3.3.0 version),使用的是 IdentifierGenerator 接口的 nextId 方法(默认实现类为 DefaultIdentifierGenerator,使用的是雪花算法) |
| ASSIGN_UUID | 分配 UUID,主键类型为 String(@since 3.3.0 version),使用的是 IdentifierGenerator 接口的 nextUUID 方法 |
分布式全局唯一 ID 长整型类型(请使用 ASSIGN_ID) | |
32 位 UUID 字符串(请使用 ASSIGN_UUID) | |
分布式全局唯一 ID 字符串类型(请使用 ASSIGN_ID) |
注意
1、AUTO 主键类型必须确保数据库表的主键字段设置了 ID 自增,否则无法生效
2、ASSIGN_ID 主键类型(使用雪花算法生成 ID),与数据库表的主键字段是否设置 ID 自增没有任何关系
3、ASSIGN_ID 和 ASSIGN_UUID 这两种主键类型,只有当插入对象的 ID 为空时,才会自动填充主键值
@TableField
- 描述:字段注解(非主键)
- 使用位置:实体类非主键属性
1 |
|
| 属性 | 类型 | 必须指定 | 默认值 | 描述 |
|---|---|---|---|---|
| value | String | 否 | "" | 数据库字段名 |
| exist | boolean | 否 | true | 是否为数据库表字段 |
| condition | String | 否 | "" | 字段 where 实体查询比较条件,有值设置则按设置的值为准,没有则为默认全局的 %s=#{%s} |
| update | String | 否 | "" | 字段 update set 部分注入,例如:当在 version 字段上注解 update="%s+1",表示更新时会 set version=version+1 (该属性优先级高于 el 属性) |
| insertStrategy | Enum | 否 | FieldStrategy.DEFAULT | 举例:NOT_NULLinsert into table_a(<if test="columnProperty != null">column</if>) values (<if test="columnProperty != null">#{columnProperty}</if>) |
| updateStrategy | Enum | 否 | FieldStrategy.DEFAULT | 举例:IGNOREDupdate table_a set column=#{columnProperty} |
| whereStrategy | Enum | 否 | FieldStrategy.DEFAULT | 举例:NOT_EMPTYwhere <if test="columnProperty != null and columnProperty!=''">column=#{columnProperty}</if> |
| fill | Enum | 否 | FieldFill.DEFAULT | 字段自动填充策略 |
| select | boolean | 否 | true | 是否进行 select 查询 |
| keepGlobalFormat | boolean | 否 | false | 是否保持使用全局的 format 进行处理 |
| jdbcType | JdbcType | 否 | JdbcType.UNDEFINED | JDBC 类型 (该默认值不代表会按照该值生效) |
| typeHandler | Class<? extends TypeHandler> | 否 | UnknownTypeHandler.class | 类型处理器 (该默认值不代表会按照该值生效) |
| numericScale | String | 否 | "" | 指定小数点后保留的位数 |
关于 jdbcType 和 typeHandler 以及 numericScale 的说明
numericScale只生效于update的 SQL 语句jdbcType和typeHandler如果不配合@TableName(autoResultMap = true)一起使用,也只生效于update的 SQL 语句- 对于
typeHandler,如果你的字段类型和 set 进去的类型为equals关系,则只需要让你的typeHandler让 Mybatis 加载到即可,不需要使用注解
FieldStrategy
字段策略 FieldStrategy 的作用主要是在进行新增、更新时,根据配置的策略判断是否对实体对象的值进行空值判断,如果策略为字段不能为空,则不会对为空的字段进行赋值或更新。同样的,在进行 where 条件查询时,根据 whereStrategy 策略判断是否对字段进行空值判断,如果策略为字段不能为空,则为空的字段不会作为查询条件组装到 where 条件中。
| 值 | 描述 |
|---|---|
| IGNORED | 忽略判断 |
| NOT_NULL | 非 NULL 判断 |
| NOT_EMPTY | 非空判断(只对字符串类型字段,其他类型字段依然为非 NULL 判断) |
| DEFAULT | 追随全局配置 |
| NEVER | 不加入 SQL |
FieldFill
- 描述:字段自动填充策略
| 值 | 描述 |
|---|---|
| DEFAULT | 默认不处理 |
| INSERT | 插入时填充字段 |
| UPDATE | 更新时填充字段 |
| INSERT_UPDATE | 插入和更新时填充字段 |
@Version
- 描述:乐观锁注解
- 使用位置:实体类属性
- 使用案例:乐观锁插件
@OrderBy
- 描述:内置 SQL 默认排序规则,优先级低于
wrapper条件查询
| 属性 | 类型 | 必须指定 | 默认值 | 描述 |
|---|---|---|---|---|
| asc | boolean | 否 | false | 是否升序查询 |
| isDesc | boolean | 否 | true | 是否倒序查询 |
| sort | short | 否 | Short.MAX_VALUE | 数字越小越靠前 |
@EnumValue
- 描述:普通枚举类注解
- 使用位置:实体类枚举类型的属性
- 使用案例:通用枚举
@TableLogic
- 描述:表字段逻辑处理注解(逻辑删除)
- 使用场景:数据删除后需要进行数据恢复
- 物理删除:真实删除,将对应数据从数据库中删除,之后查询不到此条被删除的数据
- 逻辑删除:假删除,将对应数据中代表是否被删除字段的状态修改为 “被删除状态”,之后在数据库中仍旧能看到此条数据记录
- 使用案例:逻辑删除
| 属性 | 类型 | 必须指定 | 默认值 | 描述 |
|---|---|---|---|---|
| value | String | 否 | “” | 逻辑未删除时的值 |
| delval | String | 否 | “” | 逻辑删除时的值 |
@KeySequence
- 描述:序列主键策略
oracle - 属性:
value、resultMap
| 属性 | 类型 | 必须指定 | 默认值 | 描述 |
|---|---|---|---|---|
| value | String | 否 | “” | 序列名 |
| clazz | Class | 否 | Long.class | id 的类型,可以指定 String.class,这样返回的 Sequence 值是字符串 1 |
通用 Mapper CRUD 操作
本节所需的案例代码,可以直接从 GitHub 下载对应章节 mybatis-plus-lesson-03。
查询数据操作
- Mapper 接口
1 | public interface EmployeeMapper extends BaseMapper<Employee> { |
- Junit 测试代码
1 |
|
删除数据操作
- Mapper 接口
1 | public interface EmployeeMapper extends BaseMapper<Employee> { |
- Junit 测试代码
1 |
|
插入数据操作
插入数据获取主键值
若数据库表使用的是自增主键,MyBatis 支持在插入数据后返回自增的主键值,详细的使用教程可以看 这里。值得一提的是,MyBatis-Plus 在插入数据后会自动将主键值回写到 JavaBean 中,不需要任何配置。示例代码如下:
- Mapper 接口
1 | public interface EmployeeMapper extends BaseMapper<Employee> { |
- Junit 测试代码
1 |
|
上述代码执行后的结果如下:
1 | insert result: 1 |
插入数据进行非空判断
MyBatis-Plus 在插入数据的时候,默认会对实体类的每个属性进行非空判断,只有非空的实体类属性会拼接到 SQL 语句中。
- 实体类
1 | public class Employee { |
- Mapper 接口
1 | public interface EmployeeMapper extends BaseMapper<Employee> { |
- Junit 测试代码
1 |
|
观察下述的代码执行结果,可以发现只有非空的实体类属性会拼接到 SQL 语句中。
1 | ==> Preparing: INSERT INTO t_employee ( last_name, email ) VALUES ( ?, ? ) |
若希望新增操作忽略实体类属性的非空判断,可以在实体类的属性上使用 @TableField 注解,并通过 @TableField 注解的 insertStrategy 参数来指定判断策略。
- 实体类
1 | public class Employee { |
重新执行 Junit 测试代码,观察下述的执行结果,可以发现非空的实体类属性也会拼接到 SQL 语句中。
1 | ==> Preparing: INSERT INTO t_employee ( last_name, gender, email, age ) VALUES ( ?, ?, ?, ? ) |
更新数据操作
更新数据进行非空判断
MyBatis-Plus 在更新数据的时候,默认会对实体类的每个属性进行非空判断,只有非空的属性才会拼接到 SQL 语句中。
- 实体类
1 | public class Employee { |
- Mapper 接口
1 | public interface EmployeeMapper extends BaseMapper<Employee> { |
- Junit 测试代码
1 |
|
观察下述的代码执行结果,可以发现只有非空的实体类属性会拼接到 SQL 语句中。
1 | ==> Preparing: UPDATE t_employee SET last_name=?, email=? WHERE id=? |
若希望更新操作忽略实体类属性的非空判断,可以在实体类的属性上使用 @TableField 注解,并通过 @TableField 注解的 updateStrategy 参数来指定判断策略。
- 实体类
1 | public class Employee { |
重新执行 Junit 测试代码,观察下述的执行结果,可以发现非空的实体类属性也会拼接到 SQL 语句中。
1 | ==> Preparing: UPDATE t_employee SET email=?, last_name=?, gender=?, age=? WHERE id=? |
通用 Service CRUD 操作
MyBatis-Plus 为通用的 Service CRUD 操作提供了 IService 接口 和 ServiceImpl 实现类,里面封装了常用的 CRUD 方法。本节所需的案例代码,可以直接从 GitHub 下载对应章节 mybatis-plus-lesson-04。
创建 Service 接口和实现类
- Service 接口
1 | /** |
- Service 实现类
1 | /** |
调用 Service 接口和实现类
- Junit 测试代码
1 |
|
数据库扩展介绍
在企业开发中,往往需要选择合适的方案去应对数据规模的增长,以应对数据库逐渐增长的访问压力和数据量。数据库的扩展方式主要包括:业务分库、主从复制、数据库分表、数据库分片、数据库分区。
数据库分表
将不同业务数据分散存储到不同的数据库服务器,能够支撑百万甚至千万用户规模的业务,但如果业务继续发展,同一业务的单表数据也会达到单台数据库服务器的处理瓶颈。例如,淘宝的几亿用户数据,如果全部存放在一台数据库服务器的一张表中,肯定是无法满足性能要求的,此时就需要对单表数据进行拆分。单表数据拆分有两种方式,包括垂直分表和水平分表。示意图如下:

垂直分表
垂直分表适合将表中某些不常用且占了大量空间的列拆分出去。例如,上面示意图中的 nickname 和 description 字段,假设开发一个婚恋网站,用户在筛选其他用户的时候,主要是用 age 和 sex 两个字段进行查询,而 nickname 和 description 两个字段主要用于展示,一般不会在业务查询中用到。description 字段本身又比较占存储空间,因此可以将这两个字段独立到另外一张表中,这样在查询 age 和 sex 时,就能带来一定的性能提升。
水平分表
水平分表适合表行数特别大的表,有的公司要求单表行数超过 5000万 就必须进行分表,这个数字可以作为参考,但并不是绝对标准,关键还是要看表的访问性能。对于一些比较复杂的表,可能超过 1000万 就要分表了;而对于一些简单的表,即使存储数据超过 1亿 行,也可以不分表。但不管怎样,当看到表的数据量达到千万级别时,作为架构师就要警觉起来,因为这很可能是架构的性能瓶颈或者隐患。值得一提的是,水平分表相比垂直分表,会引入更多的复杂性,例如要处理全局唯一的数据 ID(分布式唯一全局 ID)。
提示
更多关于分布式唯一全局 ID 的生成方案可以看 这里。
UUID
UUID 按照 OSF 制定的标准计算,用到了以太网卡地址、纳秒级时间、芯片 ID 码和许多可能的数字,并由以下几部分的组成:当前日期和时间、时钟序列、全局唯一的 IEEE 机器识别号。UUID 的标准形式包含 32 个 16 进制的数字,以连字号分为 5 段,形式为 cb6ce510-74fe-4e18-ac3d-05a5c96d3a0f 的 36 个字符。特别注意,基于 MAC 地址生成 UUID 的算法可能会造成 MAC 地址泄露,这个漏洞曾被用于寻找梅丽莎病毒的制作者位置。
适用场景:只要对存储空间没有苛刻要求的都能够适用,比如各种链路追踪、日志存储等。优点:性能高,本地生成,没有网络消耗。如果只考虑唯一性,那么可以使用。缺点:无序:即无法预测 ID 的生成顺序,不能生成递增的有序数字。ID 长度过长:分布式 ID 一般都是作为主键,但是 MySQL 官方推荐主键尽量越短越好,36 个字符长度的 UUID 不是很推荐。入库性能差:既然分布式 ID 是主键,然后主键是包含索引的,MySQL 的索引是通过 B+ 树来实现的,每一次新的 UUID 数据的插入,为了查询的优化,都会对索引的底层 B+ 树进行修改。因为 UUID 数据是无序的,所以每一次 UUID 的数据插入都会对主键底层的 B+ 树进行很大的修改,这一点非常不好。插入的 ID 完全无序,不但会导致一些中间节点产生分裂(B+ 树索引分裂),也会白白的创造出很多的不饱和节点,这样大大的降低了数据库的插入性能。
主键自增
以最常见的用户 ID 为例,可以按照 1000000 的范围大小进行分段,1 ~ 999999 放到表 1 中,1000000 ~ 1999999 放到表 2 中,以此类推。
复杂点:分段大小的选取。分段太小会导致切分后子表数量过多,增加维护复杂度;分段太大可能会导致单表依然存在性能问题,一般建议分段大小在100万至2000万之间,具体需要根据业务需求选取合适的分段大小。优点:可以随着数据的增加平滑地扩充新的表。例如,现在的用户是 100 万,如果增加到 1000 万,只需要增加新的表就可以了,原有的数据不需要动。缺点:分布不均匀。假如按照 1000 万来进行分表,有可能某个分段实际存储的数据量只有 1 条,而另外一个分段实际存储的数据量有 1000 万条。
取模运算
同样以用户 ID 为例,假如一开始就规划了 10 个数据库表,可以简单地用 user_id % 10 的值来表示数据所属的数据库表编号,ID 为 985 的用户放到编号为 5 的子表中,ID 为 10086 的用户放到编号为 6 的子表中。
复杂点:初始表数量的确定。表数量太多维护比较麻烦,表数量太少又可能导致单表性能存在问题。优点:表分布比较均匀。缺点:扩充新的表很麻烦,所有表的数据都要重新迁移。
雪花算法
雪花算法(SnowFlake)是由 Twitter 公布的分布式主键生成算法,它能够保证不同表的主键的不重复性,以及相同表的主键的有序性。雪花算法的特性是有序、全局唯一、高性能、低延迟(响应时间在 2ms 以内),可在分布式环境(多集群,跨机房)下使用,因此使用雪花算法得到的 ID 是分段组成的:
- 与指定日期的时间差(毫秒级),41 位,够用 69 年
- 集群 ID + 机器 ID,一共 10 位,包括 5 位
datacenterId和 5 位workerId,最多支持 1024 台机器 - 序列号,12 位,每台机器每毫秒内最多产生 4096 个序列号

1bit:符号位,固定是 0,表示全部 ID 都是正整数41bit:时间戳(毫秒数时间差),从指定的日期算起,够用 69 年,用 Long 类型表示的时间戳是从1970-01-01 00:00:00开始算起的10bit:机器 ID,有异地部署,多集群的也可以配置,需要线下规划好各地机房,各集群,各实例 ID 的编号12bit:序列号,前面都相同的话,最多可以支持到 4096 个
优缺点:
适用场景:分布式应用环境的数据主键优点:- 可以根据自身业务特性分配
bit位,非常灵活 - 毫秒数在高位,自增序列在低位,整个 ID 都是趋势递增的
- 不依赖数据库等三方系统,以服务的方式部署,稳定性更高,生成 ID 的效率也是非常高,低延迟
- 可以根据自身业务特性分配
缺点:- 强依赖机器时钟,如果机器的时钟回拨了,会导致生成重复的 ID
- 若生成环境中使用了容器化技术,实例的个数随时有变化,那么 SnowFlake 算法需要一定的改造才能更好地应用到生产环境中
- 在单机上是递增的,但是由于涉及到分布式环境,每台机器上的时钟不可能完全同步(如时钟回拨),有时候可能会出现不是全局递增的情况(此缺点可认为无所谓,一般分布式 ID 只是要求趋势递增,并不会严格要求递增,90% 的业务需求都只需要趋势递增)
