Mybatis-Plus

Mybatis-Plus

一、Mybatis-Plus基本原理

  1. ScanEntity: 扫描实体
  2. Reflection Extraction :抽取实体类中的属性和字段
  3. Analysis Table Name Column:根据实体中的属性字段等来分析表,建立实体与表的关系
  4. Sql:根据不同需求生成sql语句,并将封装为mapper
  5. Injection Mybatis Container: 注入mapper到容器

二、自动化CRUD接口

(一)、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>
(二)、service层接口

首先用户定义的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

本部分(Configuration)的配置大都为 MyBatis 原生支持的配置,这意味着您可以通过 MyBatis XML 配置文件的形式进行配置。

1、mapUnderscoreToCamelCase
  • 类型:boolean
  • 默认值:true

是否开启自动驼峰命名规则(camel case)映射,即从经典数据库列名 A_COLUMN(下划线命名) 到经典 Java 属性名 aColumn(驼峰命名) 的类似映射。

注意

此属性在 MyBatis 中原默认值为 false,在 MyBatis-Plus 中,此属性也将用于生成最终的 SQL 的 select body

如果您的数据库命名符合规则无需使用 @TableField 注解指定数据库字段名

(二)、GlobalConfig

本部分为全局配置

(三)、DbConfig
1、idType
  • 类型:com.baomidou.mybatisplus.annotation.IdType
  • 默认值:ASSIGN_ID

全局默认主键类型

2、tableprefix
  • 类型:String
  • 默认值:null

表名前缀

3、keyGenerator
  • 类型:com.baomidou.mybatisplus.core.incrementer.IKeyGenerator
  • 默认值:null

表主键生成器(starter 下支持@bean注入)

4、logicDeleteField
  • 类型:String
  • 默认值:null

全局的 entity 的逻辑删除字段属性名,(逻辑删除下有效)

5、logicDeleteValue
  • 类型:String
  • 默认值:1

逻辑已删除值,(逻辑删除下有效)

6、logicNotDeleteValue
  • 类型:String
  • 默认值:0

逻辑未删除值,(逻辑删除下有效)

7、insertStrategy
  • 类型:com.baomidou.mybatisplus.annotation.FieldStrategy
  • 默认值:NOT_NULL

字段验证策略之 insert,在 insert 的时候的字段验证策略

8、updateStrategy
  • 类型:com.baomidou.mybatisplus.annotation.FieldStrategy
  • 默认值:NOT_NULL

字段验证策略之 update,在 update 的时候的字段验证策略

9、whereStrategy
  • 类型:com.baomidou.mybatisplus.annotation.FieldStrategy
  • 默认值:NOT_NULL

字段验证策略之 select,在 select 的时候的字段验证策略既 wrapper 根据内部 entity 生成的 where 条件

(四)、其它
1、configLocation
  • 类型:String
  • 默认值:null

MyBatis 配置文件位置,如果您有单独的 MyBatis 配置,请将其路径配置到 configLocation 中.MyBatis Configuration 的具体内容请参考MyBatis 官方文档(opens new window)

2、mapperLocations
  • 类型:String[]
  • 默认值:["classpath*:/mapper/**/*.xml"]

MyBatis Mapper 所对应的 XML 文件位置,如果您在 Mapper 中有自定义方法(XML 中有自定义实现),需要进行该配置,告诉 Mapper 所对应的 XML 文件位置

注意

Maven 多模块项目的扫描路径需以 classpath*: 开头 (即加载多个 jar 包下的 XML 文件)

3、typeAliasesPackage
  • 类型:String
  • 默认值:null

MyBaits 别名包扫描路径,通过该属性可以给包中的类注册别名,注册后在 Mapper 对应的 XML 文件中可以直接使用类名,而不用使用全限定的类名(即 XML 中调用的时候不用包含包名)

四、常见注解

(一)、@TableName
  • 描述:表名注解,标识实体类对应的表
  • 使用位置:实体类
