# scaffold 项目之 MyBatis 数据库
# 简介
MyBatis 是一个优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以使用简单的 XML 或注解来配置和映射原生类型、接口和 Java 的 POJO(Plain Old Java Objects)为数据库中的记录。
MyBatis 是最容易读懂的 java 框架之一, 本项目用 MyBatis 封装成 springboot 组件。
# 实体类
BaseDO 是所有数据库实体父类,有所有子类都共有的熟悉,例如 创建人、创建时间 等 代码如下:
/** | |
* <p> Project: scaffold - BaseDO </p> | |
* | |
* 基础实体对象 | |
* @author Tz | |
* @date 2024/01/09 23:45 | |
* @version 1.0.0 | |
* @since 1.0.0 | |
*/ | |
@Data | |
public abstract class BaseDO implements Serializable { | |
/** | |
* 创建时间 | |
*/ | |
@TableField(fill = FieldFill.INSERT) | |
private LocalDateTime createTime; | |
/** | |
* 最后更新时间 | |
*/ | |
@TableField(fill = FieldFill.INSERT_UPDATE) | |
private LocalDateTime updateTime; | |
/** | |
* 创建者,目前使用 SysUser 的 id 编号 | |
* | |
* 使用 String 类型的原因是,未来可能会存在非数值的情况,留好拓展性。 | |
*/ | |
@TableField(fill = FieldFill.INSERT, jdbcType = JdbcType.VARCHAR) | |
private String creator; | |
/** | |
* 更新者,目前使用 SysUser 的 id 编号 | |
* | |
* 使用 String 类型的原因是,未来可能会存在非数值的情况,留好拓展性。 | |
*/ | |
@TableField(fill = FieldFill.INSERT_UPDATE, jdbcType = JdbcType.VARCHAR) | |
private String updater; | |
/** | |
* 是否删除 | |
*/ | |
@TableLogic | |
private Boolean deleted; | |
} |
createTime+creator字段,创建人相关信息。updater+updateTime字段,创建人相关信息。deleted字段,逻辑删除。
# 主键编号
id 主键编号,推荐使用 Long 型自增,原因是:
- 自增,保证数据库是按顺序写入,性能更加优秀。
- Long 型,避免未来业务增长,超过 Int 范围。
项目的 id 默认采用数据库自增的策略,如果希望使用 Snowflake 雪花算法,可以修改 application.yaml 配置文件,将配置项 mybatis-plus.global-config.db-config.id-type 修改为 ASSIGN_ID 。如下配置所示:
mybatis-plus: | |
configuration: | |
map-underscore-to-camel-case: true # 虽然默认为 true ,但是还是显示去指定下。 | |
global-config: | |
db-config: | |
id-type: NONE # “智能” 模式,基于 IdTypeEnvironmentPostProcessor + 数据源的类型,自动适配成 AUTO、INPUT 模式。 | |
# id-type: AUTO # 自增 ID,适合 MySQL 等直接自增的数据库 | |
# id-type: INPUT # 用户输入 ID,适合 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库 | |
# id-type: ASSIGN_ID # 分配 ID,默认使用雪花算法。注意,Oracle、PostgreSQL、Kingbase、DB2、H2 数据库时,需要去除实体类上的 @KeySequence 注解 | |
logic-delete-value: 1 # 逻辑已删除值 (默认为 1) | |
logic-not-delete-value: 0 # 逻辑未删除值 (默认为 0) | |
banner: false # 关闭控制台的 Banner 打印 | |
type-aliases-package: ${scaffold.info.base-package}.module.*.dal.dataobject | |
# id-type: ASSIGN_ID |
# 逻辑删除
所有表通过 deleted 字段来实现逻辑删除,值为 0 表示未删除,值为 1 表示已删除,可见 application.yaml 配置文件的 logic-delete-value 和 logic-not-delete-value 配置项。如下配置所示:
mybatis-plus: | |
configuration: | |
map-underscore-to-camel-case: true # 虽然默认为 true ,但是还是显示去指定下。 | |
global-config: | |
db-config: | |
id-type: NONE # “智能” 模式,基于 IdTypeEnvironmentPostProcessor + 数据源的类型,自动适配成 AUTO、INPUT 模式。 | |
# id-type: AUTO # 自增 ID,适合 MySQL 等直接自增的数据库 | |
# id-type: INPUT # 用户输入 ID,适合 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库 | |
# id-type: ASSIGN_ID # 分配 ID,默认使用雪花算法。注意,Oracle、PostgreSQL、Kingbase、DB2、H2 数据库时,需要去除实体类上的 @KeySequence 注解 | |
logic-delete-value: 1 # 逻辑已删除值 (默认为 1) | |
logic-not-delete-value: 0 # 逻辑未删除值 (默认为 0) | |
banner: false # 关闭控制台的 Banner 打印 | |
type-aliases-package: ${scaffold.info.base-package}.module.*.dal.dataobject | |
# logic-delete-value 和 logic-not-delete-value 配置 |
所有 SELECT 查询,都会自动拼接
WHERE deleted = 0查询条件,过滤已经删除的记录。如果被删除的记录,只能通过在 XML 或者@SELECT来手写 SQL 语句。例如说:@SELECT("select id from system_user ")
List<UserDO> getAllUser();
建立唯一索引时,需要额外增加
delete_time字段,添加到唯一索引字段中,避免唯一索引冲突。例如说,system_users使用username作为唯一索引:- 未添加前:先逻辑删除了一条
username = Tz的记录,然后又插入了一条username = Tz的记录时,会报索引冲突的异常。
- 已添加后:先逻辑删除了一条
username = Tz的记录并更新delete_time为当前时间,然后又插入一条username = Tz并且delete_time为 0 的记录,不会导致唯一索引冲突。
- 未添加前:先逻辑删除了一条
# 自动填充
DefaultDBFieldHandler 基于 MyBatis 自动填充机制,实现 BaseDO 通用字段的自动设置。代码如下:
/** | |
* <p> Project: scaffold - DefaultDBFieldHandler </p> | |
* | |
* 通用参数填充实现类 | |
* <p> | |
* 如果没有显式的对通用参数进行赋值,这里会对通用参数进行填充、赋值 | |
* @author Tz | |
* @date 2024/01/09 23:45 | |
* @version 1.0.0 | |
* @since 1.0.0 | |
*/ | |
public class DefaultDBFieldHandler implements MetaObjectHandler { | |
@Override | |
public void insertFill(MetaObject metaObject) { | |
if (Objects.nonNull(metaObject) && metaObject.getOriginalObject() instanceof BaseDO) { | |
BaseDO baseDO = (BaseDO) metaObject.getOriginalObject(); | |
LocalDateTime current = LocalDateTime.now(); | |
// 创建时间为空,则以当前时间为插入时间 | |
if (Objects.isNull(baseDO.getCreateTime())) { | |
baseDO.setCreateTime(current); | |
} | |
// 更新时间为空,则以当前时间为更新时间 | |
if (Objects.isNull(baseDO.getUpdateTime())) { | |
baseDO.setUpdateTime(current); | |
} | |
Long userId = WebFrameworkUtils.getLoginUserId(); | |
// 当前登录用户不为空,创建人为空,则当前登录用户为创建人 | |
if (Objects.nonNull(userId) && Objects.isNull(baseDO.getCreator())) { | |
baseDO.setCreator(userId.toString()); | |
} | |
// 当前登录用户不为空,更新人为空,则当前登录用户为更新人 | |
if (Objects.nonNull(userId) && Objects.isNull(baseDO.getUpdater())) { | |
baseDO.setUpdater(userId.toString()); | |
} | |
} | |
} | |
@Override | |
public void updateFill(MetaObject metaObject) { | |
// 更新时间为空,则以当前时间为更新时间 | |
Object modifyTime = getFieldValByName("updateTime", metaObject); | |
if (Objects.isNull(modifyTime)) { | |
setFieldValByName("updateTime", LocalDateTime.now(), metaObject); | |
} | |
// 当前登录用户不为空,更新人为空,则当前登录用户为更新人 | |
Object modifier = getFieldValByName("updater", metaObject); | |
Long userId = WebFrameworkUtils.getLoginUserId(); | |
if (Objects.nonNull(userId) && Objects.isNull(modifier)) { | |
setFieldValByName("updater", userId.toString(), metaObject); | |
} | |
} | |
} |
# “复杂” 字段类型
MyBatis Plus 提供 TypeHandler 字段类型处理器,用于 JavaType 与 JdbcType 之间的转换。示例如下:
/** | |
* <p> Project: scaffold - AdminUserDO </p> | |
* | |
* 管理后台的用户 DO | |
* <p> | |
* KeySequence: 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 | |
* @author Tz | |
* @date 2024/01/09 23:45 | |
* @version 1.0.0 | |
* @since 1.0.0 | |
*/ | |
@TableName(value = "system_users", autoResultMap = true) | |
@KeySequence("system_user_seq") | |
@Data | |
@EqualsAndHashCode(callSuper = true) | |
@Builder | |
@NoArgsConstructor | |
@AllArgsConstructor | |
public class AdminUserDO extends TenantBaseDO { | |
/** | |
* 岗位编号数组 | |
*/ | |
@TableField(typeHandler = JsonLongSetTypeHandler.class) | |
private Set<Long> postIds; | |
} |
- 需要 开启 @TableName (value = "system_users", autoResultMap = true)
autoResultMap = true- @TableField (typeHandler = JsonLongSetTypeHandler.class) 转换的字段需要加:
typeHandler = JsonLongSetTypeHandler.class指定- 自定义类型处理类需重写
AbstractJsonTypeHandler<Object>类的逻辑- 最后系统就可以实现,写入数据库自动转成
json字符串, 读取时就转成了Set<Long>类型
常用的字段类型处理器有:
JacksonTypeHandler:通用的 Jackson 实现 JSON 字段类型处理器。
JsonLongSetTypeHandler:针对Set<Long>的 Jackson 实现 JSON 字段类型处理器。
另外,如果你后续要拓展自定义的 TypeHandler 实现,可以添加到 com.tz.scaffold.framework.mybatis.core.type 包下。
注意事项:
使用 TypeHandler 时,需要设置实体的
@TableName注解的@autoResultMap = true。
# 编码规范
数据库实体类放在
dal.dataobject包下,以 DO 结尾;数据库访问类放在dal.mysql包下,以 Mapper 结尾。如下图所示:![]()
数据库实体类的注释要完整,特别是哪些字段是关联(外键)、枚举、冗余等等。例如说:

禁止在 Controller、Service 中,直接进行 MyBatis Plus 操作。原因是:大量 MyBatis 操作散落在 Service 中,会导致 Service 的代码越来乱,无法聚焦业务逻辑。并且,通过只允许将 MyBatis Plus 操作编写 Mapper 层,更好的实现 SELECT 查询的复用,而不是 Service 会存在很多相同且重复的 SELECT 查询的逻辑。
Mapper 的 SELECT 查询方法的命名,采用 Spring Data 的 "Query methods" 策略,方法名使用
selectBy查询条件规则。例如说:package com.tz.scaffold.module.system.dal.mysql.user;
/*** <p> Project: scaffold - AdminUserMapper </p>
*
* 管理后台的用户 Mapper
* @author Tz
* @date 2024/01/09 23:45
* @version 1.0.0
* @since 1.0.0
*/
@Mapperpublic interface AdminUserMapper extends BaseMapperX<AdminUserDO> {
default AdminUserDO selectByUsername(String username) {
return selectOne(AdminUserDO::getUsername, username);
}default AdminUserDO selectByEmail(String email) {
return selectOne(AdminUserDO::getEmail, email);
}default AdminUserDO selectByMobile(String mobile) {
return selectOne(AdminUserDO::getMobile, mobile);
}default PageResult<AdminUserDO> selectPage(UserPageReqVO reqVO, Collection<Long> deptIds) {
return selectPage(reqVO, new LambdaQueryWrapperX<AdminUserDO>()
.likeIfPresent(AdminUserDO::getUsername, reqVO.getUsername())
.likeIfPresent(AdminUserDO::getMobile, reqVO.getMobile())
.eqIfPresent(AdminUserDO::getStatus, reqVO.getStatus())
.betweenIfPresent(AdminUserDO::getCreateTime, reqVO.getCreateTime())
.inIfPresent(AdminUserDO::getDeptId, deptIds)
.orderByDesc(AdminUserDO::getId));
}default List<AdminUserDO> selectList(UserExportReqVO reqVO, Collection<Long> deptIds) {
return selectList(new LambdaQueryWrapperX<AdminUserDO>()
.likeIfPresent(AdminUserDO::getUsername, reqVO.getUsername())
.likeIfPresent(AdminUserDO::getMobile, reqVO.getMobile())
.eqIfPresent(AdminUserDO::getStatus, reqVO.getStatus())
.betweenIfPresent(AdminUserDO::getCreateTime, reqVO.getCreateTime())
.inIfPresent(AdminUserDO::getDeptId, deptIds));
}default List<AdminUserDO> selectListByNickname(String nickname) {
return selectList(new LambdaQueryWrapperX<AdminUserDO>().like(AdminUserDO::getNickname, nickname));
}default List<AdminUserDO> selectListByStatus(Integer status) {
return selectList(AdminUserDO::getStatus, status);
}default List<AdminUserDO> selectListByDeptIds(Collection<Long> deptIds) {
return selectList(AdminUserDO::getDeptId, deptIds);
}}上面的
selectByUsername(String username)、selectByEmail(String email)等等就是例子。优先使用 LambdaQueryWrapper 条件构造器,使用方法获得字段名,避免手写
"字段"可能写错的情况。例如说:default AdminUserDO selectByUsername(String username) {
return selectOne(AdminUserDO::getUsername, username);
}这里的
selectOne(AdminUserDO::getUsername, username)写法就是正确的简单的单表查询,优先在 Mapper 中通过
default方法实现。例如说:default List<AdminUserDO> selectList(UserExportReqVO reqVO, Collection<Long> deptIds) {
return selectList(new LambdaQueryWrapperX<AdminUserDO>()
.likeIfPresent(AdminUserDO::getUsername, reqVO.getUsername())
.likeIfPresent(AdminUserDO::getMobile, reqVO.getMobile())
.eqIfPresent(AdminUserDO::getStatus, reqVO.getStatus())
.betweenIfPresent(AdminUserDO::getCreateTime, reqVO.getCreateTime())
.inIfPresent(AdminUserDO::getDeptId, deptIds));
}
# CRUD 接口
BaseMapperX 接口,继承 MyBatis Plus 的 BaseMapper 接口,提供更强的 CRUD 操作能力。代码如下:
package com.tz.scaffold.framework.mybatis.core.mapper; | |
/** | |
* <p> Project: scaffold - BaseMapperX </p> | |
* | |
* 在 MyBatis Plus 的 BaseMapper 的基础上拓展,提供更多的能力 | |
* <p> | |
* <li> | |
* 1. {@link BaseMapper} 为 MyBatis Plus 的基础接口,提供基础的 CRUD 能力 | |
* <li> | |
* 2. {@link MPJBaseMapper} 为 MyBatis Plus Join 的基础接口,提供连表 Join 能力 | |
* @author Tz | |
* @date 2024/01/09 23:45 | |
* @version 1.0.0 | |
* @since 1.0.0 | |
*/ | |
public interface BaseMapperX<T> extends MPJBaseMapper<T> { | |
default PageResult<T> selectPage(PageParam pageParam, @Param("ew") Wrapper<T> queryWrapper) { | |
// 特殊:不分页,直接查询全部 | |
if (PageParam.PAGE_SIZE_NONE.equals(pageParam.getPageNo())) { | |
List<T> list = selectList(queryWrapper); | |
return new PageResult<>(list, (long) list.size()); | |
} | |
// MyBatis Plus 查询 | |
IPage<T> mpPage = MyBatisUtils.buildPage(pageParam); | |
selectPage(mpPage, queryWrapper); | |
// 转换返回 | |
return new PageResult<>(mpPage.getRecords(), mpPage.getTotal()); | |
} | |
default <DTO> PageResult<DTO> selectJoinPage(PageParam pageParam, Class<DTO> resultTypeClass, MPJBaseJoin<T> joinQueryWrapper) { | |
IPage<DTO> mpPage = MyBatisUtils.buildPage(pageParam); | |
selectJoinPage(mpPage, resultTypeClass, joinQueryWrapper); | |
// 转换返回 | |
return new PageResult<>(mpPage.getRecords(), mpPage.getTotal()); | |
} | |
default T selectOne(String field, Object value) { | |
return selectOne(new QueryWrapper<T>().eq(field, value)); | |
} | |
default T selectOne(SFunction<T, ?> field, Object value) { | |
return selectOne(new LambdaQueryWrapper<T>().eq(field, value)); | |
} | |
default T selectOne(String field1, Object value1, String field2, Object value2) { | |
return selectOne(new QueryWrapper<T>().eq(field1, value1).eq(field2, value2)); | |
} | |
default T selectOne(SFunction<T, ?> field1, Object value1, SFunction<T, ?> field2, Object value2) { | |
return selectOne(new LambdaQueryWrapper<T>().eq(field1, value1).eq(field2, value2)); | |
} | |
default T selectOne(SFunction<T, ?> field1, Object value1, SFunction<T, ?> field2, Object value2, | |
SFunction<T, ?> field3, Object value3) { | |
return selectOne(new LambdaQueryWrapper<T>().eq(field1, value1).eq(field2, value2) | |
.eq(field3, value3)); | |
} | |
default Long selectCount() { | |
return selectCount(new QueryWrapper<>()); | |
} | |
default Long selectCount(String field, Object value) { | |
return selectCount(new QueryWrapper<T>().eq(field, value)); | |
} | |
default Long selectCount(SFunction<T, ?> field, Object value) { | |
return selectCount(new LambdaQueryWrapper<T>().eq(field, value)); | |
} | |
default List<T> selectList() { | |
return selectList(new QueryWrapper<>()); | |
} | |
default List<T> selectList(String field, Object value) { | |
return selectList(new QueryWrapper<T>().eq(field, value)); | |
} | |
default List<T> selectList(SFunction<T, ?> field, Object value) { | |
return selectList(new LambdaQueryWrapper<T>().eq(field, value)); | |
} | |
default List<T> selectList(String field, Collection<?> values) { | |
if (CollUtil.isEmpty(values)) { | |
return CollUtil.newArrayList(); | |
} | |
return selectList(new QueryWrapper<T>().in(field, values)); | |
} | |
default List<T> selectList(SFunction<T, ?> field, Collection<?> values) { | |
if (CollUtil.isEmpty(values)) { | |
return CollUtil.newArrayList(); | |
} | |
return selectList(new LambdaQueryWrapper<T>().in(field, values)); | |
} | |
@Deprecated | |
default List<T> selectList(SFunction<T, ?> leField, SFunction<T, ?> geField, Object value) { | |
return selectList(new LambdaQueryWrapper<T>().le(leField, value).ge(geField, value)); | |
} | |
default List<T> selectList(SFunction<T, ?> field1, Object value1, SFunction<T, ?> field2, Object value2) { | |
return selectList(new LambdaQueryWrapper<T>().eq(field1, value1).eq(field2, value2)); | |
} | |
/** | |
* 批量插入,适合大量数据插入 | |
* | |
* @param entities 实体们 | |
*/ | |
default void insertBatch(Collection<T> entities) { | |
Db.saveBatch(entities); | |
} | |
/** | |
* 批量插入,适合大量数据插入 | |
* | |
* @param entities 实体们 | |
* @param size 插入数量 Db.saveBatch 默认为 1000 | |
*/ | |
default void insertBatch(Collection<T> entities, int size) { | |
Db.saveBatch(entities, size); | |
} | |
default void updateBatch(T update) { | |
update(update, new QueryWrapper<>()); | |
} | |
default void updateBatch(Collection<T> entities) { | |
Db.updateBatchById(entities); | |
} | |
default void updateBatch(Collection<T> entities, int size) { | |
Db.updateBatchById(entities, size); | |
} | |
default void insertOrUpdate(T entity) { | |
Db.saveOrUpdate(entity); | |
} | |
default void insertOrUpdateBatch(Collection<T> collection) { | |
Db.saveOrUpdateBatch(collection); | |
} | |
default int delete(String field, String value) { | |
return delete(new QueryWrapper<T>().eq(field, value)); | |
} | |
default int delete(SFunction<T, ?> field, Object value) { | |
return delete(new LambdaQueryWrapper<T>().eq(field, value)); | |
} | |
} |
# selectOne
#selectOne(...) 方法,使用指定条件,查询单条记录。示例如下:
default TenantDO selectByName(String name) { | |
return selectOne(TenantDO::getName, name); | |
} |
# selectCount
#selectCount(...) 方法,使用指定条件,查询记录的数量。示例如下:
default Long selectCountByGroupId(Long groupId) { | |
return selectCount(MemberUserDO::getGroupId, groupId); | |
} |
# selectList
#selectList(...) 方法,使用指定条件,查询多条记录。示例如下:
default List<MemberUserDO> selectListByNicknameLike(String nickname) { | |
return selectList(new LambdaQueryWrapperX<MemberUserDO>() | |
.likeIfPresent(MemberUserDO::getNickname, nickname)); | |
} |
# selectPage
针对 MyBatis Plus 分页查询的二次分装,在 BaseMapperX 中实现,目的是使用项目自己的分页封装:
【入参】查询前,将项目的分页参数
PageParam,转换成 MyBatis Plus 的 IPage 对象。package com.tz.scaffold.framework.common.pojo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import javax.validation.constraints.Min;
import javax.validation.constraints.Max;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
/*** <p> Project: scaffold - PageParam </p>
*
* 分页参数
* @author Tz
* @date 2024/01/09 23:45
* @version 1.0.0
* @since 1.0.0
*/
@Schema(description="分页参数")
@Datapublic class PageParam implements Serializable {
private static final Integer PAGE_NO = 1;
private static final Integer PAGE_SIZE = 10;
/*** 每页条数 - 不分页
*
* 例如说,导出接口,可以设置 {@link #pageSize} 为 -1 不分页,查询所有数据。*/
public static final Integer PAGE_SIZE_NONE = -1;
@Schema(description = "页码,从 1 开始", requiredMode = Schema.RequiredMode.REQUIRED,example = "1")
@NotNull(message = "页码不能为空")
@Min(value = 1, message = "页码最小值为 1")
private Integer pageNo = PAGE_NO;
@Schema(description = "每页条数,最大值为 100", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
@NotNull(message = "每页条数不能为空")
@Min(value = 1, message = "每页条数最小值为 1")
@Max(value = 100, message = "每页条数最大值为 100")
private Integer pageSize = PAGE_SIZE;
}【出参】查询后,将 MyBatis Plus 的分页结果 IPage,转换成项目的分页结果
PageResult。代码如下图:package com.tz.scaffold.framework.common.pojo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
/*** <p> Project: scaffold - PageResult </p>
*
* 分页结果
* @param <T> 数据泛型
* @author Tz
* @date 2024/01/09 23:45
* @version 1.0.0
* @since 1.0.0
*/
@Schema(description = "分页结果")
@Datapublic final class PageResult<T> implements Serializable {
@Schema(description = "数据", requiredMode = Schema.RequiredMode.REQUIRED)
private List<T> list;
@Schema(description = "总量", requiredMode = Schema.RequiredMode.REQUIRED)
private Long total;
public PageResult() {
}public PageResult(List<T> list, Long total) {
this.list = list;
this.total = total;
}public PageResult(Long total) {
this.list = new ArrayList<>();
this.total = total;
}public static <T> PageResult<T> empty() {
return new PageResult<>(0L);
}public static <T> PageResult<T> empty(Long total) {
return new PageResult<>(total);
}}
分页查询例子:
default PageResult<T> selectPage(PageParam pageParam, @Param("ew") Wrapper<T> queryWrapper) { | |
// MyBatis Plus 查询 | |
IPage<T> mpPage = MyBatisUtils.buildPage(pageParam); | |
selectPage(mpPage, queryWrapper); | |
// 转换返回 | |
return new PageResult<>(mpPage.getRecords(), mpPage.getTotal()); | |
} |
IPage<T> mpPage = MyBatisUtils.buildPage(pageParam);: 拼接 PageNo、pageSize 为查询条件。selectPage(mpPage, queryWrapper);: 执行 select 分页查询,以及 select count (*) 数量查询new PageResult<>(mpPage.getRecords(), mpPage.getTotal()): 转换分页的结果为 PageResult
使用:
default PageResult<TenantDO> selectPage(TenantPageReqVO reqVO) { | |
return selectPage(reqVO, new LambdaQueryWrapperX<TenantDO>() | |
.likeIfPresent(TenantDO::getName, reqVO.getName()) | |
.likeIfPresent(TenantDO::getContactName, reqVO.getContactName()) | |
.likeIfPresent(TenantDO::getContactMobile, reqVO.getContactMobile()) | |
.eqIfPresent(TenantDO::getStatus, reqVO.getStatus()) | |
.betweenIfPresent(TenantDO::getCreateTime, reqVO.getCreateTime()) | |
.orderByDesc(TenantDO::getId)); | |
} |
# insertBatch
#insertBatch(...) 方法,遍历数组,逐条插入数据库中,适合少量数据插入,或者对性能要求不高的场景。 示例如下:
public void copyTaskAssignRules(String fromModelId, String toProcessDefinitionId) { | |
List<BpmTaskAssignRuleRespVO> rules = getTaskAssignRuleList(fromModelId, null); | |
if (CollUtil.isEmpty(rules)) { | |
return; | |
} | |
// 开始复制 | |
List<BpmTaskAssignRuleDO> newRules = BpmTaskAssignRuleConvert.INSTANCE.convertList2(rules); | |
newRules.forEach(rule -> rule.setProcessDefinitionId(toProcessDefinitionId).setId(null).setCreateTime(null) | |
.setUpdateTime(null)); | |
taskRuleMapper.insertBatch(newRules); | |
} |
taskRuleMapper.insertBatch(newRules);批量插入数据。为什么不使用
insertBatchSomeColumn批量插入?
- 只支持 MySQL 数据库。其它 Oracle 等数据库使用会报错,可见
InsertBatchSomeColumn说明。未支持多租户。插入数据库时,多租户字段不会进行自动赋值。
如果需要其他数据库也支持,可以继承
InsertBatchSomeColumn类重写对应数据库的逻辑。
# 批量插入
绝大多数场景下,推荐使用 MyBatis Plus 提供的 IService 的 #saveBatch() 方法。示例 PermissionServiceImpl 如下:
public void assignRoleMenu(Long roleId, Set<Long> menuIds) { | |
// 获得角色拥有菜单编号 | |
Set<Long> dbMenuIds = convertSet(roleMenuMapper.selectListByRoleId(roleId), RoleMenuDO::getMenuId); | |
// 计算新增和删除的菜单编号 | |
Set<Long> menuIdList = CollUtil.emptyIfNull(menuIds); | |
Collection<Long> createMenuIds = CollUtil.subtract(menuIdList, dbMenuIds); | |
Collection<Long> deleteMenuIds = CollUtil.subtract(dbMenuIds, menuIdList); | |
// 执行新增和删除。对于已经授权的菜单,不用做任何处理 | |
if (CollUtil.isNotEmpty(createMenuIds)) { | |
roleMenuMapper.insertBatch(CollectionUtils.convertList(createMenuIds, menuId -> { | |
RoleMenuDO entity = new RoleMenuDO(); | |
entity.setRoleId(roleId); | |
entity.setMenuId(menuId); | |
return entity; | |
})); | |
} | |
if (CollUtil.isNotEmpty(deleteMenuIds)) { | |
roleMenuMapper.deleteListByRoleIdAndMenuIds(roleId, deleteMenuIds); | |
} | |
} |
# 条件构造器
继承 MyBatis Plus 的条件构造器,拓展了 LambdaQueryWrapperX 和 QueryWrapperX 类,主要是增加 xxxIfPresent 方法,用于判断值不存在的时候,不要拼接到条件中。例如说:
package com.tz.scaffold.framework.mybatis.core.query; | |
import java.util.Collection; | |
/** | |
* <p> Project: scaffold - LambdaQueryWrapperX </p> | |
* | |
* 拓展 MyBatis Plus QueryWrapper 类,主要增加如下功能: | |
* <p> | |
* 1. 拼接条件的方法,增加 xxxIfPresent 方法,用于判断值不存在的时候,不要拼接到条件中。 | |
* @author Tz | |
* @date 2024/01/09 23:45 | |
* @version 1.0.0 | |
* @since 1.0.0 | |
*/ | |
public class LambdaQueryWrapperX<T> extends LambdaQueryWrapper<T> { | |
public LambdaQueryWrapperX<T> likeIfPresent(SFunction<T, ?> column, String val) { | |
if (StringUtils.hasText(val)) { | |
return (LambdaQueryWrapperX<T>) super.like(column, val); | |
} | |
return this; | |
} | |
public LambdaQueryWrapperX<T> inIfPresent(SFunction<T, ?> column, Collection<?> values) { | |
if (ObjectUtil.isAllNotEmpty(values) && !ArrayUtil.isEmpty(values)) { | |
return (LambdaQueryWrapperX<T>) super.in(column, values); | |
} | |
return this; | |
} | |
public LambdaQueryWrapperX<T> inIfPresent(SFunction<T, ?> column, Object... values) { | |
if (ObjectUtil.isAllNotEmpty(values) && !ArrayUtil.isEmpty(values)) { | |
return (LambdaQueryWrapperX<T>) super.in(column, values); | |
} | |
return this; | |
} | |
public LambdaQueryWrapperX<T> eqIfPresent(SFunction<T, ?> column, Object val) { | |
if (ObjectUtil.isNotEmpty(val)) { | |
return (LambdaQueryWrapperX<T>) super.eq(column, val); | |
} | |
return this; | |
} | |
public LambdaQueryWrapperX<T> neIfPresent(SFunction<T, ?> column, Object val) { | |
if (ObjectUtil.isNotEmpty(val)) { | |
return (LambdaQueryWrapperX<T>) super.ne(column, val); | |
} | |
return this; | |
} | |
public LambdaQueryWrapperX<T> gtIfPresent(SFunction<T, ?> column, Object val) { | |
if (val != null) { | |
return (LambdaQueryWrapperX<T>) super.gt(column, val); | |
} | |
return this; | |
} | |
public LambdaQueryWrapperX<T> geIfPresent(SFunction<T, ?> column, Object val) { | |
if (val != null) { | |
return (LambdaQueryWrapperX<T>) super.ge(column, val); | |
} | |
return this; | |
} | |
public LambdaQueryWrapperX<T> ltIfPresent(SFunction<T, ?> column, Object val) { | |
if (val != null) { | |
return (LambdaQueryWrapperX<T>) super.lt(column, val); | |
} | |
return this; | |
} | |
public LambdaQueryWrapperX<T> leIfPresent(SFunction<T, ?> column, Object val) { | |
if (val != null) { | |
return (LambdaQueryWrapperX<T>) super.le(column, val); | |
} | |
return this; | |
} | |
public LambdaQueryWrapperX<T> betweenIfPresent(SFunction<T, ?> column, Object val1, Object val2) { | |
if (val1 != null && val2 != null) { | |
return (LambdaQueryWrapperX<T>) super.between(column, val1, val2); | |
} | |
if (val1 != null) { | |
return (LambdaQueryWrapperX<T>) ge(column, val1); | |
} | |
if (val2 != null) { | |
return (LambdaQueryWrapperX<T>) le(column, val2); | |
} | |
return this; | |
} | |
public LambdaQueryWrapperX<T> betweenIfPresent(SFunction<T, ?> column, Object[] values) { | |
Object val1 = ArrayUtils.get(values, 0); | |
Object val2 = ArrayUtils.get(values, 1); | |
return betweenIfPresent(column, val1, val2); | |
} | |
// ========== 重写父类方法,方便链式调用 ========== | |
@Override | |
public LambdaQueryWrapperX<T> eq(boolean condition, SFunction<T, ?> column, Object val) { | |
super.eq(condition, column, val); | |
return this; | |
} | |
@Override | |
public LambdaQueryWrapperX<T> eq(SFunction<T, ?> column, Object val) { | |
super.eq(column, val); | |
return this; | |
} | |
@Override | |
public LambdaQueryWrapperX<T> orderByDesc(SFunction<T, ?> column) { | |
super.orderByDesc(true, column); | |
return this; | |
} | |
@Override | |
public LambdaQueryWrapperX<T> last(String lastSql) { | |
super.last(lastSql); | |
return this; | |
} | |
@Override | |
public LambdaQueryWrapperX<T> in(SFunction<T, ?> column, Collection<?> coll) { | |
super.in(column, coll); | |
return this; | |
} | |
} |
具体的使用示例:
default PageResult<ConfigDO> selectPage(ConfigPageReqVO reqVO) { | |
return selectPage(reqVO, new LambdaQueryWrapperX<ConfigDO>() | |
.likeIfPresent(ConfigDO::getName, reqVO.getName()) | |
.likeIfPresent(ConfigDO::getConfigKey, reqVO.getKey()) | |
.eqIfPresent(ConfigDO::getType, reqVO.getType()) | |
.betweenIfPresent(ConfigDO::getCreateTime, reqVO.getCreateTime())); | |
} |
# 字段加密
EncryptTypeHandler ,基于 Hutool AES 实现字段的解密与解密。
例如说, 数据源配置 的 password 密码需要实现加密存储,则只需要在该字段上添加 EncryptTypeHandler 处理器。示例代码如下:
package com.tz.scaffold.framework.mybatis.core.type; | |
import cn.hutool.core.lang.Assert; | |
import cn.hutool.crypto.SecureUtil; | |
import cn.hutool.crypto.symmetric.AES; | |
import cn.hutool.extra.spring.SpringUtil; | |
import org.apache.ibatis.type.BaseTypeHandler; | |
import org.apache.ibatis.type.JdbcType; | |
import java.sql.CallableStatement; | |
import java.sql.PreparedStatement; | |
import java.sql.ResultSet; | |
import java.sql.SQLException; | |
/** | |
* <p> Project: scaffold - EncryptTypeHandler </p> | |
* | |
* 字段的 TypeHandler 实现类,基于 {@link cn.hutool.crypto.symmetric.AES} 实现 | |
* <p> | |
* 可通过 jasypt.encryptor.password 配置项,设置密钥 | |
* @author Tz | |
* @date 2024/01/09 23:45 | |
* @version 1.0.0 | |
* @since 1.0.0 | |
*/ | |
public class EncryptTypeHandler extends BaseTypeHandler<String> { | |
private static final String ENCRYPTOR_PROPERTY_NAME = "mybatis-plus.encryptor.password"; | |
private static AES aes; | |
@Override | |
public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException { | |
ps.setString(i, encrypt(parameter)); | |
} | |
@Override | |
public String getNullableResult(ResultSet rs, String columnName) throws SQLException { | |
String value = rs.getString(columnName); | |
return decrypt(value); | |
} | |
@Override | |
public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException { | |
String value = rs.getString(columnIndex); | |
return decrypt(value); | |
} | |
@Override | |
public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { | |
String value = cs.getString(columnIndex); | |
return decrypt(value); | |
} | |
private static String decrypt(String value) { | |
if (value == null) { | |
return null; | |
} | |
return getEncryptor().decryptStr(value); | |
} | |
public static String encrypt(String rawValue) { | |
if (rawValue == null) { | |
return null; | |
} | |
return getEncryptor().encryptBase64(rawValue); | |
} | |
private static AES getEncryptor() { | |
if (aes != null) { | |
return aes; | |
} | |
// 构建 AES | |
String password = SpringUtil.getProperty(ENCRYPTOR_PROPERTY_NAME); | |
Assert.notEmpty(password, "配置项({}) 不能为空", ENCRYPTOR_PROPERTY_NAME); | |
aes = SecureUtil.aes(password.getBytes()); | |
return aes; | |
} | |
} |
具体使用:
package com.tz.scaffold.module.infra.dal.dataobject.db; | |
/** | |
* <p> Project: scaffold - DataSourceConfigDO </p> | |
* | |
* 数据源配置 | |
* <p> | |
* KeySequence: 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 | |
* @author Tz | |
* @date 2024/01/09 23:45 | |
* @version 1.0.0 | |
* @since 1.0.0 | |
*/ | |
@TableName(value = "infra_data_source_config", autoResultMap = true) | |
@KeySequence("infra_data_source_config_seq") | |
@Data | |
public class DataSourceConfigDO extends BaseDO { | |
/** | |
* 主键编号 - Master 数据源 | |
*/ | |
public static final Long ID_MASTER = 0L; | |
/** | |
* 密码 | |
*/ | |
@TableField(typeHandler = EncryptTypeHandler.class) | |
private String password; | |
} |
@TableField(typeHandler = EncryptTypeHandler.class)
