Mybatis-Plus 免手写Mapper、自动建表(mysql、pgsql、sqlite)、数据自动填充(类似JPA的审计)、关联查询(类似sql中的join)、冗余数据自动更新、动态条件

简介

尽管MybatisPlus (后文简称MP)相比较Mybatis丝滑了很多,但是,日常使用中,是否偶尔仍会怀念JPA(Hibernate)的那种纵享丝滑的感受,更好的一心投入业务开发中,如果你也是如此,那么恭喜你发现了MybatisPlusExt(后文简称MPE)。
MPE对MP做了进一步的拓展封装,即保留MP原功能,又添加更多有用便捷的功能。同样坚持与MP对Mybatis的原则,只做增强不做改变,所以,即便是在使用MPE的情况下,也完全可以百分百的使用MP的方式,因此MP能做的,MPE不仅能做还能做的更多。实际上MPE只入侵了MP的一个类(TableInfoHelper),因为要完成注解继承合并的操作,必须重写MP的注解读取逻辑。
增强功能具体体现在几个方面:免手写Mapper、自动建表(mysql、pgsql、sqlite)、数据自动填充(类似JPA的审计)、关联查询(类似sql中的join)、冗余数据自动更新、动态条件 等功能做了补充完善。
如果感觉本框架对您有所帮助,烦请去Gitee给个小星星⭐️,欢迎来撩共同进步。

特别说明

博客园的文档可能不及时,更新的功能和动态请及时关注代码仓库以及官方教程

项目地址

https://gitee.com/tangzc/mybatis-plus-ext

快速开始

引入jar包

starter内自带了MybatisPlus及spring-boot的依赖管理,如果要更改springboot的版本,可以排除掉。但是如果要变更MybatisPlus的版本,请注意了,框架中重写了MP中的TableInfoHelper类,该类在不同的MybatisPlus版本下有所差异,另外框架内也引用了MP的部分工具类,例如LambdaUtils、ReflectionKit等,这些工具类在不同的版本也有所变动,需要小心,哈哈哈哈,实在不行,可以联系我协助你~~

框架在设计上,尽量以拓展的功能为单位做了模块拆分,所有功能均能独立引入也可以合并引入,大家视情况选用吧。
版本号说明:为了更好的标记本框架与MybatisPlus框架版本的对应关系,特制定了版本号命名规则为 [mybatis-plus版本]-[jdk17-]EXT[版本号],例如:3.5.3.1-EXT307.2(jdk8版本)、3.5.3.1-jdk17-EXT307.3(jdk17版本)

方式一:全功能引入【建议】

maven仓库地址:https://mvnrepository.com/artifact/com.tangzc/mybatis-plus-ext-boot-starter

!!!重点说明,全功能框架内部已经引入了MybatisPlus的包,自己项目中的需要去掉!!!

<!-- 全功能整体引入 -->
<dependency>
  <groupId>com.tangzc</groupId>
  <artifactId>mybatis-plus-ext-boot-starter</artifactId>
  <version>{maven仓库搜索最新版}</version>
</dependency>

方式二:按需引入

📢注意:按需引入的情况下请在所有mybatis-plus-ext-*的引入之后(主要是为了控制maven引入包的顺序),单独引入对应版本的mybatis-plus,对应版本寻找方法参考版本号命名方式**[mybatis-plus版本]-EXT[版本号]**
<!-- 如果想只引入自动建表 -->
<dependency>
    <groupId>com.tangzc</groupId>
    <artifactId>mybatis-plus-ext-actable-core</artifactId>
    <version>{maven仓库搜索最新版}</version>
</dependency>
<!-- 如果想只引入自动创建EntityMapper -->
<dependency>
    <groupId>com.tangzc</groupId>
    <artifactId>mybatis-plus-ext-automapper</artifactId>
    <version>{maven仓库搜索最新版}</version>
</dependency>
<!-- 如果想只引入关联查询 -->
<dependency>
    <groupId>com.tangzc</groupId>
    <artifactId>mybatis-plus-ext-bind</artifactId>
    <version>{maven仓库搜索最新版}</version>
</dependency>
<!-- 如果想只引入数据冗余(关联更新) -->
<dependency>
    <groupId>com.tangzc</groupId>
    <artifactId>mybatis-plus-ext-datasource</artifactId>
    <version>{maven仓库搜索最新版}</version>
</dependency>
<!-- 如果想只引入动态条件 -->
<dependency>
    <groupId>com.tangzc</groupId>
    <artifactId>mybatis-plus-ext-condition</artifactId>
    <version>{maven仓库搜索最新版}</version>
</dependency>
<!-- 保险起见,额外单独引入 mybatis plus,全功能整体引入不需要-->
<dependency>
  <groupId>com.baomidou</groupId>
  <artifactId>mybatis-plus-boot-starter</artifactId>
  <version>${mybatis-plus.version}</version>