@TableName("sys_user")
public class User {
    private Long id;
    private String name;
    private Integer age;
    private String email;
}
属性类型必须指定默认值描述
valueString“”表名
schemaString“”schema
keepGlobalPrefixbooleanfalse是否保持使用全局的 tablePrefix 的值(当全局 tablePrefix 生效时)
resultMapString“”xml 中 resultMap 的 id(用于满足特定类型的实体类对象绑定)
autoResultMapbooleanfalse是否自动构建 resultMap 并使用(如果设置 resultMap 则不会进行 resultMap 的自动构建与注入)
excludePropertyString[]{}需要排除的属性名 @since 3.3.1 即在各类操作中无视所排除的属性
(二)、 @TableId
  • 描述:主键注解
  • 使用位置:实体类主键字段
  • 使用场景:mp默认将id属性/字段作为主键,当数据库字段和实体类属性不是以id字段命名时,就需要使用到该注解声明哪个属性是主键
@TableName("sys_user")
public class User {
    @TableId
    private Long id;
    private String name;
    private Integer age;
    private String email;
}
属性类型必须指定默认值描述
valueString“”主键对应的字段名
typeEnumIdType.NONE指定主键类型

IdType的类型有:

描述
AUTO数据库 ID 自增,需要建表时也设置自动递增
NONE无状态,该类型为未设置主键类型(注解里等于跟随全局,全局里约等于 INPUT)
INPUTinsert 前自行 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 方法)
(三)、@TableField

经过以上的测试,我们可以发现,MyBatis-Plus在执行SQL语句时,要保证实体类中的属性名和表中的字段名一致如果实体类中的属性名和字段名不一致的情况,会出现什么问题呢?

情况1

若实体类中的属性使用的是驼峰命名风格,而表中的字段使用的是下划线命名风格例如实体类属性userName,表中字段user_name此时MyBatis-Plus会自动将下划线命名风格转化为驼峰命名风格相当于在MyBatis中配置

情况2

若实体类中的属性和表中的字段不满足情况1例如实体类属性name,表中字段username此时需要在实体类属性上使用@TableField(“username”)设置属性所对应的字段名

属性类型必须指定默认值描述
valueString“”数据库字段名
existbooleantrue是否为数据库表字段
conditionString“”字段 where 实体查询比较条件,有值设置则按设置的值为准,没有则为默认全局的 %s=#{%s}参考(opens new window)
updateString“”字段 update set 部分注入,例如:当在version字段上注解update="%s+1" 表示更新时会 set version=version+1 (该属性优先级高于 el 属性)
insertStrategyEnumFieldStrategy.DEFAULT举例:NOT_NULL insert into table_a(<if test="columnProperty != null">column</if>) values (<if test="columnProperty != null">#{columnProperty}</if>)
updateStrategyEnumFieldStrategy.DEFAULT举例:IGNORED update table_a set column=#{columnProperty}
whereStrategyEnumFieldStrategy.DEFAULT举例:NOT_EMPTY where <if test="columnProperty != null and columnProperty!=''">column=#{columnProperty}</if>
fillEnumFieldFill.DEFAULT字段自动填充策略
selectbooleantrue是否进行 select 查询
keepGlobalFormatbooleanfalse是否保持使用全局的 format 进行处理
jdbcTypeJdbcTypeJdbcType.UNDEFINEDJDBC 类型 (该默认值不代表会按照该值生效)
typeHandlerClass<? extends TypeHandler>UnknownTypeHandler.class类型处理器 (该默认值不代表会按照该值生效)
numericScaleString“”指定小数点后保留的位数

FieldStrategy的值有:

描述
IGNORED忽略判断
NOT_NULL非 NULL 判断
NOT_EMPTY非空判断(只对字符串类型字段,其他类型字段依然为非 NULL 判断)
DEFAULT追随全局配置
NEVER不加入SQL

FieldFill的值有:

描述
DEFAULT默认不处理
INSERT插入时填充字段
UPDATE更新时填充字段
INSERT_UPDATE插入和更新时填充字段
(四)、TableLogic
1、什么是逻辑/物理删除

物理删除:真实删除,将对应数据从数据库中删除,之后查询不到此条被删除的数据

逻辑删除:假删除,将对应数据中代表是否被删除字段的状态修改为“被删除状态”,之后在数据库中仍旧能看到此条数据记录,可以进行数据恢复

2、如何实现逻辑删除
  • 数据库中创建逻辑删除状态列,设置默认值为0

在这里插入图片描述

  • 实体类中添加逻辑删除属性

    @TableLogic
    private Integer isDeleted;
    
3、加入逻辑删除后的sql语句
测试删除功能,真正执行的是修改
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
⭐️4、逻辑删除与唯一索引冲突问题

当采用删除后逻辑为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)

