likes
comments
collection
share

Dromara 新晋开源项目 MPE ,MybatisPlus 能力拓展增强包

作者站长头像
站长
· 阅读数 7

Dromara 新晋开源项目 MPE ,MybatisPlus 能力拓展增强包

​ 借用MybatisPlus的口号:为简化开发工作、提高生产率而生

​ 尽管MybatisPlus (后文简称MP)相比较Mybatis丝滑了很多,但是日常使用中,是否偶尔仍会怀念JPA(Hibernate)的那种纵享丝滑的感受,更好的一心投入业务开发中,如果你也是如此,那么恭喜你发现了MybatisPlusExt(后文简称MPE)。

​ MPE对MP做了进一步的拓展封装,即保留MP原功能,又添加更多有用便捷的功能。同样坚持MP的原则,只做增强不做改变,所以,即便是在使用MPE的情况下,也可以百分百的只使用MP的方式,因此MP能做的,MPE不仅能做还能做的更多。

​ 增强功能具体体现在几个方面:免手写Mapper自动建表数据自动填充(类似JPA中的审计)关联查询(类似sql中的join)冗余数据自动更新动态查询条件

开始

一、引入jar包

<!-- spring boot2.* -->
<dependency>
    <groupId>com.tangzc</groupId>
    <artifactId>mybatis-plus-ext-spring-boot-starter</artifactId>
    <version>{maven仓库搜索最新版}</version>
</dependency>

<!-- spring boot3.* -->
<dependency>
    <groupId>com.tangzc</groupId>
    <artifactId>mybatis-plus-ext-spring-boot3-starter</artifactId>
    <version>{maven仓库搜索最新版}</version>
</dependency>

二、代码预生成

痛点:

  1. 某个地方的代码想使用下实体字段的名称,但是又不想写死一个字符串(丑、编译期不可校验)。
  2. 手动为每个实体写一个Mapper类,但是Mapper类中都是空的。

这些交给MPE吧!!!

它在代码编译期前,自动预生成实体字段的定义、实体Mapper的接口定义、实体Repository类的定义(该类是进一步封装Mapper的)

// 标记生成表字段定义
@AutoDefine
// 标记生成Mapper和Repository
@AutoRepository
@Data
public class TestTable {

    private String id;
    private String name;
    private int age;
}

效果如下:

Dromara 新晋开源项目 MPE ,MybatisPlus 能力拓展增强包

三、自动建表

MPE自动建表依托于另一款自研框架AutoTable,MPE是基于AutoTable做了部分注解的拓展,同时做了Mybatis-plus的兼容处理。

此处做一个简单的使用介绍

@EnableAutoTable
@SpringBootApplication
public class DemoAutoTableApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoAutoTableApplication.class, args);
    }
}
@Data
@Table
public class MyTable {
    private Integer id;
    private String userName;
}

上述代码就会自动把MyTable映射为my_table表,字段分别是id:intuser_name:varchar(255)

PS:具体表名、字段名是否转下划线是根据MybatisPlus的配置来的

下面展示一个注解全面的例子:

@Data
@AutoDefine
// 指定表的编码
@MysqlCharset(value = "utf8mb4", collate = "utf8mb4_general_ci")
// 指定表的存储引擎
@MysqlEngine("myisam")
// 表头同样可以声明单个索引(此处只是举例,等价于username字段上的@Index)
@TableIndex(name = "username_index", fields = {MyTableDefine.username}, type = IndexTypeEnum.UNIQUE)
// 需要在表头声明多个索引的情况下,需要用@TableIndexes包裹起来
@TableIndexes({
        // 声明普通联合索引
        @TableIndex(name = "username_phone_index", fields = {MyTableDefine.username, MyTableDefine.phone}),
        // 声明唯一联合索引,单独指定phone的索引排序方式,构建索引的时候indexFields中字段的顺序权重高于fields中的字段
        @TableIndex(name = "username_phone_uni_index", fields = {MyTableDefine.username}, indexFields = {@IndexField(field = MyTableDefine.phone, sort = IndexSortTypeEnum.DESC)}, type = IndexTypeEnum.UNIQUE),
})
// 指定表名、表注释、数据源、忽略字段(不参与建表,等效于字段上的@Ignore)
@Table(value = "test_table", comment = "测试表", dsName = "my-mysql", excludeProperty={MyTableDefine.extra})
public class MyTable {
    // 指定主键自增注释、类型(数据库数字类型可以跟java字符串类型相互转化)、长度
    // 注意字段名称id会被自动认定为主键不需要再额外指定
    @ColumnComment("id主键(因为我是独立注解,所以我是大哥,会覆盖下面的comment属性)")
    @ColumnId(mode = IdType.AUTO, comment = "id主键", type = MysqlTypeConstant.BIGINT, length = 32)
    private String id;

