1、前言
mybatis-plus在目前的互联网公司都比较常用,但是我发现大家使用起来没有什么规范,导致代码逻辑混乱,可复用性较差,在此我讲一下自己在项目中实践的经验。
2、编码实践
2.1、包结构路径
├─persistence 持久化相关逻辑
└─db 数据库持久化
├─dao 数据库操作接口,提供给上层业务逻辑使用
│ └─impl dao接口实现
├─entity 映射数据表结构的DO模型
├─mapper mybatis mapper接口,自定义sql对应的方法
├─query 数据库查询参数模型
└─vo 数据库关联查询结果
2.2、dao
2.2.1、dao层接口不要继承IService
dao接口是直接提供给上层业务逻辑使用的,有些人把dao层和应用层的service完全不分,他们直接使用mybatis-plus生成代码工具生成的service作为应用层的service,service层的接口直接继承IService
接口,里面既定义了纯数据库操作方法,又定义了业务逻辑方法,这导致代码逻辑非常混乱,这是非常糟糕的设计。大部分人对dao层的作用理解没错,但是很多人为了方便会继承IService
接口,我是不建议这样做的。dao层接口是对数据库操作的抽象,不应该对外暴露mybatis-plus的技术相关的代码的。对上层业务逻辑暴露了IService
产生的后果是,对单表操作大家越来越喜欢直接在业务逻辑中调用IService
中的方法,如getXXX、listXXX、updateXXX、removeXXX方法,dao接口只有寥寥无几的自定义查询方法。这导致了三个问题,1)很多业务逻辑写了不少功能重复的数据库操作逻辑,没办法很好地复用dao层的接口,2)IService带有Wrapper
参数的方法对业务操作的目的性表达不够清晰,看的人要看完Wrapper的完整参数才知道其实际意义,3)业务代码逻辑和mybatis-plus框架本身的代码强耦合,降低了代码的扩展性。
推荐的写法
商品数据库操作接口
public interface ProductDao {
/**
* 根据id查询
*
* @param id
* @return
*/
ProductDO getById(Long id);
/**
* 根据商品编码查询
*
* @param code 商品编码
* @return
*/
ProductDO getByCode(String code);
}
商品数据库接口实现
@Service
public class ProductDaoImpl extends ServiceImpl<ProductMapper, ProductDO> implements ProductDao {
@Override
public ProductDO getById(Long id) {
return super.getById(id);
}
@Override
public ProductDO getByCode(String code) {
return getOne(Wrappers.<ProductDO>lambdaQuery().eq(ProductDO::getCode, code));
}
}
业务逻辑直接调用productDao的getByCode方法,也比较简洁,而且可以在其他业务逻辑中复用。
ProductDO product = productDao.getByCode(code);
不推荐的写法
商品数据库操作接口
public interface ProductDao extends IService<ProductDO> {
}
商品数据库接口实现
@Service
public class ProductDaoImpl extends ServiceImpl<ProductMapper, ProductDO> implements ProductDao {
}
业务逻辑根据商品编码查询商品信息
ProductDO product = productDao.getOne(Wrappers.<ProductDO>lambdaQuery().eq(ProductDO::getCode, code));
这种写法有三个不好的地方:
1)这种写法业务意义表达能力较差,看方法名getOne不能理解其确切的含义;
2)很多需要用到同样查询功能的业务逻辑都要重写一次Wrapper查询条件;
3)mybatis-plus自身的IService接口暴露到了业务逻辑里面,如果以后不用mybatis-plus了,代码重构成本高。
2.2.2、批量查询优化
1、批量查询入参一般是List类型,业务逻辑很可能传null或者空List进来,判空逻辑不能少,否则就会报空指针异常或者sql查询in语法错误异常。我发现有些人喜欢在判空后抛出异常,提示入参不能为空。我觉得判空后没必要抛出异常,而是返回一个空集合Collections.emptyList()
。因为有些业务逻辑是有可能传空参数进来的,上层逻辑是可以继续往下走的,如果调用方没有直接捕获这个异常,那就中止了整个流程,如果调用方直接去捕获这个异常,那代码就变得异常丑陋。
2、批量查询入参的List的size可能非常大,如果没做什么限制的话,很容易报sql长度超长异常。这种情况可以判断一下List的大小,如果超过一定的大小直接抛出明确的异常,或者分割List的成多个更小的List,再多次查库。
代码示例
public List<ProductDO> listByCodes(List<String> codes) {
if (CollectionUtils.isEmpty(codes)) {
return Collections.emptyList();
}
if (codes.size() > 500) {
return Lists.partition(codes, 500).stream()
.map(it -> list(Wrappers.<ProductDO>lambdaQuery().in(ProductDO::getCode, it)))
.flatMap(Collection::stream).collect(Collectors.toList());
} else {
return list(Wrappers.<ProductDO>lambdaQuery().in(ProductDO::getCode, codes));
}
}
2.3、entity
对于数据表的设计,一般都会有一个约定的规范,也可以参数阿里巴巴编码规范。一般同一个系统的表都会按照规范设计一些业务无关的固有的字段,如id、创建时间、更新时间、创建人、更新人、乐观锁版本号、逻辑删除标识等,这些固有的字段可以提取成一个基类(如BaseDataObj
),再由各个表的DO类(如ProductDO
)去继承它,这样DO类看起来会更加简洁和清晰。
@Data
public class BaseDataObj {
/**
* 表主键
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 创建人
*/
private Long createUser;
/**
* 更新人
*/
private Long updateUser;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
/**
* 乐观锁版本号
*/
@Version
private Integer version;
/**
* 逻辑删除标识:0正常、null删除
*/
@TableLogic
private Integer delFlag;
}
@Data
@TableName("product")
public class ProductDO extends BaseDataObj {
/**
* 商品编码
*/
private String code;
/**
* 商品名称
*/
private String name;
/**
* 商品价格
*/
private BigDecimal price;
/**
* 0:待售,1:在售,2:下架
*/
private Integer status;
/**
* 商品分类id
*/
private Long categoryId;
/**
* 供应商id
*/
private Long supplierId;
}
2.4、mapper
mapper一般只定义自定义sql的操作方法,但是不建议直接在应用层、factory等业务逻辑里面直接引用mapper,而是应该在dao层重新定义与mapper定义一样的方法,业务逻辑代码仅通过dao层接口去操作数据库。理由有两个:1)mapper是比较底层的接口,他继承BaseMapper
接口,业务代码直接引用他会与mybatis-plus底层实现强耦合,降低了代码的可扩展性;2)业务代码里面既有通过dao接口去访问数据库,也有通过mapper去操作,通过多个不同抽象层次的入口去操作同一样东西,这样的代码不够优雅。
2.5、query
这个包定义数据库查询参数,类名以Query结尾(如ProductPageQuery
),作为dao和mapper的查询参数。看到有些人喜欢直接把api层的DTO请求参数作为dao层的参数,这样子dao层就耦合应用层某个具体接口的数据结构了,其他应用层逻辑要复用这个dao接口,这就造成了不同接口之间互相耦合,代码扩展性差。
2.6、vo
这个包定义数据库查询结果,一般是多表查询的结果,一般类名以VO结尾。同样也有人喜欢直接使用api层的DTO作为dao层的返回参数,其弊端与上述把DTO请求参数直接作为dao层入参一致。
2.7、逻辑删除与唯一索引
逻辑删除有两个配置方式
1、通过application.properties
配置文件或者配置中心全局配置
mybatis-plus.global-config.db-config.logic-not-delete-value=0
mybatis-plus.global-config.db-config.logic-delete-value=null
2、通过@TableLogic
指定某个表模型的逻辑删除标识
@TableLogic(value = "0",delval = "null")
需要主要的点是有唯一索引的情况,设计不好容易报唯一索引冲突异常。需要注意两点:
1)设计唯一索引的时候要把逻辑删除列设计成唯一索引的一部分,否则删除了的数据再想新增一条唯一索引一样的数据就会报唯一索引冲突异常;
2)第一点的情况已经考虑的情况下,某个唯一索引的数据也有可能被多次删除。如某个场景:用户删除了某条记录,然后又重新新增了一条和删除的那条记录唯一索引一样的记录,后面再删除这条新增的记录。如果逻辑删除标识值delval
是常数,这个场景就会报唯一索引冲突异常。解决方案有两个:
(1)把delval
设为now()
,当数据被删除时,逻辑删除标识设为当前时间戳,删除标识的数据库数据类型要定义为bigint
;
(2)把delval
设为null
,利用null
使唯一索引失效的特性。我比较喜欢用第2种方式,数据库类型定义为int
就可以,更节省空间。