五、雪花算法

(一)、背景

为了去应对数据规模的增长以及应对逐渐增长的访问压力和数据量

数据的扩展方式主要包括:业务分库、主从复制,数据库分表

(二)、数据库分表

将不同业务数据分散存储到不同的数据库服务器,能够支撑百万甚至千万规模的业务,但如果业务继续发展,同一业务的单表数据会达到单台数据服务器的处理瓶颈。此时需要分库分表,通常有水平拆分和垂直拆分

1、垂直分表

垂直分表适合将表中某些不常用且占了大量空间的列拆分出去。例如,前面示意图中的 nickname 和 description 字段,假设我们是一个婚恋网站,用户在筛选其他用户的时候,主要是用 age 和 sex 两个字段进行查询,而 nickname 和 description 两个字段主要用于展示,一般不会在业务查询中用到。description 本身又比较长,因此我们可以将这两个字段独立到另外一张表中,这样在查询 age 和 sex 时,就能带来一定的性能提升。

2、水平分表

水平分表适合表行数特别大的表,有的公司要求单表行数超过 5000 万就必须进行分表,这个数字可以作为参考,但并不是绝对标准,关键还是要看表的访问性能。对于一些比较复杂的表,可能超过 1000万就要分表了;而对于一些简单的表,即使存储数据超过 1 亿行,也可以不分表。但不管怎样,当看到表的数据量达到千万级别时,作为架构师就要警觉起来,因为这很可能是架构的性能瓶颈或者隐患。

3、水平分表确保主键唯一性

水平分表相比垂直分表,会引入更多的复杂性,例如要求全局唯一的数据id该如何处理

(1)、主键自增

①以最常见的用户 ID 为例,可以按照 1000000 的范围大小进行分段,1 ~ 999999 放到表 1中,

1000000 ~ 1999999 放到表2中,以此类推。

②复杂点:分段大小的选取。分段太小会导致切分后子表数量过多,增加维护复杂度;分段太大可能会导致单表依然存在性能问题,一般建议分段大小在 100 万至 2000 万之间,具体需要根据业务选取合适的分段大小。

③优点:可以随着数据的增加平滑地扩充新的表。例如,现在的用户是 100 万,如果增加到 1000 万,只需要增加新的表就可以了,原有的数据不需要动。

④缺点:分布不均匀。假如按照 1000 万来进行分表,有可能某个分段实际存储的数据量只有 1 条,而另外一个分段实际存储的数据量有 1000 万条。

(2)、取模

①同样以用户 ID 为例,假如我们一开始就规划了 10 个数据库表,可以简单地用 user_id % 10 的值来表示数据所属的数据库表编号,ID 为 985 的用户放到编号为 5 的子表中,ID 为 10086 的用户放到编号为 6 的子表中。

②复杂点:初始表数量的确定。表数量太多维护比较麻烦,表数量太少又可能导致单表性能存在问题。

③优点:表分布比较均匀。

④缺点:扩充新的表很麻烦,所有数据都要重分布。

(3)、雪花算法

雪花算法是由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<>();
(一)、QueryWrapper的使用
1、组装查询条件

查询用户名包含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);
}
2、组装排序条件

查询用户信息,按年龄降序排序,若年龄相同,按照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);
}
3、组装删除条件

删除邮箱地址为null的用户信息

