給Mybatis-Plus插上小翅膀,支持多表查詢

以前一直使用Mybatis-Plus,說實話,我的仍是比較喜歡Mybatis-Plusjava

ORM框架用的比較多的就兩個,JPAMybatis。聽說國內用Mybatis比較多,國外用JPA比較多。mysql

Mybatis-Plus是在Mybatis的基礎上,增長了不少牛🍺的功能。sql

再粘一下官網介紹的特性,又囉嗦了:

  1. 無侵入:只作加強不作改變,引入它不會對現有工程產生影響,如絲般順滑
  2. 損耗小:啓動即會自動注入基本 CURD,性能基本無損耗,直接面向對象操做
  3. 強大的 CRUD 操做:內置通用 Mapper、通用 Service,僅僅經過少許配置便可實現單表大部分 CRUD 操做,更有強大的條件構造器,知足各種使用需求
  4. 支持 Lambda 形式調用:經過 Lambda 表達式,方便的編寫各種查詢條件,無需再擔憂字段寫錯
  5. 支持主鍵自動生成:支持多達 4 種主鍵策略(內含分佈式惟一 ID 生成器 - Sequence),可自由配置,完美解決主鍵問題
  6. 支持 ActiveRecord 模式:支持 ActiveRecord 形式調用,實體類只需繼承 Model 類便可進行強大的 CRUD 操做
  7. 支持自定義全局通用操做:支持全局通用方法注入( Write once, use anywhere )
  8. 內置代碼生成器:採用代碼或者 Maven 插件可快速生成 Mapper 、 Model 、 Service 、 Controller 層代碼,支持模板引擎,更有超多自定義配置等您來使用
  9. 內置分頁插件:基於 MyBatis 物理分頁,開發者無需關心具體操做,配置好插件以後,寫分頁等同於普通 List 查詢
  10. 分頁插件支持多種數據庫:支持 MySQL、MariaDB、Oracle、DB二、H二、HSQL、SQLite、Postgre、SQLServer 等多種數據庫
  11. 內置性能分析插件:可輸出 Sql 語句以及其執行時間,建議開發測試時啓用該功能,能快速揪出慢查詢
  12. 內置全局攔截插件:提供全表 delete 、 update 操做智能分析阻斷,也可自定義攔截規則,預防誤操做

詳細的能夠去官網看:mybatis.plus/ 官網新域名也是牛🍺。反正用過的都說好。數據庫

至於JPA,雖然我的以爲有點太死板,不過也有值得學習的地方。mybatis

很早之前,用Mybatis-Plus的時候,有一個比較麻煩的問題,就是若是一組數據存在多張表中,這些表之間多是一對一,一對多或者多對一,那我要想所有查出來就要調好幾個Mapper的查詢方法。代碼行數一下就增長了不少。app

以前也看過Mybatis-Plus的源碼,想過如何使Mybatis-Plus支持多表聯接查詢。但是發現難度不小。由於Mybatis-Plus底層就只支持單表。框架

最近看到JPA@OneToOne@OneToMany@ManyToMany這些註解,突然一個想法就在個人腦海裏閃現出來,若是像JPA那樣使用註解的方式,是否是簡單不少呢?分佈式

事先聲明,全是本身想的,沒有看JPA源碼, 因此實現方式可能和JPA不同。ide

說幹就幹

  1. 添加註解
  2. 處理註解
  3. 打包發佈

可能有人不知道,其實Mybatis也是支持攔截器的,既然如此,用攔截器處理註解就能夠啦。性能

註解 One2One

@Inherited
@Documented
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface One2One {

    /**
     * 本類主鍵列名
     */
    String self() default "id";

    /**
     * 本類主鍵在關聯表中的列名
     */
    String as();

    /**
     * 關聯的 mapper
     */
    Class<? extends BaseMapper> mapper();

}