    // 字段非NULL
    @NotNull
    // 字段默认值是空字符串
    @ColumnDefault(type = DefaultValueEnum.EMPTY_STRING)
    // 指定字段长度
    @ColumnType(length = 100)
    // 指定字段注释
    @ColumnComment("用户名")
    // 唯一索引
    @Index(type = IndexTypeEnum.UNIQUE)
    private String username;

    // 设置默认值为0
    @ColumnDefault("0")
    @ColumnComment("年龄")
    private Integer age;

    @ColumnType(length = 20)
    // 设置注释、默认值、不为空
    @Column(comment = "电话", defaultValue = "+00 00000000", notNull = true)
  	// 唯一索引快捷方式
    @UniqueIndex
    private String phone;

    // 设置注释、小数(等同于@ColumnType(length = 12, decimalLength = 6))
    @Column(comment = "资产", length = 12, decimalLength = 6)
    private BigDecimal money;

    // boolean值设置默认值
    @ColumnDefault("true")
    @Column(comment = "激活状态")
  	// 普通索引:指定索引名称、注释、索引方法
    @Index(name = "active_index", comment = "激活状态索引")
    private Boolean active;

    // 单独设置字段类型
    @ColumnType(MysqlTypeConstant.TEXT)
    @ColumnComment("个人简介")
    private String description;

    // 设置默认值为当前时间
    @ColumnDefault("CURRENT_TIMESTAMP")
    @Column(comment = "注册时间")
    private LocalDateTime registerTime;

    // 忽略该字段,不参与建表
    @Ignore
    private String extra;
}

四、数据填充

可以在对数据库做插入或更新操作的时候,自动赋值数据操作人、操作时间、默认值等。

以文章发布为例,在发布Artice的时候,我们无需再去关心过多的与业务无关的字段值,最终只需要关心title、content两个核心数据即可,其他的数据均会被框架处理。

其中分别涉及了数据插入数据更新数据插入及更新三个处理时机,其中每个时机均可以插入系统时间及自定义用户信息。

定义文章实体

@Data
@Table(comment = "文章")
public class Article {
    
    // 字符串类型的ID,默认也是雪花算法的一串数字(MP的默认功能)
    @ColumnComment("主键")
    private String id;
    
    @ColumnComment("标题")
    private String title;
    
    @ColumnComment("内容")
    private String content;
    
    // 默认值用法:文章默认激活状态,ACTIVE为ActicleStatusEnum[ACTIVE, INACTIVE]的枚举名称字符串
    @DefaultValue("ACTIVE")
    @ColumnComment("内容")
    private ActicleStatusEnum status;
    
    @ColumnComment("发布时间")
    // 【插入】数据时候会自动获取系统当前时间赋值,支持多种数据类型,具体可参考@FillTime注解详细介绍(注意,这里的时间是MP执行insert的操作的时候的时间,并不是对象构建时候的时间)
    @InsertFillTime
    private Date publishedTime;
    
    @ColumnComment("发布人")
    // 【插入】的时候,自动填充用户id,UserIdAutoFillHandler看下面代码
    @InsertFillData(UserIdAutoFillHandler.class)
    private String publishedUserId;
    
