本文想讨论两个方面的内容,第一部分是 MyBatis Plus(以下简称MP)的最佳实践, 第二部分讨论了从面向对象的角度抽取出可复用的业务逻辑,主要包括逻辑删除的实现、通用CRUD的封装、自动填充字段等。
一. MP最佳实践总结
-
不要使用IService。IService 封装了数据访问层的逻辑,实际上就应该属于业务访问层, 而且使用IService打破了表示层、业务逻辑层、数据访问层三层模型,将数据访问层的逻辑转移到业务逻辑层, 导致我们的Service对象变得异常臃肿。
-
尽量使用lambda链式处理。使用函数式参数避免了字符串拼写的问题,不过由于我们并不使用IService,Query并不能完全链式表示,比如还要显示调用 selectOne(query) 等方法。
-
不要使用联表查询。虽然MP支持了一定程度的联表查询,但是终究支持有限,不利用后续优化。使用 xml 即可,mybatis 对于 1对多、多对多的支持就很好。
-
不要使用MP提供的逻辑删除。在实际业务中我们常常使用逻辑删除,即把对数据的删除改为标记删除位。MP的逻辑删除不支持唯一索引的使用,我们需要自己实现逻辑删除。 对于一条用户信息,email字段是唯一的,UNIQUE INDEX unique_email (
email) 这样设置索引是有问题的,因为我们可能有多条已经逻辑删除的邮箱。 解决方法是引入额外的字段保证唯一,常见的方法是引入uuid,对于并发量不大的业务也可以用时间戳, UNIQUE INDEX unique_email (email,delete_flag)。详见后续代码。 -
熟练使用条件构造器。官网提供了条件构造器的详细说明,熟练使用可以极大提高写代码的效率。
-
可以通过继承通用接口 BaseMapper 实现代码复用。也有人通过对于QueryMapper的封装实现复用;还可以通过组合对象的方式,由于Mapper对象由代理生成, 我们通过委托(Delegate) 实现对到具体的Mapper的调用。
二. 逻辑删除实现
以下为数据库表映射对象的实现:
@Getter
@Setter
@ToString(callSuper = true)
public class UserModel extends CommonModel<UserModel> {
private String email;
private String address;
}
// 此处仅为举例:继承Model可实现对象自主调用CRUD:ActiveRecord,因为会导致代码风格不一致,不建议使用
// 逻辑删除支持对象
@Data
public abstract class CommonModel<T extends CommonModel<T>> extends Model<T> implements Cloneable {
public static final int VALID = 1;
public static final int INVALID = 2;
/**
* 主键ID
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
// 创建时间
// 仅在第一次创建数据时手动添加,MP无需管理
@TableField(select = false, insertStrategy = FieldStrategy.NEVER, updateStrategy = FieldStrategy.NEVER)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
@JsonSerialize(using = LocalDateTimeSerializer.class)
private LocalDateTime createTime;
// 创建人
// 仅在第一次创建数据时数据库自动添加,MP无需管理
@TableField(updateStrategy = FieldStrategy.NEVER)
private Long createBy;
// 保留字段,方便拓展
// 此注解防止传空删除
@TableField(insertStrategy = FieldStrategy.NOT_EMPTY, updateStrategy = FieldStrategy.NOT_EMPTY)
private String remark;
// 逻辑删除字段 (1: valid 2: invalid)
private Integer valid;
// 更新时间
// 无需 MP/编程 管理,数据库设置为:on update CURRENT_TIMESTAMP
@TableField(insertStrategy = FieldStrategy.NEVER, updateStrategy = FieldStrategy.NEVER)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
@JsonSerialize(using = LocalDateTimeSerializer.class)
private LocalDateTime updateTime;
// 修改人
// 配置自动填充
@TableField(fill = FieldFill.INSERT_UPDATE)
private Long modifiedBy;
@TableField(fill = FieldFill.INSERT_UPDATE)
private String modifier;
// 删除时间(配合valid 实现唯一约束)
// 仅在逻辑删除时设置值
@TableField(insertStrategy = FieldStrategy.NEVER, updateStrategy = FieldStrategy.NOT_NULL)
private Long deleteTime;
// 序列化需要无参数构造器
public CommonModel() {
}
// copy constructor 支持
protected CommonModel(@NotNull CommonModelBuilder<T, ?, ?> b) {
this.id = b.id;
this.createTime = b.createTime;
this.createBy = b.createBy;
this.remark = b.remark;
this.valid = b.isValid;
this.updateTime = b.updateTime;
this.modifiedBy = b.modifiedBy;
this.deleteTime = b.deleteTime;
this.modifier = b.modifier;
}
// 根据 id 重写 equals 和 hashcode
@Override
public boolean equals(@Nullable Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
CommonModel<?> commonModel = (CommonModel<?>) o;
return Objects.equal(id, commonModel.id);
}
@Override
public int hashCode() {
return Objects.hashCode(id);
}
// 父类要求重写
@Override
public Serializable pkVal() {
return getId();
}
@Override
public String toString() {
return "CommonModel{" +
"id=" + id +
'}';
}
// 虽然 Effective Java 并不推荐使用 Cloneable
// 但是clone方法确实好用,而且提供了统一的拷贝方法
// 需要注意的是这里的实现是浅拷贝,表的大部分字段为不可变类型,一般不会出现对象B修改字段影响对象A的情况
@NotNull
@Override
@SuppressWarnings("unchecked")
@SneakyThrows
public final T clone() {
return (T) super.clone();
}
// 方便使用的拷贝方法
@NotNull
public final T withId(Long id) {
T copy = clone();
copy.setId(id);
return copy;
}
// 提示程序员物理删除已禁用
@Deprecated
@Override
public final boolean deleteById() {
throw new UnsupportedOperationException();
}
// 略去其他 @Deprecated方法
// 复杂对象的创建推荐使用 builder 模式
// SuperBuilder, 实现原理见我的另一篇文章:《深入理解Java泛型、协变逆变、泛型通配符、自限定》
public static abstract class CommonModelBuilder<T extends CommonModel<T>, C extends CommonModel<T>, B extends CommonModelBuilder<T, C, B>> {
private Long id;
private LocalDateTime createTime;
private Long createBy;
private String remark;
private Integer isValid;
private LocalDateTime updateTime;
private Long modifiedBy;
private Long deleteTime;
private String modifier;
public @NotNull
B id(Long id) {
this.id = id;
return self();
}
public @NotNull
B createTime(LocalDateTime createTime) {
this.createTime = createTime;
return self();
}
public @NotNull
B createBy(Long createBy) {
this.createBy = createBy;
return self();
}
public @NotNull
B remark(String remark) {
this.remark = remark;
return self();
}
public @NotNull
B isValid(Integer valid) {
this.Valid = valid;
return self();
}
public @NotNull
B updateTime(LocalDateTime updateTime) {
this.updateTime = updateTime;
return self();
}
public @NotNull
B modifiedBy(Long modifiedBy) {
this.modifiedBy = modifiedBy;
return self();
}
public @NotNull
B deleteTime(Long deleteTime) {
this.deleteTime = deleteTime;
return self();
}
public @NotNull
B modifier(String modifier) {
this.modifier = modifier;
return self();
}
protected abstract @NotNull
B self();
public abstract @NotNull
C build();
public @NotNull
String toString() {
return "CommonModel.CommonModelBuilder(" +
"super=" + super.toString() +
", id=" + this.id +
", createTime=" + this.createTime +
", createBy=" + this.createBy +
", remark=" + this.remark +
", isValid=" + this.isValid +
", updateTime=" + this.updateTime +
", modifiedBy=" + this.modifiedBy +
", deleteTime=" + this.deleteTime +
", modifier=" + this.modifier + ")";
}
}
}
以下为自动填充字段的实现:
@Component
public class OperatorMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
User user = User.getCache();
this.strictInsertFill(metaObject, "modifiedBy", Long.class, user.getUserId());
this.strictInsertFill(metaObject, "modifier", String.class, user.getEmail());
}
@Override
public void updateFill(MetaObject metaObject) {
User user = User.getCache();
this.strictUpdateFill(metaObject, "modifier", String.class, user.getEmail());
this.strictUpdateFill(metaObject, "modifiedBy", Long.class, user.getUserId());
}
}
通过继承BaseMapper接口可以自定义公用接口逻辑:
public enum FindAll {
// 使用枚举作为方法参数值
// 相比selectAll(true)有更好的可读性
ENABLED, DISABLED
}
// 继承BaseMapper,可以通过default方法实现代码复用
// 注意不要让 @MapperScan 扫描到此接口
public interface LogicDeleteSupportMapper<T> extends BaseMapper<T> {
// 由于我们支持了逻辑删除,原来的物理删除方法不使用了,但是父接口的方法不能删除
// 使用Deprecated提示用户不要使用这个方法(可参考ImmutableList)
@Deprecated
@Override
int deleteById(T entity);
@Deprecated
@Override
int deleteBatchIds(Collection<?> idList);
@Deprecated
@Override
int deleteByMap(Map<String, Object> columnMap);
@Deprecated
@Override
int deleteById(Serializable id);
@Deprecated
@Override
int delete(Wrapper<T> queryWrapper);
// Optional 的唯一最佳实践,返回值返回 Optional,不用担心NPE
default Optional<T> getValidById(Long id) {
T result = selectOne(getQueryWrapper()
.eq("valid", 1)
.eq("id", id));
return Optional.ofNullable(result);
}
default ImmutableSet<T> getValidListByIds(Collection<Long> ids) {
return getValidListByIds(ids, FindAll.DISABLED);
}
default ImmutableSet<T> getAllValid() {
return getValidListByIds(ImmutableSet.of(), FindAll.ENABLED);
}
// 入参为Collection, 出参为ImmutableSet
// 保证宽接口,出参无需保证顺序且有唯一性要求,所以用 Set
default ImmutableSet<T> getValidListByIds(Collection<Long> ids, FindAll findAllConfig) {
if (findAllConfig == FindAll.DISABLED && ids.isEmpty()) {
return ImmutableSet.of();
}
// 此处使用QueryWrapper仅作示例,推荐使用 LambdaWrapper
QueryWrapper<T> queryWrapper = getQueryWrapper()
.in(CollectionUtils.isNotEmpty(ids), "id", ids)
.eq("valid", 1);
List<T> queryResult = selectList(queryWrapper);
// 集合类型天然具有空值
return queryResult == null ? ImmutableSet.of() : ImmutableSet.copyOf(queryResult);
}
default int logicDelete(Long id) {
LambdaUpdateWrapper<T> wrapper = Wrappers.<T>lambdaUpdate()
.set(CommonModel::getValid, CommonModel.INVALID)
.set(CommonModel::getDeleteTime, System.currentTimeMillis())
.eq(CommonModel::getId, id)
.eq(CommonModel::getValid, CommonModel.VALID);
return update(null, wrapper);
}
// 每次 new QueryWrapper 需要传泛型参数,此方法方便调用
// 参考 Lists.newArrayList()
default QueryWrapper<T> getQueryWrapper() {
return new QueryWrapper<>();
}
}
// 子类通过继承,添加独立的实现
@Repository
public interface UserDao extends LogicDeleteSupportMapper<UserEntity> {
// 由xml实现
List<UserDetailDTO> getListByCondition(UserQueryRequest request);
default Option<UserEntity> getUserByEmail(String email) {
LambdaQueryWrapper<UserEntity> queryWrapper = new LambdaQueryWrapper<UserEntity>()
.eq(UserEntity::getEmail, email)
.eq(UserEntity::getIsValid, 1);
return Optional.ofNullable(selectOne(queryWrapper));
}
default UserEntity getCheckedUserByEmail(String email) {
return getUserByEmail(email).orElseThrow(() -> new BizException(ResultCode.USER_NOT_FOUND_FAIL));
}
}