</dependency>

自动建表

新版(自[3.5.2-EXT300]开始)重写了整个功能模块,同时也借鉴了ACTable的部分思路,再次向ACTable提出感谢。

完全兼容MybatisPlus

近乎百分百兼容MybatisPlus的配置,其中类名和字段名与数据库的表名和列名的处理规则均保持一致,通过MybatisPlus的相关配置即可生效。

说明

目前仅实现了MySQL、SQLite数据库,下一步打算支持PgSql。

逻辑流程图:

在这里插入图片描述

快速开始

引入自动建表,非常方便快捷,两个注解搞定。请看下面例子
1、启动类上添加@EnableAutoTable

@EnableAutoTable
@SpringBootApplication
public class DemoAutoTableApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoAutoTableApplication.class, args);
    }
}

2、实体上添加@Table

import com.tangzc.mpe.autotable.annotation.Table;
import lombok.Data;

@Data
@Table
public class MysqlTable {

    private Integer id;

    private String username;

    private Integer age;

    private String phone;
}

更多使用教程和复杂用法,请移步:https://www.yuque.com/dontang/codewiki/ogsgcluxhtl1wzyr

自动创建EntityMapper

日常使用MybatisPlus的过程中,总是需要手动为每个Entity创建一个对应的Mapper接口,同时继承BaseMapper接口,大部分情况下,如果没有自定义SQL的场景,Mapper接口就显得多余了,这个工作,适合让框架来做。

引入方式

新版本(版本号大于等于3.5.3.1-EXT400)starter自带,无需单独引入

<dependency>
    <groupId>com.tangzc</groupId>
    <artifactId>mybatis-plus-ext-boot-starter</artifactId>
    <!-- jdk8的版本 -->
    <version>3.5.3.1-EXT400</version>
    <!-- jdk17的版本 -->
    <version>3.5.3.1-jdk17-EXT400</version>
</dependency>

老版本,或者想独立引入的情况

<dependency>
    <groupId>com.tangzc</groupId>
    <artifactId>mybatis-plus-ext-automapper</artifactId>
    <!-- jdk8的版本 -->
    <version>3.5.3.1-EXT400</version>
    <!-- jdk17的版本 -->
    <version>3.5.3.1-jdk17-EXT400</version>
</dependency>

插件方式引入。不了解这种方式的,忽略即可

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>${maven-compiler-plugin.version}</version>
            <configuration>
                ...
                <annotationProcessorPaths>
                    <path>
                        <groupId>com.tangzc</groupId>
                        <artifactId>mybatis-plus-ext-automapper</artifactId>
                        <!-- jdk8的版本 -->
                        <version>3.5.3.1-EXT400</version>
                        <!-- jdk17的版本 -->
                        <version>3.5.3.1-jdk17-EXT400</version>
                    </path>
                </annotationProcessorPaths>
            </configuration>
        </plugin>
    </plugins>
</build>

使用

@AutoMapper一个注解即可

package org.example.app.entity;

import com.tangzc.mpe.automapper.AutoMapper;
import com.tangzc.mpe.autotable.annotation.Table;
import lombok.Data;

// 标记为该实体生成一个Mapper
@AutoMapper
@Table
@Data
public class TestTable {

    private String id;
    private String name;
    private int age;
}
package org.example.app.service;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.example.app.entity.TestTable;
// 注意📢:这里的包名默认跟TestEntity在一起
import org.example.app.entity.TestTableMapper;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.List;

@Service
public class TestTableService {

    @Resource
    private TestTableMapper testTableMapper;

    public List<TestTable> list() {
        return testTableMapper.selectList(new QueryWrapper<>());
    }
}

说明:假设TestTable是一个Entity实体类,框架会在代码编译期间,在同级目录(包)下自动创建一个叫TestTableMapper的接口,其他地方可以直接引用。

@AutoMapper有三个属性:

value:自定义完整的Mapper名称,指定后,将会替换掉默认生成策略
suffix:生成的Mapper类的后缀,前半部分固定为Entity的名字
packageName:生成Mapper所在的包名,默认与Entity包名一致

数据填充

作用:

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

注意:

如果本地已经实现了com.baomidou.mybatisplus.core.handlers.MetaObjectHandler,将会导致MPE框架的com.tangzc.mpe.base.AutoFillMetaObjectHandler无法生效,进而将会产生@InsertOptionDate、@InsertOptionUser、@InsertUpdateOptionDate、@InsertUpdateOptionUser、@DefaultValue...等注解无法工作的情况,请注意!

示例:

以文章发布为例,在发布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("发布时间")
    // 【插入】数据时候会自动获取系统当前时间赋值,支持多种数据类型,具体可参考@OptionDate注解详细介绍(注意,这里的时间是MP执行insert的操作的时候的时间,并不是对象构建时候的时间)
    @InsertOptionDate
    private Date publishedTime;
    
    @ColumnComment("发布人")
    // 【插入】的时候,自动填充用户id,UserIdAutoFillHandler看下面代码
    @InsertOptionUser(UserIdAutoFillHandler.class)
    private String publishedUserId;
    
    @ColumnComment("发布人名字")
    // 【插入】的时候,自动填充用户名字,UsernameAutoFillHandler看下面代码
    @InsertOptionUser(UsernameAutoFillHandler.class)
    private String publishedUsername;
    
    @ColumnComment("最后更新时间")
    // 【插入和更新】数据时候会自动获取系统当前时间赋值,支持多种数据类型,具体可参考@OptionDate注解详细介绍
    @InsertUpdateOptionDate
    private Date publishedTime;
    
    @ColumnComment("最后更新人")
    // 【更新】的时候,自动填充用户id,UserIdAutoFillHandler看下面代码
    // @UpdateOptionUser(UserIdAutoFillHandler.class)
    // 【插入和更新】的时候,自动填充用户id,UserIdAutoFillHandler看下面代码
    @InsertUpdateOptionUser(UserIdAutoFillHandler.class)
    private String publishedUserId;
    
    @ColumnComment("最后更新人名字")
    // 【更新】的时候,自动填充用户名字,UsernameAutoFillHandler看下面代码
    // @UpdateOptionUser(UsernameAutoFillHandler.class)
    // 【插入和更新】的时候,自动填充用户名字,UsernameAutoFillHandler看下面代码
    @InsertUpdateOptionUser(UsernameAutoFillHandler.class)
    private String publishedUsername;
}
/**
 * 全局获取用户ID
 * 此处实现IOptionByAutoFillHandler接口和AutoFillHandler接口均可,建议实现IOptionByAutoFillHandler接口,
 * 因为框架内的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");
    }
}

关联查询

作用:

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

讲解:

示例展开前,先明确几个概念:

  • @BindEntity@BindEntityByMid是关联Entity的数据。
  • @BindField@BindFieldByMid系列是关联Entity下的某个字段。
  • @Bind*系列注解用在单类型的属性上即表达一对一,当注解用在集合类型的属性上时便表达一对多的意思。至于多对多的概念,就是外层数据查询的是集合形式,然后集合内部的对象是一对多的标注,多个一对多交至在一起既是多对多了。

以上概念看不懂没关系,看代码,再回过头来理解就好理解了。

注意:

为了解决数据库兼容支持的问题,关联查询底层原理是基于MybatisPlus的BaseMapper实现的,所以要求所有关联的实体必须要对应的Mapper且继承自MybatisPlus的BaseMapper,包括中间表的实体,在使用中间表关联查询的情况下,也需要遵循此约束。
MPE相当于把实体对应的Mapper视为数据访问窗口了,所以但凡需要从数据库查询数据的行为均需要通过对应的Mapper完成。

用法:

// 第一步: 各种办法获取到User集合
List<User> userList = selectUserList();
// 第二步: 核心 Binder.bind 会基于User上的注解,自动级联出相关的数据赋值到userList的user中
Binder.bind(userList);

关联查询.示例一:

基础示例:基于用户与身份证之间一对一的关系,示例展示了用户信息想关联身份证号或身份证全量信息的用法。

1、构建实体对象

@Data
@Table(comment = "身份证信息")
public class IdentityCard {
    
    @ColumnComment("主键")
    private String id;
    
    @Unique
    @ColumnComment("用户id")
    private String userId;
    
    @ColumnComment("身份证号")
    private String identityNumber;
    
    // 身份证其他信息
    ......
}
@Data
@Table(comment = "用户信息")
public class User {

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

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

    @ColumnComment("密码")
    private String password;
  
  	// 关联身份证完整信息
    // 其中‘conditions’的赋值,表示当前对象(User)的id属性与关联对象(IdentityCard)的userId属性相同
    // 即,如果当前User对象的id为1的话,那么IdentityCard数据中某条userId为1的数据会被关联过来
  	@BindEntity(conditions = @JoinCondition(selfField = "id", joinField = "userId"))
  	private IdentityCard idCard;
  
  	// 同理只关联身份证号码,@BindField比@BindEntity多了entity、field字段。
    // 因为无法从属性的类型上直接获取实体,需要指明哪个实体的哪个字段。
  	@BindField(entity = IdentityCard.class, 
               field = "identityNumber", 
               conditions = @JoinCondition(selfField = "id", joinField = "userId"))
  	private String idCardNumber;
}

PS:注意,joinField = "userId"中的_userId_是类的属性,该值的具体表现形式受配置__mybatis-plus.configuration.map-underscore-to-camel-case__的影响。
例如默认情况下,_userId_会自动转为_user_id_参与SQL处理,当__map-underscore-to-camel-case__配置为false的时候,则不会自动转化,保持_userId_的方式参与SQL处理。

2、数据查询

/**
 * 正常继承自MP的BaseMapper<T>即可
 */