    @ColumnComment("发布人名字")
    // 【插入】的时候,自动填充用户名字,UsernameAutoFillHandler看下面代码
    @InsertFillData(UsernameAutoFillHandler.class)
    private String publishedUsername;
    
    @ColumnComment("最后更新时间")
    // 【插入和更新】数据时候会自动获取系统当前时间赋值,支持多种数据类型,具体可参考@FillTime注解详细介绍
    @InsertUpdateFillTime
    private Date publishedTime;
    
    @ColumnComment("最后更新人")
    // 【更新】的时候,自动填充用户id,UserIdAutoFillHandler看下面代码
    // @UpdateFillData(UserIdAutoFillHandler.class)
    // 【插入和更新】的时候,自动填充用户id,UserIdAutoFillHandler看下面代码
    @InsertUpdateFillData(UserIdAutoFillHandler.class)
    private String publishedUserId;
    
    @ColumnComment("最后更新人名字")
    // 【更新】的时候,自动填充用户名字,UsernameAutoFillHandler看下面代码
    // @UpdateFillData(UsernameAutoFillHandler.class)
    // 【插入和更新】的时候,自动填充用户名字,UsernameAutoFillHandler看下面代码
    @InsertUpdateFillData(UsernameAutoFillHandler.class)
    private String publishedUsername;
}

实现动态填充【用户id】的接口

/**
 * 全局获取用户ID
 * 此处实现IOptionByAutoFillHandler接口和AutoFillHandler接口均可,
 * 实现IOptionByAutoFillHandler接口,可以兼容框架内的BaseEntity。
 * BaseEntity默认需要IOptionByAutoFillHandler的实现。BaseEntity的使用请查看官网。
 */
@Component
public class UserIdAutoFillHandler implements IOptionByAutoFillHandler<String> {

    /**
     * @param object 当前操作的数据对象
     * @param clazz  当前操作的数据对象的class
     * @param field  当前操作的数据对象上的字段
     * @retur
     */
    @Override
    public String getVal(Object object, Class<?> clazz, Field field) {
        RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();
        HttpServletRequest request = ((ServletRequestAttributes)requestAttributes).getRequest();
        // 配合网关或者过滤器,token校验成功后就把用户信息塞到header中
        return request.getHeader("user-id");
    }
}

实现动态填充【用户名】的接口

/**
 * 全局获取用户名
 */
@Component
public class UsernameAutoFillHandler implements AutoFillHandler<String> {

    /**
     * @param object 当前操作的数据对象
     * @param clazz  当前操作的数据对象的class
     * @param field  当前操作的数据对象上的字段
     * @return 当前登录用户id
     */
    @Override
    public String getVal(Object object, Class<?> clazz, Field field) {
        RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();
        HttpServletRequest request = ((ServletRequestAttributes)requestAttributes).getRequest();
        // 配合网关或者过滤器,token校验成功后就把用户信息塞到header中
        return request.getHeader("user-name");
    }
}

五、关联查询

类似JPA的数据关联查询解决方案,替代sql中的join方式(或者内存组装数据的方式),通过注解关联多表之间的关系,查询某实体的时候,自动带出其关联性的数据。

以用户与文章之间的关系来举例

定义实体

@Data
@AutoDefine
@Table(comment = "文章")
public class Article {

    @ColumnComment("主键")
    private String id;

    @ColumnComment("标题")
    private String title;

    @Column(comment = "内容", type = MySqlTypeConstant.MEDIUMTEXT)
    private String content;
    
    @ColumnComment("发布人")
    private String publishedUserId;

    @ColumnComment("审核: 0 不通过、1 通过")
    private int audit;
    
    @ColumnComment("发布时间(时间戳)")
    private Long publishedTime;
}
@Data
@AutoDefine
@Table(comment = "用户信息")
public class User {

    @ColumnComment("主键")
    private String id;

    @ColumnComment("用户名")
    private String username;

    @ColumnComment("密码")
    private String password;
  
