Mybatis-Plus(进阶)

    科技2022-07-11  88

    一、ActiveRecord模式

    ​ ActiveRecord也属于ORM(对象关系映射)层,由Rails最早提出,遵循标准的ORM模型:表映射到记录,记录映射到对象,字段映射到对象属性。配合遵循的命名和配置惯例,能够很大程度的快速实现模型的操作,而且简洁易懂。

    ActiveRecord的主要思想是:

    每一个数据库表对应创建一个类,类的每一个对象实例对应于数据库中表的一行记录;通常表的每个字段在类中都有相应的Field;

    ActiveRecord同时负责把自己持久化,在ActiveRecord中封装了对数据库的访问,即CURD;;

    ActiveRecord是一种领域模型(Domain Model),封装了部分业务逻辑;

    二、使用ActiveRecord

    使用ActiveRecord需要再实体类上继承Model接口,同时也需要有一个Mapper接口实现BaseMapper接口,并指定泛型.

    例如:

    UserDemo实体类继承了Model

    @Component @Data @AllArgsConstructor @NoArgsConstructor @TableName("user") public class UserDemo extends Model<UserDemo> { @TableId(value = "id") private Long id; //select如果为false表示不从数据库查询该字段 @TableField(select = true ) private String name; private Integer age; private String email; //插入数据时自动填充数据,需要配置插件 @TableField(fill = FieldFill.INSERT) private LocalDateTime insertTime; //修改数据时自动填充数据,需要配置插件 @TableField(fill = FieldFill.UPDATE) private LocalDateTime updateTime; }

    同时需要有个Mapper接口继承BaseMapper并指定泛型为UserDemo

    @Repository public interface UserDemoMapper extends BaseMapper<UserDemo>{ }

    测试

    @Test public void testActiveRecord(){ List<UserDemo> users = userDemo.selectAll(); users.forEach(System.out::println); }

    结果如下:成功查询到数据

    但是如果只有实体类没有Mapper接口就会报错如下:

    Model抽象类里面也有通用的CRUD,可以直接使用,这里就不一一演示了

    三、Mybatis-Plus常用插件

    3.1、插件简介

    ​ MyBatis 允许你在映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:

    Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)ParameterHandler (getParameterObject, setParameters)ResultSetHandler (handleResultSets, handleOutputParameters)StatementHandler (prepare, parameterize, batch, update, query)

    这些类中方法的细节可以通过查看每个方法的签名来发现,或者直接查看 MyBatis 发行包中的源代码。 如果你想做的不仅仅是监控方法的调用,那么你最好相当了解要重写的方法的行为。 因为在试图修改或重写已有方法的行为时,很可能会破坏 MyBatis 的核心模块。 这些都是更底层的类和方法,所以使用插件的时候要特别当心。

    插件的作用就是为了增强功能,它通过拦截器拦截需要增强的方法,通过动态代理,为被拦截的方法增强功能

    3.2、如何使用插件

    要想自定义插件必须要实现Interceptor接口,这个接口中有三个方法需要实现。

    方法作用intercept这个方法是mybatis的核心方法,要实现自定义逻辑,基本都是改造这个方法,其中invocation参数可以通过反射要获取原始方法和对应参数信息plugin它的作用是用来生成一个拦截对方,也就是代理对象,使得被代理的对象一定会经过intercept方法,通常都会使用mybatis提供的工具类Plugin来获取代理对象,如果有自己独特需求,可以自定义setProperties这个方法就是用来设置插件的一些属性

    @Intercepts注解就是用来标明拦截4个接口中的那个接口和接口中的哪些方法。如下自定义一个拦截器插件

    @Intercepts({@Signature(type = Executor.class,method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class} ), @Signature(type = Executor.class,method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class} )} ) public class MyInteceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { System.out.println(""); System.out.println(""); System.out.println(""); System.out.println(""); System.out.println("成功拦截查询!!!"); System.out.println(""); System.out.println(""); System.out.println(""); System.out.println(""); Object proceed = invocation.proceed(); return proceed; } @Override public Object plugin(Object target) { return Plugin.wrap(target,this); } @Override public void setProperties(Properties properties) { } } //将拦截器添加到容器中 @Bean public MyInteceptor myInteceptor(){ return new MyInteceptor(); }

    对Executor接口的query方法进行了拦截,并通过动态代理生成代理类,此时,执行查询方法就会别拦截

    测试:

    @Test public void testSelect(){ userMapper.selectById(2); }

    如下图,成功对查询方法进行拦截,并在控制台打印语句

    3.3、执行分析插件

    在MP中提供了对SQL执行的分析的插件,可用作阻断全表更新、删除的操作,注意:该插件仅适用于开发环境,不适用于生产环境

    //注册SqlExplainInterceptor(高版本已经被标记过时) @Bean public SqlExplainInterceptor sqlExplainInterceptor(){ SqlExplainInterceptor sqlExplainInterceptor = new SqlExplainInterceptor(); List<ISqlParser> sqlParserList = new ArrayList<>(); // 攻击 SQL 阻断解析器、加入解析链 sqlParserList.add(new BlockAttackSqlParser()); sqlExplainInterceptor.setSqlParserList(sqlParserList); return sqlExplainInterceptor; } //删除表中所有的数据 @Test public void sqlExplainTest(){ userMapper.delete(null); }

    结果如下,控制台抛出异常,禁止全表删除

    3.4、性能分析插件

    性能分析拦截器,用于输出每条 SQL 语句及其执行时间,可以设置最大执行时间,超过时间会抛出异常。同样该插件只适合开发环境。

    导入p6spy的pom依赖

    <!--SQL分析插件依赖--> <dependency> <groupId>p6spy</groupId> <artifactId>p6spy</artifactId> <version>3.9.0</version> </dependency>

    使用p6spy需要修改jdbc的URL和Dirver

    spring: datasource: username: 'root' password: 'root' url: jdbc:p6spy:mysql://localhost:3306/mybatis_plus?useSSL=false&serverTimezone=Asia/Shanghai&useLegacyDatetimeCode=false driver-class-name: com.p6spy.engine.spy.P6SpyDriver

    同时在resources目录下创建spy.properties文件(MP官网文档复制的)

    #3.2.1以上使用 modulelist=com.baomidou.mybatisplus.extension.p6spy.MybatisPlusLogFactory,com.p6spy.engine.outage.P6OutageFactory # 自定义日志打印 logMessageFormat=com.baomidou.mybatisplus.extension.p6spy.P6SpyLogger #日志输出到控制台 appender=com.baomidou.mybatisplus.extension.p6spy.StdoutLogger # 使用日志系统记录 sql #appender=com.p6spy.engine.spy.appender.Slf4JLogger # 设置 p6spy driver 代理 deregisterdrivers=true # 取消JDBC URL前缀 useprefix=true # 配置记录 Log 例外,可去掉的结果集有error,info,batch,debug,statement,commit,rollback,result,resultset. excludecategories=info,debug,result,commit,resultset # 日期格式 dateformat=yyyy-MM-dd HH:mm:ss # 实际驱动可多个 #driverlist=org.h2.Driver # 是否开启慢SQL记录 outagedetection=true # 慢SQL记录标准 2 秒 outagedetectioninterval=2

    测试:

    @Test public void testSelect(){ userMapper.selectBatchIds(Arrays.asList(1,2,3)); }

    结果如图:

    可以通过logMessageFormat和customLogMessageFormat自定义日志的输出格式如下:

    logMessageFormat=com.p6spy.engine.spy.appender.CustomLineFormat customLogMessageFormat=%(currentTime) \n SQL耗时: %(executionTime) ms \n 连接信息: %(category)-%(connectionId) \n 执行语句: %(sqlSingleLine)

    可以选择的变量如下

    %(connectionId):connection id%(currentTime):当前时间%(executionTime):执行耗时%(category):执行分组%(effectiveSql):提交的 SQL 换行%(effectiveSqlSingleLine):提交的 SQL 不换行显示%(sql):执行的真实 SQL 语句,已替换占位%(sqlSingleLine):执行的真实 SQL 语句,已替换占位不换行显示

    3.5、乐观锁插件

    使用乐观锁的意图是当要更新一条记录的时候,希望这条记录没有被别人更新。

    乐观锁实现方式:

    取出记录时,获取当前version

    更新时,带上这个version

    执行更新时, set version = newVersion where version = oldVersion

    如果version不对,就更新失败

    乐观锁配置需要2步 记得两步

    插件配置 //高版本中已经被标记过时 @Bean public OptimisticLockerInterceptor optimisticLockerInterceptor() { return new OptimisticLockerInterceptor(); } 注解实体字段@Version,必需要!! @Version private Integer version;

    特别说明:

    支持的数据类型只有:int,Integer,long,Long,Date,Timestamp,LocalDateTime整数类型下 newVersion = oldVersion + 1newVersion 会回写到 entity 中仅支持 updateById(id) 与 update(entity, wrapper) 方法在 update(entity, wrapper) 方法下, wrapper 不能复用!!!

    实体类如下:

    @Component @Data @AllArgsConstructor @NoArgsConstructor @TableName("tb_user") public class User extends Model<User> { private Long id; private String userName; private String password; private String name; private Integer age; private String email; @Version private Integer version; }

    表结构如下:

    测试:

    @Test public void testOptimisticLock(){ User user1 = userMapper.selectById(1l); User user2 = userMapper.selectById(2l); user1.setAge(80); user2.setAge(90); int result = this.userMapper.updateById(user1); int result2 = this.userMapper.updateById(user2); }

    结果如下:

    第一条修改语句成功,第二条修改语句失败,因为修改数据的时候要判断取出数据时的version和修改数据时数据库的version是否一致。第一条语句修改过后,数据库的version变为了1,而user2的version是0,此时再去修改数据,就与数据库version不一致,更新失败.

    3.6、逻辑删除插件

    所谓逻辑删除就是将数据标记为删除,而并非真正的物理删除(非DELETE操作),查询时需要携带状态条件,确保被标记的数据不被查询到。这样做的目的就是避免数据被真正的删除。

    使用逻辑删除还是需要两个步骤:

    application.yaml添加配置(步骤一) mybatis-plus: global-config: db-config: logic-delete-field: deleted # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2) logic-delete-value: 1 # 逻辑已删除值(默认为 1) logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)

    实体类加上@TableLogic注解(步骤二)

    @Component @Data @AllArgsConstructor @NoArgsConstructor @TableName("tb_user") public class User extends Model<User> { private Long id; private String userName; private String password; private String name; private Integer age; private String email; @Version private Integer version; @TableLogic private Integer deleted; }

    表字段:

    测试:

    @Test public void testLogicDelete(){ QueryWrapper<User> wrapper = new QueryWrapper<>(); wrapper.eq("id",1l); userMapper.delete(wrapper); userMapper.selectById(1l); }

    删除操作只是将deleted置为1,查询时将deleted作为条件查询,数据库中数据并没有被真正删除。

    注意在向有逻辑删除的表插入数据:

    字段在数据库定义默认值(推荐)insert 前自己 set 值使用自动填充功能

    四、Sql注入器

    全局配置 sqlInjector 用于注入 ISqlInjector 接口的子类,实现自定义方法注入。

    public interface ISqlInjector { /** * <p> * 检查SQL是否注入(已经注入过不再注入) * </p> * * @param builderAssistant mapper 信息 * @param mapperClass mapper 接口的 class 对象 */ void inspectInject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass); }

    自定义自己的通用方法可以实现接口 ISqlInjector 也可以继承抽象类 AbstractSqlInjector 注入通用方法 SQL 语句 然后继承 BaseMapper 添加自定义方法,全局配置 sqlInjector 注入 MP 会自动将类所有方法注入到 mybatis 容器中。

    4.1、编写MyBaseMapper

    @Repository public interface MyBaseMapper extends BaseMapper<User> { /** * 自定义查询所有方法 * @return */ List findAll(); }

    4.2、自定义Sql注入器

    如果直接继承AbstractSqlInjector的话,原有的BaseMapper中的方法将失效,所以我们选择继承DefaultSqlInjector进行扩展。

    public class MySqlInjector extends DefaultSqlInjector { @Override public List<AbstractMethod> getMethodList(Class<?> mapperClass) { List<AbstractMethod> methodList = super.getMethodList(mapperClass); //扩充自定义的方法 methodList.add(new FindAll()); return methodList; } }

    4.3、编写FindAll实体类

    public class FindAll extends AbstractMethod { @Override public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) { String sql = String.format("<script>%s SELECT %s FROM %s %s %s\n</script>", sqlFirst(), sqlSelectColumns(tableInfo, true), tableInfo.getTableName(), sqlWhereEntityWrapper(true, tableInfo), sqlComment()); SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass); return this.addSelectMappedStatementForTable(mapperClass, "findAll", sqlSource, tableInfo); } }

    4.4、注入到容器中

    @Bean public MySqlInjector mySqlInjector(){ return new MySqlInjector(); }

    4.5、测试

    @Autowired MyBaseMapper MyBaseMapper; @Test public void testFindAll(){ List<User> all = MyBaseMapper.findAll(); System.out.println(all); }

    结果如下:成功查询到数据

    五、自动填充功能

    原理:

    实现元对象处理器接口:com.baomidou.mybatisplus.core.handlers.MetaObjectHandler注解填充字段 @TableField(.. fill = FieldFill.INSERT) 生成器策略部分也可以配置! 实现MetaObjectHandler接口 @Component public class DateHandler implements MetaObjectHandler { /** *插入时给insertTime自动填充 */ @Override public void insertFill(MetaObject metaObject) { setFieldValByName("insertTime", LocalDateTime.now(),metaObject); } /** *修改时给updateTime自动填充 */ @Override public void updateFill(MetaObject metaObject) { setFieldValByName("updateTime", LocalDateTime.now(),metaObject); } }

    实体字段:

    @Component @Data @AllArgsConstructor @NoArgsConstructor @TableName("user") public class UserDemo extends Model<UserDemo> { private Long id; private String name; private Integer age; private String email; @TableField(fill = FieldFill.INSERT) private LocalDateTime insertTime; @TableField(fill = FieldFill.UPDATE) private LocalDateTime updateTime; }

    FieldFill可选

    DEFAULT :默认不处理INSERT:插入时填充UPDATE:更新时填充INSERT_UPDATE:插入和更新时填充

    数据库表

    插入测试:

    @Test public void testFillInsert(){ UserDemo userDemo = new UserDemo(); userDemo.setEmail("163@qq.com"); userDemo.setAge(100); userDemo.setName("马化腾"); userDemoMapper.insert(userDemo); }

    结果:插入数据成功,insert_time字段自动填充了当前时间

    更新测试:

    @Test public void testFillInsert(){ UserDemo userDemo2 = new UserDemo(); userDemo2.setEmail("jd@qq.com"); userDemo2.setAge(100); userDemo2.setName("强子"); QueryWrapper<UserDemo> wrapper = new QueryWrapper<>(); wrapper.eq("id",14); userDemoMapper.update(userDemo2,wrapper); }

    结果:修改成功,update_time自动填充当前时间

    Mybatis-Plus还支持通用枚举,以及代码生成器,多数据源等等一系列的功能,个位如果需要可以前往官方文档https://baomidou.com/查看,中国人写的文档,看起来很轻松的。最后给大家推荐一个IDEA的插件MybatisX,可以实现Java接口与XML文件的跳转,可以为Mapper方法自动生成XML。

    Processed: 0.035, SQL: 8