大纲 前言 版本说明 本文的教程内容是基于 MyBatis-Plus 3.5.2
版本编写的,若你使用的是 2.x
或其他版本,可能会有部分知识点、案例代码不兼容,一切以 MyBatis-Plus 官方文档为准。
MyBatis-Plus 常用注解 @TableName 描述:表名注解,标识实体类对应的表 使用位置:实体类 1 2 3 4 5 6 7 8 9 @TableName("sys_user") public class User { private Long id; private String name; private Integer age; private String email; }
属性 类型 必须指定 默认值 描述 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
)
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 2 3 4 5 6 7 8 9 10 @TableName("sys_user") public class User { @TableId private Long id; private String name; private Integer age; private String email; }
属性 类型 必须指定 默认值 描述 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_WORKER 分布式全局唯一 ID 长整型类型(请使用 ASSIGN_ID
) UUID32 位 UUID 字符串(请使用 ASSIGN_UUID
) ID_WORKER_STR 分布式全局唯一 ID 字符串类型(请使用 ASSIGN_ID
)
注意
1、AUTO
主键类型必须确保数据库表的主键字段设置了 ID 自增,否则无法生效 2、ASSIGN_ID
主键类型(使用雪花算法生成 ID),与数据库表的主键字段是否设置 ID 自增没有任何关系 3、ASSIGN_ID
和 ASSIGN_UUID
这两种主键类型,只有当插入对象的 ID 为空时,才会自动填充主键值
@TableField 描述:字段注解(非主键) 使用位置:实体类非主键属性 1 2 3 4 5 6 7 8 9 10 11 @TableName("sys_user") public class User { @TableId private Long id; @TableField("nickname") private String name; private Integer age; private String email; }
属性 类型 必须指定 默认值 描述 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_NULL insert into table_a(<if test="columnProperty != null">column</if>) values (<if test="columnProperty != null">#{columnProperty}</if>)
updateStrategy Enum 否 FieldStrategy.DEFAULT
举例:IGNORED update table_a set column=#{columnProperty}
whereStrategy Enum 否 FieldStrategy.DEFAULT
举例:NOT_EMPTY where <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
。
查询数据操作 1 2 3 public interface EmployeeMapper extends BaseMapper <Employee > {}
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 @SpringBootTest public class MyBatisPlusApplicationTest { @Autowired private EmployeeMapper empMapper; @Test public void selectById () { Employee employee = empMapper.selectById(1L ); System.out.println(employee); } @Test public void selectBatchIds () { List<Long> ids = Arrays.asList(1L , 2L , 3L ); List<Employee> list = empMapper.selectBatchIds(ids); list.forEach(System.out::println); } @Test public void selectByMap () { Map<String, Object> columnMap = new HashMap<String, Object>(); columnMap.put("last_name" , "Jim" ); List<Employee> list = empMapper.selectByMap(columnMap); list.forEach(System.out::println); } @Test public void selectList () { List<Employee> list = empMapper.selectList(null ); list.forEach(System.out::println); } }
删除数据操作 1 2 3 public interface EmployeeMapper extends BaseMapper <Employee > {}
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 @SpringBootTest public class MyBatisPlusApplicationTest { @Autowired private EmployeeMapper empMapper; @Test public void deleteById () { Integer deleteResult = empMapper.deleteById(2L ); System.out.println("deleteResult: " + deleteResult); } @Test public void deleteBatchIds () { List<Long> ids = Arrays.asList(2L , 3L ); Integer deleteResult = empMapper.deleteBatchIds(ids); System.out.println("deleteResult: " + deleteResult); } @Test public void deleteByMap () { Map<String, Object> columnMap = new HashMap<String, Object>(); columnMap.put("gender" , "0" ); Integer deleteResult = empMapper.deleteByMap(columnMap); System.out.println("deleteResult: " + deleteResult); } }
插入数据操作 插入数据获取主键值 若数据库表使用的是自增主键,MyBatis 支持在插入数据后返回自增的主键值,详细的使用教程可以看 这里 。值得一提的是,MyBatis-Plus 在插入数据后会自动将主键值回写到 JavaBean 中,不需要任何配置。示例代码如下:
1 2 3 public interface EmployeeMapper extends BaseMapper <Employee > {}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @SpringBootTest public class MyBatisPlusTest { @Autowired private EmployeeMapper empMapper; @Test public void insert () { Employee employee = new Employee("David" , "1" , "david@gmail.com" , 23 ); Integer insertResult = empMapper.insert(employee); System.out.println("insert result: " + insertResult); System.out.println(employee); } }
上述代码执行后的结果如下:
1 2 insert result: 1 Employee [id=41, lastName=David, gender=1, email=david@gmail.com, age=23]
插入数据进行非空判断 MyBatis-Plus 在插入数据的时候,默认会对实体类的每个属性进行非空判断,只有非空的实体类属性会拼接到 SQL 语句中。
1 2 3 4 5 6 7 8 9 10 11 public class Employee { private Long id; private String email; private String lastName; private String gender; private Integer age; ... }
1 2 3 public interface EmployeeMapper extends BaseMapper <Employee > {}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @SpringBootTest public class MyBatisPlusTest { @Autowired private EmployeeMapper empMapper; @Test public void insert () { Employee employee = new Employee(); employee.setLastName("David" ); employee.setEmail("david@gmail.com" ); Integer insertResult = empMapper.insert(employee); System.out.println("insertResult: " + insertResult); System.out.println(employee); } }
观察下述的代码执行结果,可以发现只有非空的实体类属性会拼接到 SQL 语句中。
1 2 3 4 5 ==> Preparing: INSERT INTO t_employee ( last_name, email ) VALUES ( ?, ? ) ==> Parameters: David(String), david@gmail.com(String) <== Updates: 1 insertResult: 1 Employee [id=44, lastName=David, gender=null, email=david@gmail.com, age=null]
若希望新增操作忽略实体类属性的非空判断,可以在实体类的属性上使用 @TableField
注解,并通过 @TableField
注解的 insertStrategy
参数来指定判断策略。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class Employee { private Long id; private String email; private String lastName; @TableField(insertStrategy = FieldStrategy.IGNORED) private String gender; @TableField(insertStrategy = FieldStrategy.IGNORED) private Integer age; ... }
重新执行 Junit 测试代码,观察下述的执行结果,可以发现非空的实体类属性也会拼接到 SQL 语句中。
1 2 3 4 5 ==> Preparing: INSERT INTO t_employee ( last_name, gender, email, age ) VALUES ( ?, ?, ?, ? ) ==> Parameters: David(String), null, david@gmail.com(String), null <== Updates: 1 insertResult: 1 Employee [id=45, lastName=David, gender=null, email=david@gmail.com, age=null]
更新数据操作 更新数据进行非空判断 MyBatis-Plus 在更新数据的时候,默认会对实体类的每个属性进行非空判断,只有非空的属性才会拼接到 SQL 语句中。
1 2 3 4 5 6 7 8 9 10 11 public class Employee { private Long id; private String email; private String lastName; private String gender; private Integer age; ... }
1 2 3 public interface EmployeeMapper extends BaseMapper <Employee > {}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @SpringBootTest public class MyBatisPlusTest { @Autowired private EmployeeMapper empMapper; @Test public void update () { Employee employee = new Employee(); employee.setId(1L ); employee.setLastName("David" ); employee.setEmail("david@gmail.com" ); Integer updateReuslt = empMapper.updateById(employee); System.out.println("updateReuslt: " + updateReuslt); } }
观察下述的代码执行结果,可以发现只有非空的实体类属性会拼接到 SQL 语句中。
1 2 3 4 ==> Preparing: UPDATE t_employee SET last_name=?, email=? WHERE id=? ==> Parameters: David(String), david@gmail.com(String), 1(Long) <== Updates: 1 updateReuslt: 1
若希望更新操作忽略实体类属性的非空判断,可以在实体类的属性上使用 @TableField
注解,并通过 @TableField
注解的 updateStrategy
参数来指定判断策略。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class Employee { private Long id; private String email; private String lastName; @TableField(updateStrategy = FieldStrategy.IGNORED) private String gender; @TableField(updateStrategy = FieldStrategy.IGNORED) private Integer age; ... }
重新执行 Junit 测试代码,观察下述的执行结果,可以发现非空的实体类属性也会拼接到 SQL 语句中。
1 2 3 4 ==> Preparing: UPDATE t_employee SET email=?, last_name=?, gender=?, age=? WHERE id=? ==> Parameters: david@gmail.com(String), David(String), null, null, 1(Long) <== Updates: 1 updateReuslt: 1
通用 Service CRUD 操作 MyBatis-Plus 为通用的 Service CRUD 操作提供了 IService
接口 和 ServiceImpl
实现类,里面封装了常用的 CRUD 方法。本节所需的案例代码,可以直接从 GitHub 下载对应章节 mybatis-plus-lesson-04
。
创建 Service 接口和实现类 1 2 3 4 5 6 public interface IEmployeeService extends IService <Employee > {}
1 2 3 4 5 6 7 @Service public class EmployeeServiceImpl extends ServiceImpl <EmployeeMapper , Employee > implements IEmployeeService {}
调用 Service 接口和实现类 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 @SpringBootTest public class MyBatisPlusApplicationTest { @Autowired private IEmployeeService empService; @Test public void count () { Long count = empService.count(); System.out.println("count: " + count); } @Test public void update () { Employee employee = new Employee(); employee.setId(1L ); employee.setGender("0" ); boolean updateResult = empService.updateById(employee); System.out.println("updateResult: " + updateResult); } @Test public void saveBatch () { List<Employee> list = new ArrayList<>(); for (int i = 0 ; i < 5 ; i++) { Employee emp = new Employee(); emp.setAge(26 ); emp.setGender("1" ); emp.setLastName("user-" + i); list.add(emp); } boolean saveResult = this .empService.saveBatch(list); System.out.println("saveResult: " + saveResult); } @Test public void delete () { boolean deleteResult = this .empService.removeById(11L ); System.out.println("deleteResult: " + deleteResult); } }
数据库扩展介绍 在企业开发中,往往需要选择合适的方案去应对数据规模的增长,以应对数据库逐渐增长的访问压力和数据量。数据库的扩展方式主要包括:业务分库、主从复制、数据库分表、数据库分片、数据库分区。
数据库分表 将不同业务数据分散存储到不同的数据库服务器,能够支撑百万甚至千万用户规模的业务,但如果业务继续发展,同一业务的单表数据也会达到单台数据库服务器的处理瓶颈。例如,淘宝的几亿用户数据,如果全部存放在一台数据库服务器的一张表中,肯定是无法满足性能要求的,此时就需要对单表数据进行拆分。单表数据拆分有两种方式,包括垂直分表和水平分表。示意图如下:
垂直分表 垂直分表适合将表中某些不常用且占了大量空间的列拆分出去。例如,上面示意图中的 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% 的业务需求都只需要趋势递增)