之前始终应用Mybatis-Plus,说实话,集体还是比拟喜爱Mybatis-Plus

ORM框架用的比拟多的就两个,JPAMybatis。据说国内用Mybatis比拟多,国外用JPA比拟多。

Mybatis-Plus是在Mybatis的根底上,减少了很多牛的性能。

再粘一下官网介绍的个性,又啰嗦了:

  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、DB2、H2、HSQL、SQLite、Postgre、SQLServer 等多种数据库
  11. 内置性能剖析插件:可输入 Sql 语句以及其执行工夫,倡议开发测试时启用该性能,能疾速揪出慢查问
  12. 内置全局拦挡插件:提供全表 delete 、 update 操作智能剖析阻断,也可自定义拦挡规定,预防误操作

具体的能够去官网看:mybatis.plus/ 官网新域名也是牛。反正用过的都说好。

至于JPA,尽管集体感觉有点太死板,不过也有值得学习的中央。

很早以前,用Mybatis-Plus的时候,有一个比拟麻烦的问题,就是如果一组数据存在多张表中,这些表之间可能是一对一,一对多或者多对一,那我要想全副查出来就要调好几个Mapper的查询方法。代码行数一下就减少了很多。

之前也看过Mybatis-Plus的源码,想过如何使Mybatis-Plus反对多表联接查问。可是发现难度不小。因为Mybatis-Plus底层就只反对单表。

最近看到JPA@OneToOne@OneToMany@ManyToMany这些注解,突然一个想法就在我的脑海里闪现进去,如果像JPA那样应用注解的形式,是不是简略很多呢?

当时申明,全是本人想的,没有看JPA源码, 所以实现形式可能和JPA不一样。

说干就干

  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})})@Slf4jpublic 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。欢送大家一起来奉献。