Mybatis-Plus 关联查询、自动建表、数据填充、动态条件
简介
本框架(Gitee地址 )结合公司日常业务场景,对Mybatis-Plus 做了进一步的拓展封装,即保留MP原功能,又添加更多有用便捷的功能。具体拓展体现在
数据自动填充(类似JPA中的审计)
、关联查询(类似sql中的join)
、自动建表(仅支持mysql)
、冗余数据自动更新
、动态条件
等功能做了补充完善。其中自动建表
,是在A.CTable 框架上的基础上改进适配本框架的,只保留了其表创建功能,因此改动较大不与原框架兼容。
项目地址
https://gitee.com/tangzc/mybatis-plus-ext
快速开始
引入jar包
starter内自带了MybatisPlus3.4.3.3版本及spring-boot2.3.12的依赖管理,如果要更改springboot的版本,可以排除掉,但是如果要变更MybatisPlus的版本,请注意了,框架中重写了TableInfoHelper,不同版本的MP该类有所变动,同时框架内也采用了MP的部分工具类,例如LambdaUtils、ReflectionKit等在不同的版本也有所变动,需要小心,哈哈哈哈,可以联系我帮你改~~
<dependency>
<groupId>com.tangzc</groupId>
<artifactId>mybatis-plus-ext-boot-starter</artifactId>
<version>1.2.9</version>
</dependency>
自动建表
根据实体上的注解及字段注解自动创建、更新数据库表。
官方的设计思路是默认Bean下的所有字段均不是表字段,需要手动通过@Column声明,我在引用过来之后,改为了默认所有字段均为表字段,只有被MP的@TableField(exist=false)修饰的才会被排除,具备@TableField(exist=false)功能的注解有:@Exclude、@Bind**系列,他们集成了@TableField,且内置exist属性为false了。
另外A.CTable框架内部集成了类似MP的功能,不如MP完善,所以我也剔除掉了,顺带解决了不兼容和bug。同时像DefaultValue注解重名了,也给它改名为ColumnDefault了,另外整理了一遍内部的注解利用spring的AliasFor做了关联,更方便管理。
其中还有一点,@Table里面加了一个primary属性,表示是否为主表,为了支持多个Entity对应一个数据库表(正常用不到请忽略_)
@Data
// @Table标记的可被识别为需要自动创建表的Entity
@Table(comment = "用户")
public class User {
// 自动识别id属性名为主键
// @IsAutoIncrement声明为自增主键,什么都不声明的话,默认为雪花算法的唯一主键(MP的自带功能),推荐默认便于后期的数据分布式存储等处理。
@IsAutoIncrement
// 字段注释
@ColumnComment("主键")
// 字段长度
@ColumnLength(32)
private String id;
// 索引
@Index
// 非空
@IsNotNull
@ColumnComment("名字")
private String name;
// 唯一索引
@Unique
// 非空
@IsNotNull
@ColumnComment("手机号")
private String phone;
// 省略其他属性
......
}
// 启用自动生成数据库表功能,此处简化了A.CTable的复杂配置,均采用默认配置
@EnableAutoTable
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
# actable的配置信息保留了如下几项,均做了默认配置,正常无需配置
actable.table.auto=update
actable.model.pack=[Spring启动类所在包]
actable.database.type=mysql
actable.index.prefix=自己定义的索引前缀#该配置项不设置默认使用actable_idx_
actable.unique.prefix=自己定义的唯一约束前缀#该配置项不设置默认使用actable_uni_
数据填充
可以在数据插入或更新的时候,自动赋值数据操作人、操作时间、默认值等属性。
以文章发布为例,讲解一下数据填充的基本用法。通过如下例子可发现,在创建Artice的时候,我们无需再去关心过多的与业务无关的字段值,只需要关心
title
、content
两个核心数据即可,其他的数据均会被框架处理。
@Data
@Table(comment = "文章")
public class Article {
// 字符串类型的ID,默认也是雪花算法的一串数字(MP的默认功能)
@ColumnComment("主键")
private String id;
@ColumnComment("标题")
private String title;
@ColumnComment("内容")
private String content;
// 文章默认激活状态
@DefaultValue("ACTIVE")
@ColumnComment("内容")
// ActicleStatusEnum(ACTIVE, INACTIVE)
private ActicleStatusEnum status;
@ColumnComment("发布时间")
// 插入数据时候会自动获取系统当前时间赋值,支持多种数据类型,具体可参考@OptionDate注解详细介绍
@InsertOptionDate
private Date publishedTime;
@ColumnComment("发布人")
// 插入的时候,根据UserIdAutoFillHandler自动填充用户id
@InsertOptionUser(UserIdAutoFillHandler.class)
private String publishedUserId;
@ColumnComment("发布人名字")
// 插入的时候,根据UserIdAutoFillHandler自动填充用户名字
@InsertOptionUser(UsernameAutoFillHandler.class)
private String publishedUsername;
@ColumnComment("最后更新时间")
// 插入和更新数据时候会自动获取系统当前时间赋值,支持多种数据类型,具体可参考@OptionDate注解详细介绍
@InsertUpdateOptionDate
private Date publishedTime;
@ColumnComment("最后更新人")
// 插入和更新的时候,根据UserIdAutoFillHandler自动填充用户id
@InsertUpdateOptionUser(UserIdAutoFillHandler.class)
private String publishedUserId;
@ColumnComment("最后更新人名字")
// 插入和更新的时候,根据UserIdAutoFillHandler自动填充用户名字
@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 当前操作的数据对象上的字段
* @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-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*系列是关联Entity的数据,@BindField*系列是关联Entity下的某个字段。当@Bind*系列注解用在对象上即表达一对一,当注解在List上时便表达一对多的意思,当外部对象本身就是查询集合的情况下便是多对多的场景了。
@Data
@Table(comment = "角色信息")
public class Role {
@ColumnComment("主键")
private String id;
@ColumnComment("角色名")
private String name;
}
@Data
@Table(comment = "用户信息")
public class User {
@ColumnComment("主键")
private String id;
@ColumnComment("用户名")
private String username;
@ColumnComment("密码")
private String password;
// 关键配置,声明了User想关联对应的Rule集合,中间表是UserRule
@BindEntityByMid(conditions = @MidCondition(
midEntity = UserRole.class, selfMidField = "userId", joinMidField = "roleId"
))
private List<Role> roles;
}
@Data
@Table(comment = "用户-角色关联关系")
public class UserRole {
@ColumnComment("主键")
private String id;
@ColumnComment("用户id")
private String userId;
@ColumnComment("角色id")
private String roleId;
}
/**
* 用户服务
*/
@Slf4j
@Service
public class UserService {
// UserRepository继承了BaseRepository<UserMapper, User>,后面会讲BaseRepository
@Resource
private UserRepository userRepository;
/**
* 根据用户的名字模糊查询所有用户的详细信息
*/
@Transactional(readOnly = true)
public List<UserDetailWithRoleDto> searchUserByNameWithRule(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);
// Binder.bind(userList); 此种用法默认关联user下所有声明需要绑定的元素
return UserMapping.MAPPER.toDto5(userList);
}
/**
* 根据用户的名字模糊查询所有用户的详细信息,等价于上一个查询方式
*/
@Transactional(readOnly = true)
public List<UserDetailWithRoleDto> searchUserByNameWithRule2(String name) {
// 本框架拓展的lambda查询器lambdaQueryPlus,增加了bindOne、bindList、bindPage
// 显然这是一种更加简便的查询方式,但是如果存在多级深度的关联关系,此种方法就不适用了,还需要借助Binder
List<User> userList = userRepository.lambdaQueryPlus()
.eq(name != null, User::getUsername, name)
.bindList(User::getRoles);
return UserMapping.MAPPER.toDto5(userList);
}
}
提示: 假如存在此种场景:User
、Role
、Menu
三个实体,他们之间的关系是:User
多对多 Role
、Role
多对多Menu
,当我查询出User的集合后,如何获取Role和Menu的数据呢?
// 数据库查询出了用户列表 【1】
List<User> userList = userRepository.list();
// 为所有用户关联角色信息 【2】
Binder.bindOn(userList, User::getRoles);
// 为所有角色信息关联菜单信息 【3】
// Deeper为一个深度遍历工具,可以深入到对象的多层属性内部,从而获取全局上该层级的所有对象同一属性
Binder.bindOn(Deeper.with(userList).inList(User::getRoles), User::getMenus);