@Test
public void test03() {
    QueryWrapper<User> queryWrapper = new QueryWrapper<>();
    queryWrapper.isNull("email");
    int res = userMapper.delete(queryWrapper);
    System.out.println(res);
}
4、组装修改条件

将用户名中包含有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("yuxuan@qq.com");
    int res = userMapper.update(user, queryWrapper);
    System.out.println(res);
}
5、条件优先级

将用户名中包含有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("yuxuan@qq.com");
    int res = userMapper.update(user, queryWrapper);
    System.out.println(res);
}

lambda中的条件优先执行

6、组装select字段

有时只希望查询某些字段

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);
}
7、组装子查询

查询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的使用

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", "qiancu@qq.com");
    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("yuxuan@qq.com");
    int res = userMapper.update(user, queryWrapper);
    System.out.println(res);
}
(三)、开发中的条件组装判断condition(类似于动态sql)

用户传来的参数需要进行校验看是否需要组装在sql当中,对于有用的参数才会进行组装

1、不使用condition时
@Test
public void test08() {
    // 模拟前端传来的数据
    String name = "";
    Integer ageBegin = 20;
    Integer ageEnd = 30;

    // 创建条件构造器
    QueryWrapper<User> 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<User> users = userMapper.selectList(queryWrapper);
    users.forEach(System.out::println);
}
2、使用condition时
列如:inSql(boolean condition, R column, String inValue)
@param condition 引发组装的条件
@Test
public void test09() {
    // 模拟前端传来的数据
    String name = "";
    Integer ageBegin = 20;
    Integer ageEnd = 30;

    // 创建条件构造器
    QueryWrapper<User> queryWrapper = new QueryWrapper<>();
    queryWrapper.like(StringUtils.isNotBlank(name), "name", name)
            .ge(ageBegin != null, "age", ageBegin)
            .le(ageEnd != null, "age", ageEnd);
    List<User> users = userMapper.selectList(queryWrapper);
    users.forEach(System.out::println);
}
3、对比使用condition前后

条件构造器的作用在于封装条件,并且最终这些条件将会组装在sql语句上,但是对于传来的参数应该进行判断,判断其是否应该组装,condition使我们在组装前进行判断,极大的简化了代码

(四)、Lambda方式的条件构造器的使用

为了防止将字段写错,可以使用lambda方式

(一)、LambdaQueryWrapper
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("yuxuan@qq.com");
    int res = userMapper.update(user, queryWrapper);
    System.out.println(res);
}
(二)、LambdaUpdateWrapper
@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, "new@qq.com");
    int res = userMapper.update(null, lambdaQueryWrapper);
}

七、常用插件

InnerInterceptor

我们提供的插件都将基于此接口来实现功能

目前已有的功能:

  • 自动分页: PaginationInnerInterceptor
  • 多租户: TenantLineInnerInterceptor
  • 动态表名: DynamicTableNameInnerInterceptor
  • 乐观锁: OptimisticLockerInnerInterceptor
  • sql 性能规范: IllegalSQLInnerInterceptor
  • 防止全表更新与删除: BlockAttackInnerInterceptor

注意:

使用多个功能需要注意顺序关系,建议使用如下顺序

  • 多租户,动态表名
  • 分页,乐观锁
  • sql 性能规范,防止全表更新与删除

总结: 对 sql 进行单次改造的优先放入,不对 sql 进行改造的最后放入

(一)、分页插件(PaginationInnerInterceptor)
1、分页插件的属性
属性名类型默认值描述
overflowbooleanfalse溢出总页数后是否进行处理(默认不处理,参见 插件#continuePage 方法)
maxLimitLong单页分页条数限制(默认无限制,参见 插件#handlerLimit 方法)
dbTypeDbType数据库类型(根据类型获取应使用的分页方言,参见 插件#findIDialect 方法)
dialectIDialect方言实现类(参见 插件#findIDialect 方法)
2、分页插件的注册
@Configuration
public class MpConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        // 创建mp的拦截器
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 注册分页插件
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        // 返回拦截器,并注入容器
        return interceptor;
    }
}
3、分页插件的使用
// 分页查询
@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());
}
4、自定义分页
(1)、mapper接口方法定义(三种方式)
IPage<UserVo> selectPageVo(IPage<?> page, Integer state);
// or (class MyPage extends Ipage<UserVo>{ 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);
(2)、mapper映射文件书写
<select id="selectPageVo" resultType="user">
    SELECT id, name
    FROM user
    WHERE age = #{age}
