Mybatis-Plus(進階)


一、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);
        }

結果如下:成功查詢到數據

image-20201002204658559

但是如果只有實體類沒有Mapper接口就會報錯如下:

image-20201002204637904

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

image-20201002204910403

三、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)

image-20201002210032812

這些類中方法的細節可以通過查看每個方法的簽名來發現,或者直接查看 MyBatis 發行包中的源代碼。 如果你想做的不僅僅是監控方法的調用,那么你最好相當了解要重寫的方法的行為。 因為在試圖修改或重寫已有方法的行為時,很可能會破壞 MyBatis 的核心模塊。 這些都是更底層的類和方法,所以使用插件的時候要特別當心。

插件的作用就是為了增強功能,它通過攔截器攔截需要增強的方法,通過動態代理,為被攔截的方法增強功能

3.2、如何使用插件

要想自定義插件必須要實現Interceptor接口,這個接口中有三個方法需要實現。

image-20201002210352760

方法 作用
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);     
        }

如下圖,成功對查詢方法進行攔截,並在控制台打印語句

image-20201003133809846

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);
        }

結果如下,控制台拋出異常,禁止全表刪除

image-20201003134713407

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));
        }

結果如圖:

image-20201003140406746

可以通過logMessageFormat和customLogMessageFormat自定義日志的輸出格式如下:

logMessageFormat=com.p6spy.engine.spy.appender.CustomLineFormat
customLogMessageFormat=%(currentTime) \n SQL耗時: %(executionTime) ms \n 連接信息: %(category)-%(connectionId) \n 執行語句: %(sqlSingleLine)

image-20201003142006822

可以選擇的變量如下

  • %(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步 記得兩步

  1. 插件配置
//高版本中已經被標記過時
@Bean
public OptimisticLockerInterceptor optimisticLockerInterceptor() {
    return new OptimisticLockerInterceptor();
}
  1. 注解實體字段@Version,必需要!!
@Version
private Integer version;

特別說明:

  • 支持的數據類型只有:int,Integer,long,Long,Date,Timestamp,LocalDateTime
  • 整數類型下 newVersion = oldVersion + 1
  • newVersion 會回寫到 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;
}

表結構如下:

image-20201003143326848

測試:

@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);
        }

結果如下:

image-20201003144856156

image-20201003144905029

第一條修改語句成功,第二條修改語句失敗,因為修改數據的時候要判斷取出數據時的version和修改數據時數據庫的version是否一致。第一條語句修改過后,數據庫的version變為了1,而user2的version是0,此時再去修改數據,就與數據庫version不一致,更新失敗.

3.6、邏輯刪除插件

所謂邏輯刪除就是將數據標記為刪除,而並非真正的物理刪除(非DELETE操作),查詢時需要攜帶狀態條件,確保被標記的數據不被查詢到。這樣做的目的就是避免數據被真正的刪除。

使用邏輯刪除還是需要兩個步驟:

  1. 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)
  1. 實體類加上@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;
    }
    

表字段:

image-20201003150808525

測試:

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

image-20201003151458947

image-20201003151557838

刪除操作只是將deleted置為1,查詢時將deleted作為條件查詢,數據庫中數據並沒有被真正刪除。

注意在向有邏輯刪除的表插入數據:

  1. 字段在數據庫定義默認值(推薦)
  2. insert 前自己 set 值
  3. 使用自動填充功能

四、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);
        }

結果如下:成功查詢到數據

image-20201003160405009

五、自動填充功能

原理:

  • 實現元對象處理器接口:com.baomidou.mybatisplus.core.handlers.MetaObjectHandler
  • 注解填充字段 @TableField(.. fill = FieldFill.INSERT) 生成器策略部分也可以配置!
  1. 實現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:插入和更新時填充

數據庫表

image-20201003161406980

image-20201003161505605

插入測試:

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

結果:插入數據成功,insert_time字段自動填充了當前時間

image-20201003161902111

image-20201003161915000

更新測試:

    @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自動填充當前時間

image-20201003162234375

image-20201003162250535

Mybatis-Plus還支持通用枚舉,以及代碼生成器,多數據源等等一系列的功能,個位如果需要可以前往官方文檔https://baomidou.com/查看,中國人寫的文檔,看起來很輕松的。最后給大家推薦一個IDEA的插件MybatisX,可以實現Java接口與XML文件的跳轉,可以為Mapper方法自動生成XML


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM