以前一直使用Mybatis-Plus
,說實話,我的仍是比較喜歡Mybatis-Plus
。java
ORM
框架用的比較多的就兩個,JPA
和Mybatis
。聽說國內用Mybatis
比較多,國外用JPA
比較多。mysql
而Mybatis-Plus
是在Mybatis
的基礎上,增長了不少牛🍺的功能。sql
詳細的能夠去官網看:mybatis.plus/ 官網新域名也是牛🍺。反正用過的都說好。數據庫
至於JPA
,雖然我的以爲有點太死板,不過也有值得學習的地方。mybatis
很早之前,用Mybatis-Plus
的時候,有一個比較麻煩的問題,就是若是一組數據存在多張表中,這些表之間多是一對一,一對多或者多對一,那我要想所有查出來就要調好幾個Mapper
的查詢方法。代碼行數一下就增長了不少。app
以前也看過Mybatis-Plus
的源碼,想過如何使Mybatis-Plus
支持多表聯接查詢。但是發現難度不小。由於Mybatis-Plus
底層就只支持單表。框架
最近看到JPA
的@OneToOne
、@OneToMany
、@ManyToMany
這些註解,突然一個想法就在個人腦海裏閃現出來,若是像JPA
那樣使用註解的方式,是否是簡單不少呢?分佈式
事先聲明,全是本身想的,沒有看JPA
源碼, 因此實現方式可能和JPA
不同。ide
可能有人不知道,其實Mybatis
也是支持攔截器的,既然如此,用攔截器處理註解就能夠啦。性能
@Inherited @Documented @Target({ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) public @interface One2One { /** * 本類主鍵列名 */ String self() default "id"; /** * 本類主鍵在關聯表中的列名 */ String as(); /** * 關聯的 mapper */ Class<? extends BaseMapper> mapper(); }
說一下,假若有兩張表,A
和B
是一對一的關係,A
表id
在B
表中是a_id
,用這樣的方式關聯的。 在A
的實體類中使用這個註解,self
就是id
,而as
就是a_id
,意思就是A
的id
做爲a_id
來查詢,而mapper
就是B
的Mapper
,下面是例子A
就是UserAccount
,B
就是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攔截器的,有興趣的能夠去看看。
@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
攔截器能夠針對不一樣的場景進行攔截,好比:
Executor
:攔截執行器的方法。ParameterHandler
:攔截參數的處理。ResultHandler
:攔截結果集的處理。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();
假設有A
、 A_B
,B
三張表,在A
的實體類中使用這個註解, self
就是A
表主鍵id
,leftMid
就是A
表的id
在中間表中的名字,也就是a_id
,而rightMid
是B
表主鍵在中間表的名字,就是b_id
, origin
就是B
表本身主鍵原來的名字,即id
,midMapper
是中間表的Mapper
,也就是A_B
對應的Mapper
,mapper
是B
表的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
。歡迎你們一塊兒來貢獻。