# 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-valuelogic-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 配置
  1. 所有 SELECT 查询,都会自动拼接 WHERE deleted = 0 查询条件,过滤已经删除的记录。如果被删除的记录,只能通过在 XML 或者 @SELECT 来手写 SQL 语句。例如说:

    @SELECT("select id from system_user ")
    List<UserDO> getAllUser();
  2. 建立唯一索引时,需要额外增加 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;
    
}
  1. 需要 开启 @TableName (value = "system_users", autoResultMap = true) autoResultMap = true
  2. @TableField (typeHandler = JsonLongSetTypeHandler.class) 转换的字段需要加: typeHandler = JsonLongSetTypeHandler.class 指定
  3. 自定义类型处理类需重写 AbstractJsonTypeHandler<Object> 类的逻辑
  4. 最后系统就可以实现,写入数据库自动转成 json 字符串, 读取时就转成了 Set<Long> 类型

常用的字段类型处理器有:

  • JacksonTypeHandler :通用的 Jackson 实现 JSON 字段类型处理器。
  • JsonLongSetTypeHandler :针对 Set<Long> 的 Jackson 实现 JSON 字段类型处理器。

另外,如果你后续要拓展自定义的 TypeHandler 实现,可以添加到 com.tz.scaffold.framework.mybatis.core.type 包下。

注意事项:

使用 TypeHandler 时,需要设置实体的 @TableName 注解的 @autoResultMap = true

# 编码规范

  1. 数据库实体类放在 dal.dataobject 包下,以 DO 结尾;数据库访问类放在 dal.mysql 包下,以 Mapper 结尾。如下图所示:

  2. 数据库实体类的注释要完整,特别是哪些字段是关联(外键)、枚举、冗余等等。例如说:

  1. 禁止在 Controller、Service 中,直接进行 MyBatis Plus 操作。原因是:大量 MyBatis 操作散落在 Service 中,会导致 Service 的代码越来乱,无法聚焦业务逻辑。并且,通过只允许将 MyBatis Plus 操作编写 Mapper 层,更好的实现 SELECT 查询的复用,而不是 Service 会存在很多相同且重复的 SELECT 查询的逻辑。

  2. 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
     */
    @Mapper
    public 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) 等等就是例子。

  3. 优先使用 LambdaQueryWrapper 条件构造器,使用方法获得字段名,避免手写 "字段" 可能写错的情况。例如说:

    default AdminUserDO selectByUsername(String username) {
        return selectOne(AdminUserDO::getUsername, username);
    }

    这里的 selectOne(AdminUserDO::getUsername, username) 写法就是正确的

  4. 简单的单表查询,优先在 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="分页参数")
    @Data
    public 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 = "分页结果")
    @Data
    public 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());
}
  1. IPage<T> mpPage = MyBatisUtils.buildPage(pageParam); : 拼接 PageNo、pageSize 为查询条件。
  2. selectPage(mpPage, queryWrapper); : 执行 select 分页查询,以及 select count (*) 数量查询
  3. 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 的条件构造器,拓展了 LambdaQueryWrapperXQueryWrapperX 类,主要是增加 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)