- ScanEntity: 扫描实体
- Reflection Extraction :抽取实体类中的属性和字段
- Analysis Table Name Column:根据实体中的属性字段等来分析表,建立实体与表的关系
- Sql:根据不同需求生成sql语句,并将封装为mapper
- Injection Mybatis Container: 注入mapper到容器
UserMapper继承了BaseMapper接口中的方法,若未自己实现,mp将会帮我们去实现这些方法,并将UserMapper实现类注入容器
对于自定义的方法,需要自己按mybatis的方式实现
@Mapper
public interface UserMapper extends BaseMapper<User> {
/**
* 根据用户id查询用户信息为map
*
* @param id id
* @return {@link Map}<{@link String}, {@link Object}>
*/
Map<String, Object> selectMapById(Long id);
}
<mapper namespace="com.bloom.mp.mapper.UserMapper">
<select id="selectMapById" resultType="map">
select id, name, age, email
from user
where id = #{id};
select>
mapper>
首先用户定义的IUserService层的接口会继承mp所提供的IService 接口中的方法
public interface IUserService extends IService<User> {
// 自定义的其它功能
void otherFun();
}
用户可以选择使用mp已经实现的**ServiceImpl<用户Mapper层接口, 实体类>**中的方法,也可以选择重写它们
还可以实现自定义的接口
public class IUserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
@Override
public boolean save(User entity) {
// 自己重写
return true;
}
// 自定义的其它功能实现
@Override
public void otherFun(){
}
}
本部分(Configuration)的配置大都为 MyBatis 原生支持的配置,这意味着您可以通过 MyBatis XML 配置文件的形式进行配置。
boolean
true
是否开启自动驼峰命名规则(camel case)映射,即从经典数据库列名 A_COLUMN(下划线命名) 到经典 Java 属性名 aColumn(驼峰命名) 的类似映射。
注意
此属性在 MyBatis 中原默认值为 false,在 MyBatis-Plus 中,此属性也将用于生成最终的 SQL 的 select body
如果您的数据库命名符合规则无需使用
@TableField
注解指定数据库字段名
本部分为全局配置
com.baomidou.mybatisplus.annotation.IdType
ASSIGN_ID
全局默认主键类型
String
null
表名前缀
com.baomidou.mybatisplus.core.incrementer.IKeyGenerator
null
表主键生成器(starter 下支持
@bean
注入)
String
null
全局的 entity 的逻辑删除字段属性名,(逻辑删除下有效)
String
1
逻辑已删除值,(逻辑删除下有效)
String
0
逻辑未删除值,(逻辑删除下有效)
com.baomidou.mybatisplus.annotation.FieldStrategy
NOT_NULL
字段验证策略之 insert,在 insert 的时候的字段验证策略
com.baomidou.mybatisplus.annotation.FieldStrategy
NOT_NULL
字段验证策略之 update,在 update 的时候的字段验证策略
com.baomidou.mybatisplus.annotation.FieldStrategy
NOT_NULL
字段验证策略之 select,在 select 的时候的字段验证策略既 wrapper 根据内部 entity 生成的 where 条件
String
null
MyBatis 配置文件位置,如果您有单独的 MyBatis 配置,请将其路径配置到
configLocation
中.MyBatis Configuration 的具体内容请参考MyBatis 官方文档(opens new window)
String[]
["classpath*:/mapper/**/*.xml"]
MyBatis Mapper 所对应的 XML 文件位置,如果您在 Mapper 中有自定义方法(XML 中有自定义实现),需要进行该配置,告诉 Mapper 所对应的 XML 文件位置
注意
Maven 多模块项目的扫描路径需以 classpath*:
开头 (即加载多个 jar 包下的 XML 文件)
String
null
MyBaits 别名包扫描路径,通过该属性可以给包中的类注册别名,注册后在 Mapper 对应的 XML 文件中可以直接使用类名,而不用使用全限定的类名(即 XML 中调用的时候不用包含包名)
@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 | 否 | “” | xml 中 resultMap 的 id(用于满足特定类型的实体类对象绑定) |
autoResultMap | boolean | 否 | false | 是否自动构建 resultMap 并使用(如果设置 resultMap 则不会进行 resultMap 的自动构建与注入) |
excludeProperty | String[] | 否 | {} | 需要排除的属性名 @since 3.3.1 即在各类操作中无视所排除的属性 |
@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 自增,需要建表时也设置自动递增 | |
NONE | 无状态,该类型为未设置主键类型(注解里等于跟随全局,全局里约等于 INPUT) | |
INPUT | insert 前自行 set 主键值 | |
ASSIGN_ID(默认) | 分配 ID(主键类型为 **Number(Long 和 Integer)**或 String)(since 3.3.0),使用接口IdentifierGenerator 的方法nextId (默认实现类为DefaultIdentifierGenerator 雪花算法),当设置了雪花算法后即便设置了主键自增也无效,但若强制设置主键后按照强制设置主键有限 |
|
ASSIGN_UUID | 分配 UUID,主键类型为 String(since 3.3.0),使用接口IdentifierGenerator 的方法nextUUID (默认 default 方法) |
经过以上的测试,我们可以发现,MyBatis-Plus在执行SQL语句时,要保证实体类中的属性名和表中的字段名一致如果实体类中的属性名和字段名不一致的情况,会出现什么问题呢?
情况1
若实体类中的属性使用的是驼峰命名风格,而表中的字段使用的是下划线命名风格例如实体类属性userName,表中字段user_name此时MyBatis-Plus会自动将下划线命名风格转化为驼峰命名风格相当于在MyBatis中配置
情况2
若实体类中的属性和表中的字段不满足情况1例如实体类属性name,表中字段username此时需要在实体类属性上使用@TableField(“username”)设置属性所对应的字段名
属性 | 类型 | 必须指定 | 默认值 | 描述 |
---|---|---|---|---|
value | String | 否 | “” | 数据库字段名 |
exist | boolean | 否 | true | 是否为数据库表字段 |
condition | String | 否 | “” | 字段 where 实体查询比较条件,有值设置则按设置的值为准,没有则为默认全局的 %s=#{%s} ,参考(opens new window) |
update | String | 否 | “” | 字段 update set 部分注入,例如:当在version字段上注解update="%s+1" 表示更新时会 set version=version+1 (该属性优先级高于 el 属性) |
insertStrategy | Enum | 否 | FieldStrategy.DEFAULT | 举例:NOT_NULL insert into table_a( |
updateStrategy | Enum | 否 | FieldStrategy.DEFAULT | 举例:IGNORED update table_a set column=#{columnProperty} |
whereStrategy | Enum | 否 | FieldStrategy.DEFAULT | 举例:NOT_EMPTY where |
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 | 否 | “” | 指定小数点后保留的位数 |
FieldStrategy的值有:
值 | 描述 |
---|---|
IGNORED | 忽略判断 |
NOT_NULL | 非 NULL 判断 |
NOT_EMPTY | 非空判断(只对字符串类型字段,其他类型字段依然为非 NULL 判断) |
DEFAULT | 追随全局配置 |
NEVER | 不加入SQL |
FieldFill的值有:
值 | 描述 |
---|---|
DEFAULT | 默认不处理 |
INSERT | 插入时填充字段 |
UPDATE | 更新时填充字段 |
INSERT_UPDATE | 插入和更新时填充字段 |
物理删除:真实删除,将对应数据从数据库中删除,之后查询不到此条被删除的数据
逻辑删除:假删除,将对应数据中代表是否被删除字段的状态修改为“被删除状态”,之后在数据库中仍旧能看到此条数据记录,可以进行数据恢复
- 数据库中创建逻辑删除状态列,设置默认值为0
实体类中添加逻辑删除属性
@TableLogic private Integer isDeleted;
测试删除功能,真正执行的是修改
UPDATE t_user SET is_deleted=1 WHERE id=? AND is_deleted=0
测试查询功能,被逻辑删除的数据默认不会被查询
SELECT id,username AS name,age,email,is_deleted FROM t_user WHERE is_deleted=0
当采用删除后逻辑为1,未删除逻辑为0时会产生逻辑删除与唯一索引冲突问题
解决方案是:用时间戳作为逻辑删除后的值
mybatis-plus:
global-config: # mybatis-plus全局配置
db-config:
logic-delete-field: isDeleted # 全局逻辑删除字段
logic-delete-value: UNIX_TIMESTAMP(now()) # 逻辑已删除值(默认为时间戳)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
为了去应对数据规模的增长以及应对逐渐增长的访问压力和数据量
数据的扩展方式主要包括:业务分库、主从复制,数据库分表
将不同业务数据分散存储到不同的数据库服务器,能够支撑百万甚至千万规模的业务,但如果业务继续发展,同一业务的单表数据会达到单台数据服务器的处理瓶颈。此时需要分库分表,通常有水平拆分和垂直拆分
垂直分表适合将表中某些不常用且占了大量空间的列拆分出去。例如,前面示意图中的 nickname 和 description 字段,假设我们是一个婚恋网站,用户在筛选其他用户的时候,主要是用 age 和 sex 两个字段进行查询,而 nickname 和 description 两个字段主要用于展示,一般不会在业务查询中用到。description 本身又比较长,因此我们可以将这两个字段独立到另外一张表中,这样在查询 age 和 sex 时,就能带来一定的性能提升。
水平分表适合表行数特别大的表,有的公司要求单表行数超过 5000 万就必须进行分表,这个数字可以作为参考,但并不是绝对标准,关键还是要看表的访问性能。对于一些比较复杂的表,可能超过 1000万就要分表了;而对于一些简单的表,即使存储数据超过 1 亿行,也可以不分表。但不管怎样,当看到表的数据量达到千万级别时,作为架构师就要警觉起来,因为这很可能是架构的性能瓶颈或者隐患。
水平分表相比垂直分表,会引入更多的复杂性,例如要求全局唯一的数据id该如何处理
①以最常见的用户 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 的子表中。
②复杂点:初始表数量的确定。表数量太多维护比较麻烦,表数量太少又可能导致单表性能存在问题。
③优点:表分布比较均匀。
④缺点:扩充新的表很麻烦,所有数据都要重分布。
雪花算法是由Twitter发布的分布式主键生成算法,它能够保证不同表的主键的不重复性,以及相同表的主键的有序性。
①核心思想:
长度共64bit(一个long型)。首先是一个符号位,1bit标识,由于long基本类型在Java中是带符号的,最高位是符号位,正数是0,负数是1,所以id一般是正数,最高位是0。41bit时间截(毫秒级),存储的是时间截的差值(当前时间截 - 开始时间截),结果约等于69.73年。10bit作为机器的ID(5个bit是数据中心,5个bit的机器ID,可以部署在1024个节点)。12bit作为毫秒内的流水号(意味着每个节点在每毫秒可以产生 4096 个 ID)。
②优点:整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞,并且效率较高。
Wrapper : 条件构造抽象类,最顶端父类
- AbstractWrapper : 用于查询条件封装,生成 sql 的 where 条件
- QueryWrapper : 查询条件封装
- UpdateWrapper : Update 条件封装
- AbstractLambdaWrapper : 使用Lambda 语法
- LambdaQueryWrapper :用于Lambda语法使用的查询Wrapper
- LambdaUpdateWrapper : Lambda 更新封装Wrapper
注意
无论什么条件构造器在创建时需要指定泛型 Wrapper<要操作的实体>
例:QueryWrapper<User> queryWrapper = new QueryWrapper<>();
查询用户名包含a,年龄在20到30,邮箱信息不为空
@Test
public void test01() {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.like("name", "a")
.between("age", 20, 30)
.isNotNull("email");
List<User> userList = userMapper.selectList(queryWrapper);
userList.forEach(System.out::println);
}
查询用户信息,按年龄降序排序,若年龄相同,按照id升序排序
@Test
public void test02() {
// 查询用户信息,按年龄降序排序,若年龄相同,按照id升序排序
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.orderByDesc("age").orderByAsc("id");
List<User> userList = userMapper.selectList(queryWrapper);
userList.forEach(System.out::println);
}
删除邮箱地址为null的用户信息
@Test
public void test03() {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.isNull("email");
int res = userMapper.delete(queryWrapper);
System.out.println(res);
}
将用户名中包含有a并且(年龄大于20或邮箱为null)的用户信息修改
UPDATE t_user SET age=?, email=? WHERE (username LIKE ? AND (age > ? ORemail IS NULL))
@Test
public void test04() {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
// 封装修改条件
queryWrapper.like("name", "a")
.gt("age", 20)
.or()
.isNull("email");
User user = new User().setName("宇轩").setEmail("[email protected]");
int res = userMapper.update(user, queryWrapper);
System.out.println(res);
}
将用户名中包含有a并且(年龄大于20或邮箱为null)的用户信息修改
@Test
public void test05() {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
// 封装修改条件
queryWrapper.like("name", "a").and(i -> i.gt("age", 20).or().isNull("email"));
User user = new User().setName("宇轩").setEmail("[email protected]");
int res = userMapper.update(user, queryWrapper);
System.out.println(res);
}
lambda中的条件优先执行
有时只希望查询某些字段
SELECT name,age,email FROM user WHERE is_deleted=0
// 组装select字段
@Test
public void test06() {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
// 封装查询字段
queryWrapper.select("name", "age", "email");
List<Map<String, Object>> users = userMapper.selectMaps(queryWrapper);
users.forEach(System.out::println);
}
查询id小于等于100的用户信息
SELECT id,name,age,email,is_deleted FROM user WHERE is_deleted=0 AND (id IN (select id from user where id <= 100))
@Test
public void test07() {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
// inSql("id", "select id from table where id < 3")<=>where id in (select id from table where id < 3)
queryWrapper.inSql("id", "select id from user where id <= 100");
List<User> userList = userMapper.selectList(queryWrapper);
userList.forEach(System.out::println);
}
updatewrapper不仅可以进行修改条件的组装,还可以设置要修改的内容
@Test
public void test01() {
UpdateWrapper<User> updateWrapper = new UpdateWrapper<User>();
updateWrapper.like("name", "o")
.and(i -> i.gt("age", 20).or().isNull("email"));
updateWrapper.set("name", "千楚").set("email", "[email protected]");
int res = userMapper.update(null, updateWrapper);
System.out.println(res);
}
对比QueryWrapper 无非就是要修改的数据不再封装于实体User中,而是封装在有pdateWrapper实例里面了
@Test
public void test04() {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
// 封装修改条件
queryWrapper.like("name", "a")
.gt("age", 20)
.or()
.isNull("email");
User user = new User().setName("宇轩").setEmail("[email protected]");
int res = userMapper.update(user, queryWrapper);
System.out.println(res);
}
用户传来的参数需要进行校验看是否需要组装在sql当中,对于有用的参数才会进行组装
@Test
public void test08() {
// 模拟前端传来的数据
String name = "";
Integer ageBegin = 20;
Integer ageEnd = 30;
// 创建条件构造器
QueryWrapper queryWrapper = new QueryWrapper<>();
// 字段判断
if (StringUtils.isNotBlank(name)) {
queryWrapper.like("name", name);
}
if (ageBegin != null) {
queryWrapper.ge("age", ageBegin);
}
if (ageEnd != null) {
queryWrapper.le("age", ageEnd);
}
List users = userMapper.selectList(queryWrapper);
users.forEach(System.out::println);
}
列如:inSql(boolean condition, R column, String inValue)
@param condition 引发组装的条件
@Test
public void test09() {
// 模拟前端传来的数据
String name = "";
Integer ageBegin = 20;
Integer ageEnd = 30;
// 创建条件构造器
QueryWrapper queryWrapper = new QueryWrapper<>();
queryWrapper.like(StringUtils.isNotBlank(name), "name", name)
.ge(ageBegin != null, "age", ageBegin)
.le(ageEnd != null, "age", ageEnd);
List users = userMapper.selectList(queryWrapper);
users.forEach(System.out::println);
}
条件构造器的作用在于封装条件,并且最终这些条件将会组装在sql语句上,但是对于传来的参数应该进行判断,判断其是否应该组装,condition使我们在组装前进行判断,极大的简化了代码
为了防止将字段写错,可以使用lambda方式
public void test01() {
// 模拟前端传来的数据
String name = "";
Integer ageBegin = 20;
Integer ageEnd = 30;
LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper();
lambdaQueryWrapper
.like(StringUtils.isNotBlank(name), User::getName, name)
.ge(User::getAge, ageBegin)
.le(User::getAge, ageEnd);
List<User> users = userMapper.selectList(lambdaQueryWrapper);
users.forEach(System.out::println);
}
对比以前,我们可以使用User::getAge方式来代替直接手写字段名防止写错
@Test
public void test04() {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
// 封装修改条件
queryWrapper.like("name", "a")
.gt("age", 20)
.or()
.isNull("email");
User user = new User().setName("宇轩").setEmail("[email protected]");
int res = userMapper.update(user, queryWrapper);
System.out.println(res);
}
@Test
public void test01() {
LambdaUpdateWrapper<User> lambdaQueryWrapper = new LambdaUpdateWrapper();
lambdaQueryWrapper.like(User::getName, "a")
.and(i -> i.gt(User::getAge, 20).or().isNull(User::getEmail));
lambdaQueryWrapper.set(User::getAge, 30).set(User::getEmail, "[email protected]");
int res = userMapper.update(null, lambdaQueryWrapper);
}
InnerInterceptor
我们提供的插件都将基于此接口来实现功能
目前已有的功能:
- 自动分页: PaginationInnerInterceptor
- 多租户: TenantLineInnerInterceptor
- 动态表名: DynamicTableNameInnerInterceptor
- 乐观锁: OptimisticLockerInnerInterceptor
- sql 性能规范: IllegalSQLInnerInterceptor
- 防止全表更新与删除: BlockAttackInnerInterceptor
注意:
使用多个功能需要注意顺序关系,建议使用如下顺序
- 多租户,动态表名
- 分页,乐观锁
- sql 性能规范,防止全表更新与删除
总结: 对 sql 进行单次改造的优先放入,不对 sql 进行改造的最后放入
属性名 | 类型 | 默认值 | 描述 |
---|---|---|---|
overflow | boolean | false | 溢出总页数后是否进行处理(默认不处理,参见 插件#continuePage 方法) |
maxLimit | Long | 单页分页条数限制(默认无限制,参见 插件#handlerLimit 方法) |
|
dbType | DbType | 数据库类型(根据类型获取应使用的分页方言,参见 插件#findIDialect 方法) |
|
dialect | IDialect | 方言实现类(参见 插件#findIDialect 方法) |
@Configuration
public class MpConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
// 创建mp的拦截器
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 注册分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
// 返回拦截器,并注入容器
return interceptor;
}
}
// 分页查询
@Test
public void test01() {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
// 创建页面对象,封装查询需求
Page<User> page = new Page<>(2, 3);
userMapper.selectPage(page, null);
System.out.println("当前页为" + page.getCurrent());
System.out.println("每页页面大小为:" + page.getSize());
System.out.println("查询出来的记录为:" + page.getRecords());
System.out.println("查询出来的记录总数为:" + page.getTotal());
System.out.println("查询出来的记录所占总页数为:" + page.getPages());
System.out.println("是否有下一页:" + page.hasNext());
System.out.println("是否有上一页:" + page.hasPrevious());
}
IPage<UserVo> selectPageVo(IPage<?> page, Integer state);
// or (class MyPage extends Ipage{ private Integer state; })
MyPage selectPageVo(MyPage page);
// or
List<UserVo> selectPageVo(IPage<UserVo> page, Integer state);
例如:
/**
* 通过年龄查询用户信息并分页
*
* @param page 页面 mp提供的分页对象
* @param age
* @return {@link Page}<{@link User}>
*/
Page<User> selectPageVo(@Param("page") Page<User> page, @Param("age") Integer age);
<select id="selectPageVo" resultType="user">
SELECT id, name
FROM user
WHERE age = #{age}
select>
@Test
public void test02() {
// 创建页面对象,封装查询需求
Page<User> page = new Page<>(1, 3);
userMapper.selectPageVo(page, 20);
System.out.println("当前页为" + page.getCurrent());
System.out.println("每页页面大小为:" + page.getSize());
System.out.println("查询出来的记录为:" + page.getRecords());
System.out.println("查询出来的记录总数为:" + page.getTotal());
System.out.println("查询出来的记录页数为:" + page.getPages());
System.out.println("是否有下一页:" + page.hasNext());
System.out.println("是否有上一页:" + page.hasPrevious());
}
一件商品,成本价是80元,售价是100元。老板先是通知小李,说你去把商品价格增加50元。小李正在玩游戏,耽搁了一个小时。正好一个小时后,老板觉得商品价格增加到150元,价格太高,可能会影响销量。又通知小王,你把商品价格降低30元。此时,小李和小王同时操作商品后台系统。小李操作的时候,系统先取出商品价格100元;小王也在操作,取出的商品价格也是100元。小李将价格加了50元,并将100+50=150元存入了数据库;小王将商品减了30元,并将100-30=70元存入了数据库。是的,如果没有锁,小李的操作就完全被小王的覆盖了。现在商品价格是70元,比成本价低10元。几分钟后,这个商品很快出售了1千多件商品,老板亏1万多。
如果采用了乐观锁
小王保存价格前,会检查下价格是否被人修改过了。如果被修改过了,则重新取出的被修改后的价格,150元,这样他会将120元存入数据库。
如果采用了悲观锁
如果是悲观锁,小李取出数据后,小王只能等小李操作完之后,才能对价格进行操作,也会保证最终的价格是120元。
@Test
public void test03() {
// 小李查询到的价格 100
Product productLi = productMapper.selectById(1);
System.out.println("小李查询的商品价格" + productLi.getPrice());
// 小王查询到的价格 100
Product productWang = productMapper.selectById(1);
System.out.println("小王查询的商品价格" + productLi.getPrice());
// 小李将价格加50
productLi.setPrice(productLi.getPrice() + 50);
productMapper.updateById(productLi);
// 小王将商品价格-30
productLi.setPrice(productWang.getPrice() - 30);
productMapper.updateById(productWang);
// 老板查询到的价格 70
Product productBoss = productMapper.selectById(1);
System.out.println("老板查询的商品价格" + productLi.getPrice());
}
操作之前先看数据是否有更新,若有更新那么更新失败。
①小李查询数据,此时版本号为0
SELECT id,name,price,version FROM product WHERE id=1
②小王查询数据,此时版本号也为0
SELECT id,name,price,version FROM product WHERE id=1
③小李更新数据,由于版本号为0,所以可以更新,并让版本号加1,此时数据库版本号为1
PDATE product SET name=?, price=?, version=version+1 WHERE id=1 AND version=0
④小王此时也更新数据,由于版本号已经变为了1,所以小王更新失败
UPDATE product SET name=?, price=?, version=? WHERE id=1 AND version=0
当前用户在操作时另一用户不可操作,被操作对象是临界资源
@Data
public class Product {
private Long id;
private String name;
private Integer price;
@Version
private Integer version;
}
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
针对 update 和 delete 语句 作用: 阻止恶意的全表更新删除
interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
@Test
public void test04() {
User user = new User();
user.setName("小七");
// 全表更新抛出下面异常
// Cause: com.baomidou.mybatisplus.core.exceptions.MybatisPlusException: Prohibition of table update operation
userMapper.update(user, null);
}
方式一:通过注解 @EnumValue 将注解所标识的属性的值存储到数据库中
@Getter
public enum SexEnum {
MALE(1, "男"),
FEMALE(2, "女");
// 标识在使用枚举时,要插入数据库的枚举的值
@EnumValue
private Integer sex;
private String sexName;
SexEnum(Integer sex, String sexName) {
this.sex = sex;
this.sexName = sexName;
}
}
方式二:通过实现接口IEnum 并配置相关属性的getter
public enum AgeEnum implements IEnum<Integer> {
ONE(1, "一岁"),
TWO(2, "二岁"),
THREE(3, "三岁");
private int value;
private String desc;
@Override
public Integer getValue() {
return this.value;
}
}
public class User {
// ASSIGN_ID 通常用于(主键类型为 Number(Long 和 Integer)或 String
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private String name;
private Integer age;
private String email;
private SexEnum sex;
// 逻辑删除
@TableLogic
private Integer isDeleted;
}
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-generatorartifactId>
<version>3.5.1version>
dependency>
<dependency>
<groupId>org.freemarkergroupId>
<artifactId>freemarkerartifactId>
<version>2.3.31version>
dependency>
FastAutoGenerator.create("url", "username", "password")
.globalConfig(builder -> {
builder.author("baomidou") // 设置作者
.enableSwagger() // 开启 swagger 模式
.fileOverride() // 覆盖已生成文件
.outputDir("D://"); // 指定输出目录
})
.packageConfig(builder -> {
builder.parent("com.bloom") // 设置父包名
.moduleName("mp") // 设置父包模块名
.pathInfo(Collections.singletonMap(OutputFile.xml, "D://")); // 设置mapperXml生成路径
})
.strategyConfig(builder -> {
builder.addInclude("t_simple") // 设置需要生成的表名
.addTablePrefix("t_", "c_"); // 设置过滤表前缀
})
.templateEngine(new FreemarkerTemplateEngine()) // 使用Freemarker引擎模板,默认的是Velocity引擎模板
.execute();
背景:
多数据源既动态数据源,项目开发逐渐扩大,单个数据源、单一数据源已经无法满足需求项目的支撑需求。由此延伸了多数据源的扩展,下文提供了两种不同方向的扩展插件。
dynamic-datasource
开源文档付费,属于组织参与者小锅盖
发起的项目mybatis-mate
企业级付费授权,资料文档免费
dynamic-datasource-spring-boot-starter 是一个基于springboot的快速集成多数据源的启动器。
- 支持 数据源分组 ,适用于多种场景 纯粹多库 读写分离 一主多从 混合模式。
- 支持数据库敏感配置信息 加密 ENC()。
- 支持每个数据库独立初始化表结构schema和数据库database。
- 支持无数据源启动,支持懒加载数据源(需要的时候再创建连接)。
- 支持 自定义注解 ,需继承DS(3.2.0+)。
- 提供并简化对Druid,HikariCp,BeeCp,Dbcp2的快速集成。
- 提供对Mybatis-Plus,Quartz,ShardingJdbc,P6sy,Jndi等组件的集成方案。
- 提供 自定义数据源来源 方案(如全从数据库加载)。
- 提供项目启动后 动态增加移除数据源 方案。
- 提供Mybatis环境下的 纯读写分离 方案。
- 提供使用 spel动态参数 解析数据源方案。内置spel,session,header,支持自定义。
- 支持 多层数据源嵌套切换 。(ServiceA >>> ServiceB >>> ServiceC)。
- 提供 基于seata的分布式事务方案。
- 提供 本地多数据源事务方案。 附:不能和原生spring事务混用。
- 本框架只做 切换数据源 这件核心的事情,并不限制你的具体操作,切换了数据源可以做任何CRUD。
- 配置文件所有以下划线
_
分割的数据源 首部 即为组的名称,相同组名称的数据源会放在一个组下。- 切换数据源可以是组名,也可以是具体数据源名称。组名则切换时采用负载均衡算法切换。
- 默认的数据源名称为 master ,你可以通过
spring.datasource.dynamic.primary
修改。- 方法上的注解优先于类上注解。
- DS支持继承抽象类上的DS,暂不支持继承接口上的DS。
<dependency>
<groupId>com.baomidougroupId>
<artifactId>dynamic-datasource-spring-boot-starterartifactId>
<version>3.5.0version>
dependency>
spring:
datasource:
dynamic:
primary: master #设置默认的数据源或者数据源组,默认值即为master
strict: false #严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源
datasource:
master:
url: jdbc:mysql://xx.xx.xx.xx:3306/dynamic
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver # 3.2.0开始支持SPI可省略此配置
# 配置文件所有以下划线 `_` 分割的数据源 首部 即为组的名称,相同组名称的数据源会放在一个组下。
# slave组
slave_1:
url: jdbc:mysql://xx.xx.xx.xx:3307/dynamic
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
slave_2:
url: ENC(xxxxx) # 内置加密,使用请查看详细文档
username: ENC(xxxxx)
password: ENC(xxxxx)
driver-class-name: com.mysql.jdbc.Driver
#......省略
#以上会配置一个默认库master,一个组slave下有两个子库slave_1,slave_2
# 多主多从 纯粹多库(记得设置primary) 混合配置
spring: spring: spring:
datasource: datasource: datasource:
dynamic: dynamic: dynamic:
datasource: datasource: datasource:
master_1: mysql: master:
master_2: oracle: slave_1:
slave_1: sqlserver: slave_2:
slave_2: postgresql: oracle_1:
slave_3: h2: oracle_2:
@DS 可以注解在方法上或类上,同时存在就近原则 方法上注解 优先于 类上注解。
注解 | 结果 |
---|---|
没有@DS | 默认数据源 |
@DS(“dsName”) | dsName可以为组名也可以为具体某个库的名称 |
①指定UserServiceImpl类使用master数据源
@DS("master") //指定所操作的数据源
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implementsUserService {
}
②指定 ProductServiceImpl类使用slave_1数据源
@DS("slave_1")
@Service
public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product>implements ProductService {
}
③UserServiceImpl类中除了selectByCondition()方法使用"slave_1"数据源外,其它方法都使用slave组类数据源,组名则切换时采用负载均衡算法切换
@Service
@DS("slave")
public class UserServiceImpl implements UserService {
@Autowired
private JdbcTemplate jdbcTemplate;
public List selectAll() {
return jdbcTemplate.queryForList("select * from user");
}
@Override
@DS("slave_1")
public List selectByCondition() {
return jdbcTemplate.queryForList("select * from user where age >10");
}
}
①模拟两台服务器上的数据库一主一丛,都有student表,其中master中的student用于写,slave中的student用于读
②配置yml
datasource:
dynamic:
# 设置默认的数据源或者数据源组,默认值即为master
primary: master
# 严格匹配数据源,默认false.true未匹配到指定数据源时抛异常,false使用默认数据源
strict: false
datasource:
# 主数据源(默认)
master:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/db_mybatis_plus_master?serverTimezone=GMT%2B8&characterEncoding=utf-8&useSSL=false
username: 'root'
password: '000001'
type: com.alibaba.druid.pool.DruidDataSource
# 从数据源
slave_1:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/db_mybatis_plus_slave?serverTimezone=GMT%2B8&characterEncoding=utf-8&useSSL=false
username: 'root'
password: '000001'
type: com.alibaba.druid.pool.DruidDataSource
@Service
public class IStudentServiceImpl implements IStudentService {
@Autowired
private StudentMapper studentMapper;
// 主服务器负责写相关
@DS("master")
@Override
public int insertOne(Student student) {
return studentMapper.insert(student);
}
// 从服务器负责读相关
@DS("slave")
@Override
public List<Student> selectAll() {
return studentMapper.selectList(null);
}
}
@Test
public void test01() {
Student student = new Student().setAge(17).setName("紫玉").setEmail("[email protected]");
int res = iStudentService.insertOne(student);
System.out.println(res);
List<Student> studentList = iStudentService.selectAll();
studentList.forEach(System.out::println);
}
==> Preparing: INSERT INTO student ( id, name, age, email ) VALUES ( ?, ?, ?, ? )
==> Parameters: 1610936324001456130(Long), 紫玉(String), 17(Integer), ziyu@qq.com(String)
<== Updates: 1
==> Preparing: SELECT id,name,age,email FROM student
==> Parameters:
<== Total: 0
当需要记录记录插入时间时或记录更新时间,除了使用AOP实现以外,还可以使用字段填充
public enum FieldFill {
/**
* 默认不处理
*/
DEFAULT,
/**
* 插入时填充字段
*/
INSERT,
/**
* 更新时填充字段
*/
UPDATE,
/**
* 插入和更新时填充字段
*/
INSERT_UPDATE
}
注意事项:
- 填充原理是直接给
entity
的属性设置值!!!- 注解则是指定该属性在对应情况下必有值,如果无值则入库会是
null
MetaObjectHandler
提供的默认方法的策略均为:如果属性有值则不覆盖,如果填充值为null
则不填充- 字段必须声明
TableField
注解,属性fill
选择对应策略,该声明告知Mybatis-Plus
需要预留注入SQL
字段- 填充处理器
MyMetaObjectHandler
在 Spring Boot 中需要声明@Component
或@Bean
注入- 要想根据注解
FieldFill.xxx
和字段名
以及字段类型
来区分必须使用父类的strictInsertFill
或者strictUpdateFill
方法- 不需要根据任何来区分可以使用父类的
fillStrategy
方法- update(T t,Wrapper updateWrapper)时t不能为空,否则自动填充失效
@TableField(.. fill = FieldFill.INSERT)
生成器策略部分也可以配置!public class User {
// 注意!这里需要标记为填充字段
@TableField(.. fill = FieldFill.INSERT)
private String fillField;
....
}
@Slf4j
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
log.info("start insert fill ....");
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now()); // 起始版本 3.3.0(推荐使用)
// 或者
this.strictInsertFill(metaObject, "createTime", () -> LocalDateTime.now(), LocalDateTime.class); // 起始版本 3.3.3(推荐)
// 或者
this.fillStrategy(metaObject, "createTime", LocalDateTime.now()); // 也可以使用(3.3.0 该方法有bug)
}
@Override
public void updateFill(MetaObject metaObject) {
log.info("start update fill ....");
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); // 起始版本 3.3.0(推荐)
// 或者
this.strictUpdateFill(metaObject, "updateTime", () -> LocalDateTime.now(), LocalDateTime.class); // 起始版本 3.3.3(推荐)
// 或者
this.fillStrategy(metaObject, "updateTime", LocalDateTime.now()); // 也可以使用(3.3.0 该方法有bug)
}
}