@Mapper
public interface UserMapper extends BaseMapper<User> {
    
    /**
     * xml的sql省略了
     */
    List<User> listByUsername(String name);
}
/**
 * 用户服务
 */
@Slf4j
@Service
public class UserService {

    @Resource
    private UserMapper userMapper;

    /**
     * 根据用户的名字模糊查询所有用户的详细信息
     */
    @Transactional(readOnly = true)
    public List<User> searchUserByName(String name) {

        // 获取到需要的user集合
        List<User> userList = userMapper.listByUsername(name);
        // 关键步骤,指定关联角色数据。如果你打开sql打印,会看到3条sql语句,第一条根据id去User表查询user信息,第二条根据userId去IdentityCard中查询
      	// 【推荐】用法一、指定属性关联。
        Binder.bindOn(userList, User::getIdCardNumber);
      	// 【不推荐】用法二、全关联。此种用法默认关联user下所有声明需要绑定的属性。
        // Binder.bind(userList);

        return userList;
    }
}

3、进阶查询(推荐)

熟悉MybatisPlus的话可以利用MybatisPlus的lambda表达式查询

/**
 * 正常继承自MP的BaseMapper<T>即可
 */
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
/**
 * BaseRepository,该类集成了MP的ServiceImpl,ServiceImpl包装了BaseMapper的同时,引入了lambda形式的数据查询
 * BaseRepository在ServiceImpl的基础上,添加了动态监听数据变化的事件,配合【数据冗余】功能,同时也拓展了lambda支持快捷bind操作
 * 后文有关于BaseRepository的专门讲解
 */ 
@Repository
public class UserRepository extends BaseRepository<UserMapper, User> {
}
/**
 * 用户服务
 */
@Slf4j
@Service
public class UserService {

    @Resource
    private UserRepository userRepository;

    /**
     * 【推荐】根据用户的名字模糊查询所有用户的详细信息,等价于上一个查询方式
     */
    @Transactional(readOnly = true)
    public List<User> searchUserByName(String name) {

        // 本框架拓展的lambda查询器lambdaQueryPlus,增加了bindOne、bindList、bindPage
        // 显然这是一种更加简便的查询方式,但是如果存在多级深度的关联关系,此种方法就不适用了,还需要借助Binder
        List<User> userList = userRepository.lambdaQueryPlus()
                .eq(name != null, User::getUsername, name)
          		// 【推荐】用法一、指定属性关联,只关联身份证号这个字段。
                .bindList(User::getIdCardNumber);
      			// 【不推荐】用法二、全关联。
      			// .bindList();

        return userList;
    }
}

关联查询.示例二:

进阶示例:用户与用户发布的文章的场景

场景一:获取用户信息的同时想获取用户的全部发布记录。

1、构建实体对象

@Data
@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;
}
@Data
@Table(comment = "用户信息")
public class User {

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

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

    @ColumnComment("密码")
    private String password;
  
  	// 关联该用户发布的所有文章()
  	@BindEntity(conditions = @JoinCondition(selfField = "id", joinField = "publishedUserId"))
  	private List<Article> articles;
}

2、数据查询

参考例1

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

1、构建实体对象

@Data
@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
@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 = "id", joinField = "publishedUserId"), customCondition = "audit = 1", orderBy = @JoinOrderBy(field = "publishedTime", isAsc = false))
  	private List<Article> articles;
}

2、数据查询

参考例1

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

3.5.2-EXT140 及之后的版本才支持 last 属性

1、构建实体对象

文章实体 见场景二

@Data
@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 = "id", joinField = "userId"), customCondition = "audit = 1", orderBy = @JoinOrderBy(field = "publishedTime", isAsc = false), last = "limit 3")
  	private List<Article> articles;
}

关联查询.示例三:

高级示例:用户-角色-菜单(权限),三者之间分别都是多对多的关系,查询用户的时候,期望查询用户下所有的角色与权限。

1、构建实体对象

@Data
@Table(comment = "菜单信息")
public class Menu {

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

    @ColumnComment("菜单名")
    private String name;
    
    // 省略其他信息
    .......
}
@Data
@Table(comment = "角色信息")
public class Role {

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

    @ColumnComment("角色名")
    private String name;
    
    // @BindEntityByMid是可以实现通过中间表关联数据,主要属性conditions,midEntity指定中间表实体(该实体必须有对应的MP的Mapper),
    // 
    @BindEntityByMid(conditions = @MidCondition(
            midEntity = RoleMenu.class, selfMidField = "roleId", joinMidField = "menuId"
    ), orderBy = @JoinOrderBy(field = "name"))
    private List<Menu> menus;
    