  	// 关联该用户发布的所有文章("audit = 1" 表示的是Article下的audit为1的情况,customCondition的值只能是被关联表下的字段值,且会以and的形式添加在查询条件末尾。)
  	@BindEntity(conditions = @JoinCondition(selfField = UserDefine.id, joinField = ArticleDefine.publishedUserId), customCondition = "audit = 1", orderBy = @JoinOrderBy(field = ArticleDefine.publishedTime, isAsc = false))
  	private List<Article> articles;
}

需求:获取用户信息的同时只想获取用户已通过审核的发布记录,并且根据发布时间倒序排序。(通过自定义SQL条件)

【写法一】

// 获取到需要的user集合
User user = userMapper.getByUsername(name);
// 【推荐】用法一、指定属性关联。
Binder.bindOn(user, User::getArticles);
// 【不推荐】用法二、全关联。此种用法关联user下所有声明需要绑定的属性。
// Binder.bind(user);

【写法二】

// 本框架拓展的lambda查询器lambdaQueryPlus,增加了bindOne、bindList、bindPage
// 显然这是一种更加简便的查询方式,但是如果存在多级深度的关联关系,此种方法就不适用了,还需要借助Binder
User user = userRepository.lambdaQueryPlus()
        .eq(User::getUsername, name)
      	// 【推荐】用法一、指定属性关联,只关联文章这个字段。
        .bindList(User::getArticles);
    		// 【不推荐】用法二、全关联。此种用法关联user下所有声明需要绑定的属性。
    		// .bindList();

* 如果你打开sql打印,会看到2条sql语句,第一条根据name去user查询信息,第二条根据userId去article中查询关联的所有数据。

篇幅有限,更多用法(中间表查询、多对多查询等),请移步官方文档

注意:

为了解决数据库兼容支持的问题,关联查询底层原理是基于MybatisPlus的BaseMapper实现的,所以要求所有关联的实体必须要对应的Mapper且继承自MybatisPlus的BaseMapper,包括中间表的实体,在使用中间表关联查询的情况下,也需要遵循此约束。

MPE相当于把实体对应的Mapper视为数据访问窗口了,所以但凡需要从数据库查询数据的行为均需要通过对应的Mapper完成。

六、数据冗余

为了避免高频的数据关联查询,一种方案是做数据冗余,将其他表的部分字段冗余到当前表。但是这个方案牵扯一个数据修改后如何同步的问题,本功能就是为了解决这个问题而生的。

假设用户评论的场景,评论上需要冗余用户名和头像,如果用户的名字和头像有改动,则需要同步新的改动,代码如下:

@Data
@AutoDefine
@Table(comment = "用户信息")
public class User {

    @ColumnComment("主键")
    private String id;

    @ColumnComment("用户名")
    private String username;

    @ColumnComment("头像")
    private String icon;
    
    // 省略其他属性
    ......
}
@Data
@AutoDefine
@Table(comment = "评论")
public class Comment {

    @ColumnComment("主键")
    private String id;

    @ColumnComment("评论内容")
    private String content;

    @ColumnComment("评论人id")
    private String userId;

    // source指定了数据来源的Entity,同样可以使用sourceName来指定全路径的方式,field指定了映射哪个字段
    // conditions中隐含了一个joinField字段,该字段默认是“id”,即@Condition(selfField = "userId", joinField = "id")等同于示例中的写法
    @DataSource(source = User.class, field = UserDefine.username, conditions = @Condition(selfField = UserDefine.userId))
    @ColumnComment("评论人名称")
    private String userName;

    // 如上,同理
    @DataSource(source = User.class, field = UserDefine.icon, condition = @Condition(selfField = UserDefine.userId))
    @ColumnComment("评论人头像")
    private String userIcon;
}

基于@DataSource注解,框架会自动为指定字段注册监听EntityUpdateEvent事件(MPE内置事件,可手动发起),所有MP的Mapper的updateByIdupdateBatchById两个方法执行的时候会自动发布EntityUpdateEvent事件。如果使用其他数据更新方式(比如手动写sql的形式)不会自动触发数据自动更新,如果想触发,需要用户自己抛出EntityUpdateEvent事件,即可完成数据自动更新。