說一下,假若有兩張表,AB是一對一的關係,AidB表中是a_id,用這樣的方式關聯的。 在A的實體類中使用這個註解,self就是id,而as就是a_id,意思就是Aid做爲a_id來查詢,而mapper就是BMapper,下面是例子A就是UserAccountB就是UserAddress

@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@ApiModel(value = "UserAccount對象", description = "用戶相關")
public class UserAccount extends Model<UserAccount> {

    private static final long serialVersionUID = 1L;

    @ApiModelProperty(value = "id")
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    @ApiModelProperty(value = "暱稱")
    private String nickName;

    @TableField(exist = false)
    //把id的值 做爲userId  在 UserAddressMapper中 查詢
    @One2One(self = "id", as = "user_id", mapper = UserAddressMapper.class)
    private UserAddress address;

    @Override
    protected Serializable pkVal() {
        return this.id;
    }
}

Mybatis攔截器 One2OneInterceptor

這裏再也不詳細介紹攔截器了,以前也寫了幾篇關於Mybatis攔截器的,有興趣的能夠去看看。

Mybatis攔截器實現Geometry類型數據存儲與查詢

Mybatis攔截器打印完整SQL

@Intercepts({
        @Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})
})
@Slf4j
public class One2OneInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object result = invocation.proceed();
        if (result == null) {
            return null;
        }
        if (result instanceof ArrayList) {
            ArrayList list = (ArrayList) result;
            for (Object o : list) {
                handleOne2OneAnnotation(o);
            }
        } else {
            handleOne2OneAnnotation(result);
        }
        return result;
    }

    @SneakyThrows
    private void handleOne2OneAnnotation(Object o) {
        Class<?> aClass = o.getClass();
        Field[] fields = aClass.getDeclaredFields();
        for (Field field : fields) {
            field.setAccessible(true);
            One2One one2One = field.getAnnotation(One2One.class);
            if (one2One != null) {
                String self = one2One.self();
                Object value = MpExtraUtil.getValue(o, self);
                String as = one2One.as();
                Class<? extends BaseMapper> mapper = one2One.mapper();
                BaseMapper baseMapper = SpringBeanFactoryUtils.getApplicationContext().getBean(mapper);
                QueryWrapper<Object> eq = Condition.create().eq(as, value);
                Object one = baseMapper.selectOne(eq);
                field.set(o, one);
            }
        }
    }

    @Override
    public Object plugin(Object o) {
        return Plugin.wrap(o, this);
    }

    @Override
    public void setProperties(Properties properties) {
    }
}

Mybatis攔截器能夠針對不一樣的場景進行攔截,好比:

  1. Executor:攔截執行器的方法。
  2. ParameterHandler:攔截參數的處理。
  3. ResultHandler:攔截結果集的處理。
  4. StatementHandler:攔截Sql語法構建的處理。

這裏是經過攔截結果集的方式,在返回的對象上查找這個註解,找到註解後,再根據註解的配置,自動去數據庫查詢,查到結果後把數據封裝到返回的結果集中。這樣就避免了本身去屢次調Mapper的查詢方法。

難點:雖然註解上標明瞭是什麼Mapper,但是在攔截器中取到的仍是BaseMapper,而用BaseMapper實在很差查詢,我試了不少方法,不過還好Mybatis-Plus支持使用Condition.create().eq(as, value);拼接條件SQL,而後可使用baseMapper.selectOne(eq);去查詢。

public class MpExtraUtil {

    @SneakyThrows
    public static Object getValue(Object o, String name) {
        Class<?> aClass = o.getClass();
        Field[] fields = aClass.getDeclaredFields();
        for (Field field : fields) {
            field.setAccessible(true);
            if (field.getName().equals(name)) {
                return field.get(o);
            }
        }
        throw new IllegalArgumentException("未查詢到名稱爲:" + name + " 的字段");
    }
}

MpExtraUtil就是使用反射的方式,獲取id的值。