</select>
(3)、测试
@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());
}
(二)、乐观锁插件(OptimisticLockerInnerInterceptor)
1、背景

一件商品,成本价是80元,售价是100元。老板先是通知小李,说你去把商品价格增加50元。小李正在玩游戏,耽搁了一个小时。正好一个小时后,老板觉得商品价格增加到150元,价格太高,可能会影响销量。又通知小王,你把商品价格降低30元。此时,小李和小王同时操作商品后台系统。小李操作的时候,系统先取出商品价格100元;小王也在操作,取出的商品价格也是100元。小李将价格加了50元,并将100+50=150元存入了数据库;小王将商品减了30元,并将100-30=70元存入了数据库。是的,如果没有锁,小李的操作就完全被小王的覆盖了。现在商品价格是70元,比成本价低10元。几分钟后,这个商品很快出售了1千多件商品,老板亏1万多。

如果采用了乐观锁

小王保存价格前,会检查下价格是否被人修改过了。如果被修改过了,则重新取出的被修改后的价格,150元,这样他会将120元存入数据库。

如果采用了悲观锁

如果是悲观锁,小李取出数据后,小王只能等小李操作完之后,才能对价格进行操作,也会保证最终的价格是120元。

2、模拟修改冲突
@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());
}
3、乐观锁悲观锁原理
(1)、乐观锁实现原理

操作之前先看数据是否有更新,若有更新那么更新失败。

①小李查询数据,此时版本号为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
(2)、悲观锁实现原理

当前用户在操作时另一用户不可操作,被操作对象是临界资源

4、使用乐观锁插件解决冲突实现
(1)、添加注解标识版本号字段
@Data
public class Product {
    private Long id;
    private String name;
    private Integer price;
    @Version
    private Integer version;
}
(2)、注册乐观锁插件
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
(三)、防全表更新与删除插件(BlockAttackInnerInterceptor)
1、插件使用目的

针对 update 和 delete 语句 作用: 阻止恶意的全表更新删除

2、插件使用方法
interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
3、插件使用测试
@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.baomidou</groupId>
    <artifactId>mybatis-plus-generator</artifactId>
    <version>3.5.1</version>
</dependency>
<dependency>
    <groupId>org.freemarker</groupId>
    <artifactId>freemarker</artifactId>
    <version>2.3.31</version>
</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方式实现多数据源
1、dynamic-datasource的特性

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事务混用。
2、dynamic-datasource的约定和规范
  1. 本框架只做 切换数据源 这件核心的事情,并不限制你的具体操作,切换了数据源可以做任何CRUD。
  2. 配置文件所有以下划线 _ 分割的数据源 首部 即为组的名称,相同组名称的数据源会放在一个组下。
  3. 切换数据源可以是组名,也可以是具体数据源名称。组名则切换时采用负载均衡算法切换。
  4. 默认的数据源名称为 master ,你可以通过 spring.datasource.dynamic.primary 修改。
  5. 方法上的注解优先于类上注解。
  6. DS支持继承抽象类上的DS,暂不支持继承接口上的DS。
3、dynamic-datasource的使用方法
(1)、引入dynamic-datasource-spring-boot-starter依赖
<dependency>
  <groupId>com.baomidou</groupId>
  <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
  <version>3.5.0</version>
</dependency>
(2)配置多数据源。
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:
(3)、使用 @DS 切换数据源

@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");
  }
}
(三)、模拟读写分离
1、搭建环境

①模拟两台服务器上的数据库一主一丛,都有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
2、实现读写分离
@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("ziyu@qq.com");
     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;

    ....
}
  • 实现元对象处理器接口:com.baomidou.mybatisplus.core.handlers.MetaObjectHandler
@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)
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值