具体用法及讲解,请移步官方文档

七、动态条件

根据预先设置的条件函数,对数据的更新、删除、查询做动态的筛选。常用于数据权限方面。

比如根据不同权限获取不同数据,用户只能看到自己的数据,管理员能看到所有人的数据,我们通常需要在每一个查询、更新、删除的sql操作上都追加上某个条件,这种操作比较机械化,而且某些情况下很容易忘记,可以抽象成注解直接配置到Entity上,就省去了每个数据操作关心这个特殊条件了。

/**
 * congfig中注册动态条件拦截器【1.3.0之前的版本(不包括1.3.0)可以忽略,不注册该Bean】
 */
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
    MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
    // 添加动态条件,若同时添加了其他的拦截器,继续添加即可
    interceptor.addInnerInterceptor(new DynamicConditionInterceptor());
    // 如果使用了分页,请放在DynamicConditionInterceptor之后
    interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
    return interceptor;
}
@Data
@Table(comment = "文章")
public class Article {

    @ColumnComment("主键")
    private String id;

    @ColumnComment("标题")
    private String title;
    
    @ColumnComment("内容")
    private String content;

    @ColumnComment("发布人")
    // 添加了该注解后,针对文章的查询、修改、删除操作,均会被自动带上 published_user_id=?或者published_user_id in (?)的条件,?值来自于CurrentUserDynamicConditionHandler的values()返回值
    @DynamicCondition(CurrentUserDynamicConditionHandler.class)
    private String publishedUserId;
    
    // 省略其他字段
    ......
}
@Component
public class CurrentUserDynamicConditionHandler implements IDynamicConditionHandler {

    @Resource
    private HttpServletRequest request;

    @Override
    public List<Object> values() {
        // 只有当enable()返回true的时候 本动态条件才生效。
        // 返回空集合或者null的时候,sql上体现的是 [column] is null,只返回一个值的时候sql上体现的是 [column]=***,
        // 返回集合的时候,sql上体现的是 [column] in (***)
        String userId = request.getHeader("USER_ID");
        return Collections.singletonList(userId);
    }

    @Override
    public boolean enable() {
        // 简单例子:header中取用户权限,如果是非管理员则执行该过滤条件,如果是管理员默认查全部,返回false,本动态条件失效
        String userRule = request.getHeader("USER_ROLE");
        return !"ADMIN".equals(userRule);
    }
}

具体用法及讲解,请移步官方文档

八、字段序列化与反序列化

数据存储的时候自动序列化字段上的复杂数据类型为字符串(类json格式),数据读取的时候自动反序列化回来,无需额外编写转化的Handler(MP官方的方案,需要手动为每一个复杂数据类型指定一个BaseTypeHandler)。

该方案存在一定的局限性,实际是借鉴了Redisson的一种数据序列化方案,将数据本身的特征(类全名称)在序列化的时候,一并记录下来,用于反序列的依据,所以序列化之后的字符串并不是一个标准的json。这种方案的缺点很明显,就是类的全名称(包名+类名)不能随意更改,因为一旦更改,会导致找不到class的问题,进而无法正常的反序列化已经存在的数据。

@Data
@TableName(autoResultMap = true) // 必须
@Table(comment = "用户")
public class Users {

    @ColumnComment("ID")
    private Long id;

    @Serializable // 必须
    @ColumnComment("爱好")
    private List<Like> likes;
}
@Data
public class Like {
    private String id;
    private String name;
}

感谢

感谢dromara.org开源社区提供的机会

感谢支持的小伙伴

作者介绍

90年,男,已婚,前后端均有涉猎,毕业后一直在济南这座城市,如果有同地区的小伙伴随时可约

作者开源项目,求各位看官动动发财的小手,给个star

gitee.com/dromara/myb…

gitee.com/tangzc/auto…