    // 省略其他信息
    .......
}
@Data
@Table(comment = "用户信息")
public class User {

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

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

    @ColumnComment("密码")
    private String password;

    // 通过中间表关联所有相关的角色。
    // 通过中间表的形式需要使用@Bind*ByMid
    @BindEntityByMid(conditions = @MidCondition(
            midEntity = UserRole.class, selfMidField = "userId", joinMidField = "roleId"
    ), orderBy = @JoinOrderBy(field = "name"))
    private List<Role> roles;
    
    // 省略其他信息
    .......
}
@Data
@Table(comment = "角色-菜单关联关系")
public class RoleMenu {

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

    @ColumnComment("角色id")
    private String roleId;

    @ColumnComment("菜单id")
    private String menuId;
}
@Data
@Table(comment = "用户-角色关联关系")
public class UserRole {

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

    @ColumnComment("用户id")
    private String userId;

    @ColumnComment("角色id")
    private String roleId;
}

2、数据查询

/**
 * 用户服务
 */
@Slf4j
@Service
public class UserService {

    @Resource
    private UserRepository userRepository;

    /**
     * 根据用户的名字模糊查询所有用户的详细信息
     */
    @Transactional(readOnly = true)
    public List<UserDetailWithRoleDto> searchUserByName(String name) {

        // MP的lambda查询方式
        List<User> userList = userRepository.lambdaQuery()
                .eq(name != null, User::getUsername, name)
                .list();
        // 关键步骤,指定关联角色数据。如果你打开sql打印,会看到3条sql语句,第一条根据id去User表查询user信息,第二条根据userId去UserRule中间表查询所有的ruleId,第三条sql根据ruleId集合去Rule表查询全部的权限
        Binder.bindOn(userList, User::getRoles);
        // Deeper为一个深度遍历工具,可以深入到对象的多层属性内部,从而获取全局上该层级的所有对象同一属性
      	Binder.bindOn(Deeper.with(userList).inList(User::getRoles), Role::getMenus);

        return UserMapping.MAPPER.toDto5(userList);
    }

    /**
     * 根据用户的名字模糊查询所有用户的详细信息,等价于上一个查询方式
     */
    @Transactional(readOnly = true)
    public List<UserDetailWithRoleDto> searchUserByName2(String name) {

        // 本框架拓展的lambda查询器lambdaQueryPlus,增加了bindOne、bindList、bindPage
        // 显然这是一种更加简便的查询方式,但是如果存在多级深度的关联关系,此种方法就不适用了,还需要借助Binder
        List<User> userList = userRepository.lambdaQueryPlus()
                .eq(name != null, User::getUsername, name)
          		// 用法一、指定属性关联,只关联身份证号这个字段。
                .bindList(User::getRoles);
      			// 用法二、全关联。
      			// .bindList();
        // Deeper为一个深度遍历工具,可以深入到对象的多层属性内部,从而获取全局上该层级的所有对象同一属性
      	Binder.bindOn(Deeper.with(userList).inList(User::getRoles), Role::getMenus);

        return UserMapping.MAPPER.toDto5(userList);
    }
}

数据冗余

作用:

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

讲解:

基于@DataSource注解,框架会自动注册监听EntityUpdateEvent事件,响应的所有MP的Mapper的updateById和updateBatchById两个方法会自动发布EntityUpdateEvent事件。因此,除了MP的updateById和updateBatchById两个更新方法外,其他数据更新方式(比如手动写sql的形式)不会触发数据自动更新,如果想触发需要用户自己抛出EntityUpdateEvent事件,完成数据自动更新。

示例:

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

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

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

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

    @ColumnComment("头像")
    private String icon;
    
    // 省略其他属性
    ......
}
@Data
@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 = "username", conditions = @Condition(selfField = "userId"))
    @ColumnComment("评论人名称")
    private String userName;

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

动态条件

作用:

根据预先设置的条件函数,对数据的更新、删除、查询做筛选操作。

示例:

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

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

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

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

    @ColumnComment("发布人")
    @InsertOptionUser(UserIdAutoFillHandler.class)
    // 添加了该注解后,针对文章的查询、修改、删除操作,均会被自动带上 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的问题,进而无法正常的反序列化已经存在的数据。

示例:

@Serializable + @TableName(autoResultMap = true) 两个注解,其中@Serializable是对@TableField的包装,指定了typeHandler为CustomJacksonTypeHandler

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

    @ColumnComment("ID")
    private Long id;

    @Serializable // 必须
    @ColumnComment("爱好")
    private List<Like> likes;
}