再講一個多對多的註解
@Inherited
@Documented
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Many2Many {

    /**
     * 本類主鍵列名
     */
    String self() default "id";

    /**
     * 本類主鍵在中間表的列名
     */
    String leftMid();

    /**
     * 另外一個多方在中間表中的列名
     */
    String rightMid();

    /**
     * 另外一個多方在本表中的列名
     */
    String origin();

    /**
     * 關聯的 mapper
     */
    Class<? extends BaseMapper> midMapper();

    /**
     * 關聯的 mapper
     */
    Class<? extends BaseMapper> mapper();

假設有AA_BB三張表,在A的實體類中使用這個註解, self就是A表主鍵idleftMid就是A表的id在中間表中的名字,也就是a_id,而rightMidB表主鍵在中間表的名字,就是b_id, origin就是B表本身主鍵原來的名字,即idmidMapper是中間表的Mapper,也就是A_B對應的MappermapperB表的Mapper

這個確實有點繞。

還有一個@One2Many就不說了,和@One2One同樣,至於Many2One,從另外一個角度看就是@One2One

使用方法

先把表建立好,而後代碼生成器一鍵生成代碼。

CREATE TABLE `user_account` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'id',
  `nick_name` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '暱稱',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用戶相關';

CREATE TABLE `user_address` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '地址id',
  `user_id` bigint(20) DEFAULT NULL COMMENT '用戶id',
  `address` varchar(200) DEFAULT NULL COMMENT '詳細地址',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

CREATE TABLE `user_class` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '課程id',
  `class_name` varchar(20) DEFAULT NULL COMMENT '課程名稱',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

CREATE TABLE `user_hobby` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '愛好id',
  `user_id` bigint(20) DEFAULT NULL COMMENT '用戶id',
  `hobby` varchar(40) DEFAULT NULL COMMENT '愛好名字',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

CREATE TABLE `user_mid_class` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '中間表id',
  `user_id` bigint(20) DEFAULT NULL COMMENT '用戶id',
  `class_id` bigint(20) DEFAULT NULL COMMENT '課程id',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

添加依賴,已經發布到中央倉庫了,能夠直接使用:

<dependency>
    <groupId>top.lww0511</groupId>
    <artifactId>mp-extra</artifactId>
    <version>1.0.1</version>
</dependency>

在啓動類上添加註解

@EnableMpExtra

配置攔截器

由於通常項目都會配置本身的MybatisConfiguration,我在這裏配置後,打包,而後被引入,是沒法生效的。

因此就想了一種折中的方法。

之前MybatisConfiguration是經過new出來的,如今經過MybatisExtraConfig.getMPConfig();來獲取,這樣獲取到的MybatisConfiguration就已經添加好了攔截器。

完整Mybatis-Plus配置類例子,注意第43行:

@Slf4j
@Configuration
@MapperScan(basePackages = "com.ler.demo.mapper", sqlSessionTemplateRef = "sqlSessionTemplate")
public class MybatisConfig {

    private static final String BASE_PACKAGE = "com.ler.demo.";

    @Bean("dataSource")
    public DataSource dataSource() {
        try {
            DruidDataSource dataSource = new DruidDataSource();
            dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
            dataSource.setUrl("jdbc:mysql://localhost/mp-extra?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai");
            dataSource.setUsername("root");
            dataSource.setPassword("adminadmin");

            dataSource.setInitialSize(1);
            dataSource.setMaxActive(20);
            dataSource.setMinIdle(1);
            dataSource.setMaxWait(60_000);
            dataSource.setPoolPreparedStatements(true);
            dataSource.setMaxPoolPreparedStatementPerConnectionSize(20);
            dataSource.setTimeBetweenEvictionRunsMillis(60_000);
            dataSource.setMinEvictableIdleTimeMillis(300_000);
            dataSource.setValidationQuery("SELECT 1");
            return dataSource;
        } catch (Throwable throwable) {
            log.error("ex caught", throwable);
            throw new RuntimeException();
        }
    }

    @Bean(name = "sqlSessionFactory")
    public SqlSessionFactory sqlSessionFactory(@Qualifier("dataSource") DataSource dataSource) throws Exception {
        MybatisSqlSessionFactoryBean factoryBean = new MybatisSqlSessionFactoryBean();
        factoryBean.setDataSource(dataSource);
        factoryBean.setVfs(SpringBootVFS.class);
        factoryBean.setTypeAliasesPackage(BASE_PACKAGE + "entity");

        Resource[] mapperResources = new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/*.xml");
        factoryBean.setMapperLocations(mapperResources);
        // 43行 獲取配置
        MybatisConfiguration configuration = MybatisExtraConfig.getMPConfig();
        configuration.setDefaultScriptingLanguage(MybatisXMLLanguageDriver.class);
        configuration.setJdbcTypeForNull(JdbcType.NULL);
        configuration.setMapUnderscoreToCamelCase(true);
        configuration.addInterceptor(new SqlExplainInterceptor());
        configuration.setUseGeneratedKeys(true);
        factoryBean.setConfiguration(configuration);
        return factoryBean.getObject();
    }

    @Bean(name = "sqlSessionTemplate")
    public SqlSessionTemplate sqlSessionTemplate(@Qualifier("sqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory);
    }

    @Bean(name = "transactionManager")
    public PlatformTransactionManager platformTransactionManager(@Qualifier("dataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }

    @Bean(name = "transactionTemplate")
    public TransactionTemplate transactionTemplate(@Qualifier("transactionManager") PlatformTransactionManager transactionManager) {
        return new TransactionTemplate(transactionManager);
    }

}

在實體類上創建關係

@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@ApiModel(value = "UserAccount對象", description = "用戶相關")
public class UserAccount extends Model<UserAccount> {

    private static final long serialVersionUID = 1L;

    @ApiModelProperty(value = "id")
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    @ApiModelProperty(value = "暱稱")
    private String nickName;

    @TableField(exist = false)
    //把id的值 做爲userId  在 UserAddressMapper中 查詢
    @One2One(self = "id", as = "user_id", mapper = UserAddressMapper.class)
    private UserAddress address;

    @TableField(exist = false)
    @One2Many(self = "id", as = "user_id", mapper = UserHobbyMapper.class)
    private List<UserHobby> hobbies;

    @TableField(exist = false)
    @Many2Many(self = "id", leftMid = "user_id", rightMid = "class_id", origin = "id"
            , midMapper = UserMidClassMapper.class, mapper = UserClassMapper.class)
    private List<UserClass> classes;

    @Override
    protected Serializable pkVal() {
        return this.id;
    }

}

主要是那幾個註解。對了還要加@TableField(exist = false),否則會報錯。

源碼:https://www.douban.com/note/7...

測試接口

@Slf4j
@RestController
@RequestMapping("/user")
@Api(value = "/user", description = "用戶")
public class UserAccountController {

    @Resource
    private UserAccountService userAccountService;

    @Resource
    private UserAccountMapper userAccountMapper;

    @ApiOperation("查詢一個")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "", value = "", required = true),
    })
    @GetMapping(value = "/one", name = "查詢一個")
    public HttpResult one() {
        //service
        UserAccount account = userAccountService.getById(1L);
        //mapper
        // UserAccount account = userAccountMapper.selectById(1L);
        //AR模式
        // UserAccount account = new UserAccount();
        // account.setId(1L);
        // account = account.selectById();
        return HttpResult.success(account);
    }
}

接口很是簡單,調用內置的getById,但是卻查出了全部相關的數據,這都是由於配置的那些註解。

能夠看到其實發送了好幾條SQL。第一條是userAccountService.getById(1L),後面幾條都是自動發送的。

總結

實在不想貼太多代碼,其實仍是挺簡單的,源碼地址還有示例地址都貼出來啦,有興趣的能夠去看一下。以爲好用能夠點個Star。歡迎你們一塊兒來貢獻。

相關文章
相關標籤/搜索