@Data
class Like {
    private String id;
    private String name;
}

BaseEntity使用

通常的表设计中,都会要求添加一些审计数据,比如创建人、创建时间、最后修改人、最后修改时间,但是这些属性又不应该属于业务的,更多的是为了数据管理使用的。如果业务需要使用的话,建议起一个有意义的业务名称与上述的创建时间区分开,比如用户的注册时间(registrationTime)。为了简化数据审计字段的工作量,框架内部集成了BaseEntity。

@Getter
@Setter
public class BaseEntity<ID_TYPE extends Serializable, TIME_TYPE> {

    // 这里就是数据填充样例那里提到的IOptionByAutoFillHandler接口
    // 此处单独指定一个标记性的接口是为了区别用户其他数据的自动填充,例如用户名、用户电话等都会实现AutoFillHandler接口,框架上根据该接口无法拿到唯一的实现,因此同样IOptionByAutoFillHandler在整个系统中也只能有一个实现,不然会报错。
    @InsertOptionUser(IOptionByAutoFillHandler.class)
    @ColumnComment("创建人")
    protected ID_TYPE createBy;
    @InsertUpdateOptionUser(IOptionByAutoFillHandler.class)
    @ColumnComment("最后更新人")
    protected ID_TYPE updateBy;
    @InsertOptionDate
    @ColumnComment("创建时间")
    protected TIME_TYPE createTime;
    @InsertUpdateOptionDate
    @ColumnComment("最后更新时间")
    protected TIME_TYPE updateTime;
}

还存在某些情况下数据表要求设计成逻辑删除(逻辑删除存在很多弊端,不建议无脑所有表都设计为逻辑删除),所以框架同时提供了一个BaseLogicEntity,该实现方式利用的是MP本身自带的逻辑删除策略。

@Getter
@Setter
public class BaseLogicEntity<ID_TYPE extends Serializable, TIME_TYPE> extends BaseEntity<ID_TYPE, TIME_TYPE> {

    // 使用了MP支持的逻辑删除注解
    @TableLogic
    @DefaultValue("0")
    @ColumnComment("逻辑删除标志")
    protected Integer deleted;
}

BaseRepository使用

建议开发中以此为数据基本操作类,而不是以Mapper为基础操作类,如果需要使用Mapper中的方法,可以直接通过getMapper()取得Entity对应的Mapper类,此类与Mapper类相比做了很多的增强功能,尤其是其lambda语法,非常高效便捷。

// 集成了MP的ServiceImpl,实现了IBaseRepository接口(内部拓展了lambda查询操作)
public abstract class BaseRepository<M extends BaseMapper<E>, E> extends ServiceImpl<M, E> implements IBaseRepository<E> {

    @Override
    public boolean updateById(E entity) {
        boolean result = super.updateById(entity);
        if(result) {
            // 数据自动更新@DataSource注解的配合逻辑
            SpringContextUtil.getApplicationContext()
                    .publishEvent(EntityUpdateEvent.create(entity));
        }
        return result;
    }

    @Override
    public boolean updateBatchById(Collection<E> entityList, int batchSize) {
        boolean result = super.updateBatchById(entityList, batchSize);
        if(result) {
            // 数据自动更新@DataSource注解的配合逻辑
            for (E entity : entityList) {
                SpringContextUtil.getApplicationContext().publishEvent(EntityUpdateEvent.create(entity));
            }
        }
        return result;
    }

    @Override
    protected Class<M> currentMapperClass() {
        return (Class<M>) ReflectionKit.getSuperClassGenericType(this.getClass(), BaseRepository.class, 0);
    }

    @Override
    protected Class<E> currentModelClass() {
        return (Class<E>) ReflectionKit.getSuperClassGenericType(this.getClass(), BaseRepository.class, 1);
    }
}

注解详细介绍

自动建表注解

只有小部分注解,进行了轻微改动,基本所有注解均是通用的,详细教程可以直接参考A.CTable官方。

@Table

  1. 新增primary属性,对应@TablePrimary
  2. isNull属性为了一致性改为了isNotNull属性默认false
  3. 新增dsName属性,对应@DsName

@TableCharset

@TableComment

@TableEngine

@TablePrimary

新增注解,等同@Table中的primary属性,在多个Entity映射一张表的情况下,确定主Entity是哪个,数据表生成的时候根据主表来生成。

@DsName

新增注解,等同@Table中的dsName属性,在多数据源场景下,指定某个表的数据源。

@IgnoreTable

@EnableTimeSuffix

@Column

@ColumnComment

@ColumnDefault

原@DefaultValue,跟本框架中的数据插入的时候指定默认值的注解重名了,因此把这里改名字了

@ColumnType

@IsAutoIncrement

@IsKey

@IsNotNull

@IsNativeDefValue

@Unique

@Index

@IgnoreUpdate


数据填充类注解

@OptionDate

描述:

自动赋值数据操作时间。需结合mybatis-plus原框架注解[@TableField](https://mybatis.plus/guide/annotation.html#tablefield) (该注解的使用请查看官方文档,懒得看的话,请往下读,有惊喜)一并使用才有效。

被标注的字段,在可允许的类型范围(StringLonglongDateLocalDateLocalDateTime)内,数据被操作的情况下,会自动被赋值上当前时间。

如果使用String的话需要同时指明**_format_**参,用以确认格式化后的样式。

字段:

属性 类型 必需 默认值 描述
format String 非必需 yyyy-MM-dd HH:mm:ss 如果字段类型为String,需要制定字符串格式
override boolean 非必需 true 若对象上存在值,是否覆盖

扩展注解:

注解 描述
@InsertOptionDate 基于@OptionDate
的拓展,无需结合[@TableField](https://mybatis.plus/guide/annotation.html#tablefield)
,数据插入的时候,自动赋值数据操作时间。
@UpdateOptionDate 基于@OptionDate
的拓展,无需结合[@TableField](https://mybatis.plus/guide/annotation.html#tablefield)
,数据更新注意:update(Wrapper updateWrapper)方法除外)的时候,自动赋值数据操作时间。
@InsertUpdateOptionDate 基于@OptionDate
的拓展,无需结合[@TableField](https://mybatis.plus/guide/annotation.html#tablefield)
,数据插入更新注意:update(Wrapper updateWrapper)方法除外)的时候,自动赋值数据操作时间。

@OptionUser

描述:

指定实现方式,自动赋值数据操作人员信息。需结合mybatis-plus原框架注解[@TableField](https://mybatis.plus/guide/annotation.html#tablefield) (该注解的使用请查看官方文档,懒得看的话,请往下读,有惊喜)一并使用才有效。

被标注的字段,会根据@OptionUserAuditHandler的实现来返回对应的值。

通常的实现方案都是用户信息(id、name等)放入header中,全局定义函数来获取。

字段:

属性 类型 必需 默认值 描述
value Class> 必需 自定义用户信息生成方式
override boolean 非必需 true 若对象上存在值,是否覆盖

扩展注解:

注解 描述
@InsertOptionUser 基于@OptionUser
的拓展,无需结合[@TableField](https://mybatis.plus/guide/annotation.html#tablefield)
,数据插入的时候,自动赋值操作人信息。
@UpdateOptionUser 基于@OptionUser
的拓展,无需结合[@TableField](https://mybatis.plus/guide/annotation.html#tablefield)
,数据更新注意:update(Wrapper updateWrapper)方法除外)的时候,自动赋值操作人信息。
@InsertUpdateOptionUser 基于@OptionUser
的拓展,无需结合[@TableField](https://mybatis.plus/guide/annotation.html#tablefield)
,数据插入更新注意:update(Wrapper updateWrapper)方法除外)的时候,自动赋值操作人信息。

@DefaultValue

描述:

数据插入的时候字段的默认值,支持类型:String, Integer, int, Long, long, Boolean, boolean, Double, double, Float, float, BigDecimal, Date, LocalDate, LocalDateTime,枚举(仅支持枚举的名字作为默认值)

字段:

属性 类型 必需 默认值 描述
value String 必需 默认值
format boolean 非必需 yyyy-MM-dd HH:mm:ss 如果字段类型为时间类型(Date,LocalDateTime等),需要制定字符串格式

关联查询类注解

@BindField

描述:

绑定其他Entity的某个字段,可实现一对一、一对多的绑定查询。

注意:所有Bind注解底层均依赖相关Entity的Mapper,且Mapper必须继承MybatisPlus的BaseMapper<Entity, ID>

字段:

属性 类型 必需 默认值 描述
entity Class<?> 被关联的Entity
field String 被关联的Entity的具体字段
conditions @JoinCondition[] 关联Entity所需要的条件
customCondition String 被关联的Entity所需要的额外条件,通常指被关联的Entity自身的特殊条件,例如:enable=1 and is_deleted=0
orderBy @JoinOrderBy[] 排序条件,被关联的Entity或者字段为结果集的时候生效
last String 追加到sql最后的字符串,如:limit 1

@BindEntity

描述:

绑定其他Entity,可实现一对一、一对多的绑定查询。

注意:所有Bind注解底层均依赖相关Entity的Mapper,且Mapper必须继承MybatisPlus的BaseMapper<Entity, ID>

字段:

属性 类型 必需 默认值 描述
entity Class<?> 字段声明类型 被关联的Entity,不再需要显示的指明,默认取字段上的声明类型
conditions @JoinCondition[] 关联Entity所需要的条件
customCondition String 被关联的Entity所需要的额外条件,通常指被关联的Entity自身的特殊条件,例如:enable=1 and is_deleted=0
orderBy @JoinOrderBy[] 排序条件,被关联的Entity或者字段为结果集的时候生效
last String 追加到sql最后的字符串,如:limit 1
deepBind boolean false 深度绑定,列表数据的情况下会产生性能问题。(不熟悉的,不建议使用)

@JoinCondition

描述:

绑定条件

字段:

属性 类型 必需 默认值 描述
selfField String 关联Entity所需的自身字段
joinField String "id" 被关联Entity的关联字段,默认为关联Entity的id

@JoinOrderBy

描述:

绑定结果的排序

字段:

属性 类型 必需 默认值 描述
field String 被关联的Entity中结果集排序字段
isAsc boolean false 排序,true:正序,false:倒序

@BindFieldByMid

描述:

通过中间关系Entity的形式绑定其他Entity的某个字段,可实现一对一、一对多、多对多的绑定查询。

注意:所有Bind注解底层均依赖相关Entity的Mapper,且Mapper必须继承MybatisPlus的BaseMapper<Entity, ID>

字段:

属性 类型 必需 默认值 描述
entity Class<?> 被关联的Entity
field String 被关联的Entity的具体字段
conditions @MidCondition 中间表关联条件
customCondition String 被关联的Entity所需要的额外条件,通常指被关联的Entity自身的特殊条件,例如:enable=1 and is_deleted=0
orderBy @JoinOrderBy[] 排序条件,被关联的Entity或者字段为结果集的时候生效
last String 追加到sql最后的字符串,如:limit 1

@BindEntityByMid

描述:

通过中间关系Entity的形式绑定其他Entity,可实现一对一、一对多、多对多的绑定查询。

注意:所有Bind注解底层均依赖相关Entity的Mapper,且Mapper必须继承MybatisPlus的BaseMapper<Entity, ID>

字段:

属性 类型 必需 默认值 描述
entity Class<?> 被关联的Entity
conditions @MidCondition 中间表关联条件
customCondition String 被关联的Entity所需要的额外条件,通常指被关联的Entity自身的特殊条件,例如:enable=1 and is_deleted=0
orderBy @JoinOrderBy[] 排序条件,被关联的Entity或者字段为结果集的时候生效
last String 追加到sql最后的字符串,如:limit 1
deepBind boolean false 深度绑定,列表数据的情况下会产生性能问题。(不熟悉的,不建议使用)

@MidCondition

描述:

中间表条件描述

字段:

属性 类型 必需 默认值 描述
midEntity Class<?> 中间表Entity,需要对应创建其Mapper
selfField String "Id" 关联Entity所需的自身字段
selfMidField String 关联Entity所需的自身字段,中间表字段名
joinField String "id" 被关联Entity的关联字段
joinMidField String 被关联Entity的关联字段,中间表字段名

数据同步注解

@DataSource

描述:

通过注解指定数据来源,底层框架自动通过Spring中的事件机制监听EntityUpdateEvent事件,完成数据自动更新。在BaseRepository<Mapper, Entity>的基类中,默认实现了updateById、updateBatchById两个方法自动发布EntityUpdateEvent事件,所以只要对应Entity的Repository继承了BaseRepository<Mapper, Entity>便具备了通过ID更新数据的自动同步数据的功能。

拓展:分布式情况下如何同步其他服务的数据_?不妨先想一想。其实sourceName属性就是为此情况预留的,引入外部MQ,监听Spring下的EntityUpdateEvent事件,然后推送至MQ,另一边消费MQ中的事件,再还原出EntityUpdateEvent事件广播到各个系统即可,这其中还需要考虑和解决时序和事务的问题。

字段:

属性 类型 必需 默认值 描述
source Class<?> 否,与sourceName
二选一 Void.class 数据来源的Entity class
sourceName String 否,与source
二选一 "" 数据来源的Entity class 的全路径名称(包名.类名)
field String 数据来源的Entity对应的属性
conditions Condition[] 被关联的Entity所需要的条件

@Condition

描述:

数据来源的关联条件

字段:

属性 类型 必需 默认值 描述
selfField String 关联数据来源Entity所需的自身字段
sourceField String "id" 数据来源的Entity的字段,默认为id

动态条件注解

@DynamicCondition

描述:

适用场景:数据筛选,比如根据不同权限获取不同数据,用户只能看到自己的数据,管理员能看到所有人的数据。

具体demo移步快速开始的例子。

字段:

属性 类型 必需 默认值 描述
value Class<? extends IDynamicConditionHandler> IDynamicConditionHandler接口有两个方法,enable()决定了该条件是否生效,values()是条件匹配的值。
posted @ 2021-09-08 10:37  方糖先生  阅读(10212)  评论(2编辑  收藏  举报