在使用了Mybatis Plus框架进行项目重构之后,关于如何更好的利用Mybatis plus。在此做一些总结供大家参考。

主要总结了以下这几个方面的实践。

  • 基础设计

    • BaseEntity
    • 逻辑删除
    • 自动填充字段
  • 代码生成类
  • 查询操作
    • Query基类(复用+PageQuery)
    • 普通Query
    • Lambda Query
    • 复杂多表查询
    • 报表型查询
  • 保存操作
    • 模型利用JPA保存
    • 批量保存数据
  • 扩展
    • 阻止全表操作
    • 动态数据源
    • 多租户

详细代码实现在开源项目Agileboot中:https://github.com/valarchie/AgileBoot-Back-End

关于Mybatis Plus的实践,如有不足或者建议欢迎大家评论指正。

废话不多说进入正题。

基础设计

BaseEntity

对于数据库中表中的公共字段我们可以抽取出来做成基类继承。避免表映射的数据库实体类字段太过繁杂。

例如常用的创建时间、创建者、更新时间、更新者、逻辑删除字段。

/**
* Entity基类
*
* @author valarchie
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class BaseEntity<T extends Model<?>> extends Model<T> { @ApiModelProperty("创建者ID")
@TableField(value = "creator_id", fill = FieldFill.INSERT)
private Long creatorId; @ApiModelProperty("创建时间")
@TableField(value = "create_time", fill = FieldFill.INSERT)
private Date createTime; @ApiModelProperty("更新者ID")
@TableField(value = "updater_id", fill = FieldFill.UPDATE, updateStrategy = FieldStrategy.NOT_NULL)
private Long updaterId; @ApiModelProperty("更新时间")
@TableField(value = "update_time", fill = FieldFill.UPDATE)
private Date updateTime; /**
* deleted字段请在数据库中 设置为tinyInt 并且非null 默认值为0
*/
@ApiModelProperty("删除标志(0代表存在 1代表删除)")
@TableField("deleted")
@TableLogic
private Boolean deleted; }

通过继承了基类,实体类看起来就简洁了许多。

/**
* <p>
* 通知公告表
* </p>
*
* @author valarchie
* @since 2022-10-02
*/
@Getter
@Setter
@TableName("sys_notice")
@ApiModel(value = "SysNoticeEntity对象", description = "通知公告表")
public class SysNoticeEntity extends BaseEntity<SysNoticeEntity> { private static final long serialVersionUID = 1L; @ApiModelProperty("公告ID")
@TableId(value = "notice_id", type = IdType.AUTO)
private Integer noticeId; @ApiModelProperty("公告标题")
@TableField("notice_title")
private String noticeTitle; @ApiModelProperty("公告类型(1通知 2公告)")
@TableField("notice_type")
private Integer noticeType; @ApiModelProperty("公告内容")
@TableField("notice_content")
private String noticeContent; @ApiModelProperty("公告状态(1正常 0关闭)")
@TableField("`status`")
private Integer status; @ApiModelProperty("备注")
@TableField("remark")
private String remark; @Override
public Serializable pkVal() {
return this.noticeId;
} }

既然抽取出了公共字段,我们可以更进一步将这些公共字段进行自动填值处理。

Mybatis Plus提供了字段自动填充的插件。

自动填充字段

/**
* Mybatis Plus允许在插入或者更新的时候
* 自定义设定值
* @author valarchie
*/
@Component
@Slf4j
public class CustomMetaObjectHandler implements MetaObjectHandler { public static final String CREATE_TIME_FIELD = "createTime";
public static final String CREATOR_ID_FIELD = "creatorId"; public static final String UPDATE_TIME_FIELD = "updateTime";
public static final String UPDATER_ID_FIELD = "updaterId"; @Override
public void insertFill(MetaObject metaObject) {
if (metaObject.hasSetter(CREATE_TIME_FIELD)) {
this.setFieldValByName(CREATE_TIME_FIELD, new Date(), metaObject);
} if (metaObject.hasSetter(CREATOR_ID_FIELD)) {
this.strictInsertFill(metaObject, CREATOR_ID_FIELD, Long.class, getUserIdSafely());
}
} @Override
public void updateFill(MetaObject metaObject) {
if (metaObject.hasSetter(UPDATE_TIME_FIELD)) {
this.setFieldValByName(UPDATE_TIME_FIELD, new Date(), metaObject);
} if (metaObject.hasSetter(UPDATER_ID_FIELD)) {
this.strictUpdateFill(metaObject, UPDATER_ID_FIELD, Long.class, getUserIdSafely());
}
} public Long getUserIdSafely() {
Long userId = null;
try {
LoginUser loginUser = AuthenticationUtils.getLoginUser();
userId = loginUser.getUserId();
} catch (Exception e) {
log.info("can not find user in current thread.");
}
return userId;
} }

使用自定义填充值时,需要在生成实体的时候加上配置。FieldFill.INSERTFieldFill.INSERT_UPDATE

private void entityConfig(StrategyConfig.Builder builder) {
Entity.Builder entityBuilder = builder.entityBuilder(); entityBuilder
.enableLombok()
.addTableFills(new Column("create_time", FieldFill.INSERT))
.addTableFills(new Column("creator_id", FieldFill.INSERT))
.addTableFills(new Property("updateTime", FieldFill.INSERT_UPDATE))
.addTableFills(new Property("updaterId", FieldFill.INSERT_UPDATE))
// ID strategy AUTO, NONE, INPUT, ASSIGN_ID, ASSIGN_UUID;
.idType(IdType.AUTO)
.formatFileName("%sEntity"); if (isExtendsFromBaseEntity) {
entityBuilder
.superClass(BaseEntity.class)
.addSuperEntityColumns("creator_id", "create_time", "creator_name", "updater_id", "update_time",
"updater_name", "deleted");
} entityBuilder.build();
}

逻辑删除

数据库一般不进行真实删除操作。但是如果让我们手工处理这些逻辑删除的话,也是非常麻烦。Mybatis Plus有提供这样的插件。仅需要在EntityConfig中设置逻辑删除的字段是哪个即可。


entityBuilder
// deleted的字段设置成tinyint 长度为1
.logicDeleteColumnName("deleted")
.formatFileName("%sEntity");

代码生成类

Mybatis plus支持生成entity,mapper,service,controller这四层类。

但是笔者认为生成类的时候还是不要直接覆盖原本的类比较好。

我将生成的类,固定放在一个目录让使用者自己copy类到指定的目录。

以下是我自己封装的CodeGenerator的代码片段。

需要填入的字段主要是:

  • 作者名
  • 包名
  • 表名
  • 是否需要继承基类(因为不是所有表都需要继承基类)
public static void main(String[] args) {
// 默认读取application-dev yml中的master数据库配置
JSON ymlJson = JSONUtil.parse(new Yaml().load(ResourceUtil.getStream("application-dev.yml"))); CodeGenerator generator = CodeGenerator.builder()
.databaseUrl(JSONUtil.getByPath(ymlJson, URL_PATH).toString())
.username(JSONUtil.getByPath(ymlJson, USERNAME_PATH).toString())
.password(JSONUtil.getByPath(ymlJson, PASSWORD_PATH).toString())
.author("valarchie")
//生成的类 放在orm子模块下的/target/generated-code目录底下
.module("/agileboot-orm/target/generated-code")
.parentPackage("com.agileboot")
.tableName("sys_config")
// 决定是否继承基类
.isExtendsFromBaseEntity(true)
.build(); generator.generateCode();
}

查询操作

Query基类

系统内的查询大部分有共用的逻辑。比如时间范围的查询、排序。我们可以抽取这部分逻辑放在基类。

然后把具体查询条件的构造,放到子类去实现。

AbstractQuery

/**
* @author valarchie
*/
@Data
public abstract class AbstractQuery<T> { protected String orderByColumn; protected String isAsc; @JsonFormat(shape = Shape.STRING, pattern = "yyyy-MM-dd")
private Date beginTime; @JsonFormat(shape = Shape.STRING, pattern = "yyyy-MM-dd")
private Date endTime; private static final String ASC = "ascending";
private static final String DESC = "descending"; /**
* 生成query conditions
* @return
*/
public abstract QueryWrapper<T> toQueryWrapper(); public void addSortCondition(QueryWrapper<T> queryWrapper) {
if(queryWrapper != null) {
boolean sortDirection = convertSortDirection();
queryWrapper.orderBy(StrUtil.isNotBlank(orderByColumn), sortDirection,
StrUtil.toUnderlineCase(orderByColumn));
}
} public void addTimeCondition(QueryWrapper<T> queryWrapper, String fieldName) {
if (queryWrapper != null) {
queryWrapper
.ge(beginTime != null, fieldName, DatePickUtil.getBeginOfTheDay(beginTime))
.le(endTime != null, fieldName, DatePickUtil.getEndOfTheDay(endTime));
}
} public boolean convertSortDirection() {
boolean orderDirection = true;
if (StrUtil.isNotEmpty(isAsc)) {
if (ASC.equals(isAsc)) {
orderDirection = true;
} else if (DESC.equals(isAsc)) {
orderDirection = false;
}
}
return orderDirection;
} }

PageQuery

分页是非常常见的查询条件,我们可以基于AbstractQuery再做一层封装。

/**
* @author valarchie
*/
@Data
public abstract class AbstractPageQuery<T> extends AbstractQuery<T> { public static final int MAX_PAGE_NUM = 200;
public static final int MAX_PAGE_SIZE = 500; @Max(MAX_PAGE_NUM)
protected Integer pageNum = 1;
@Max(MAX_PAGE_SIZE)
protected Integer pageSize = 10; public Page<T> toPage() {
return new Page<>(pageNum, pageSize);
} }

普通Query

比如我们有个菜单查询列表,我们可以新建一个MenuQuery继承AbstractQuery。然后实现

toQueryWrapper方法去构造查询条件。

/**
* @author valarchie
*/
@Data
public class MenuQuery extends AbstractQuery<SysMenuEntity> { private String menuName;
private Boolean isVisible;
private Integer status; @Override
public QueryWrapper<SysMenuEntity> toQueryWrapper() {
QueryWrapper<SysMenuEntity> queryWrapper = new QueryWrapper<SysMenuEntity>()
.like(StrUtil.isNotEmpty(menuName), "menu_name", menuName)
.eq(isVisible != null, "is_visible", isVisible)
.eq(status != null, "status", status); queryWrapper.orderBy(true, true, Arrays.asList("parent_id", "order_num"));
return queryWrapper;
}
}

如果有另外一个不同的菜单查询列表,查询的参数一样,但是查询条件的构造不一样。我们可以新建一个DifferentMenuQuery类继承MenuQuery类,再覆写toQueryWrapper方法即可。

Lambda Query

如果在项目中的查询明确是单表操作的话,我们可以使用LambdaQuery来构造查询。

LambdaQueryWrapper<SysMenuEntity> menuQuery = Wrappers.lambdaQuery();
menuQuery.select(SysMenuEntity::getMenuId);
List<SysMenuEntity> menuList = menuService.list(menuQuery);

复杂多表查询

Mybatis Plus支持@Select注解,遇到简单的多表join查询的话,我们可以直接在代码中写SQL语句。

以下是Mapper中的实现。${ew.customSqlSegment} 会渲染出QueryWrapper类生成的查询条件。

/**
* 根据条件分页查询用户列表
* @param page 页码对象
* @param queryWrapper 查询对象
* @return 用户信息集合信息
*/
@Select("SELECT u.*, d.dept_name, d.leader_name "
+ "FROM sys_user u "
+ " LEFT JOIN sys_dept d ON u.dept_id = d.dept_id "
+ "${ew.customSqlSegment}")
Page<SearchUserDO> getUserList(Page<SearchUserDO> page,
@Param(Constants.WRAPPER) Wrapper<SearchUserDO> queryWrapper);

Service层中的实现。

@Override
public Page<SearchUserDO> getUserList(AbstractPageQuery<SearchUserDO> query) {
return baseMapper.getUserList(query.toPage(), query.toQueryWrapper());
}

报表型查询

如果遇到复杂的报表型查询,利用@Select注解的话,可能SQL看起来还是非常的复杂。此时推荐使用XML的形式。

保存操作

模型利用JPA保存

Mybatis Plus支持activeRecord特性,我们可以直接在Entity上执行save\update\delete等操作。框架会自动帮我们落库。

activeRecord需要在EntityConfig配置。

        entityBuilder
// operate entity like JPA.
.enableActiveRecord()
// deleted的字段设置成tinyint 长度为1
// ID strategy AUTO, NONE, INPUT, ASSIGN_ID, ASSIGN_UUID;
.idType(IdType.AUTO)
.formatFileName("%sEntity");

因为Entity都是生成的,我们不方便将业务逻辑直接放在Entity中。这样会和数据库实体过于耦合。

推荐新建一个模型类继承XxxxEntity,然后将逻辑填充在模型类中。

public class DeptModel extends SysDeptEntity {

    private ISysDeptService deptService;

    public DeptModel(ISysDeptService deptService) {
this.deptService = deptService;
} public DeptModel(SysDeptEntity entity, ISysDeptService deptService) {
if (entity != null) {
// 如果大数据量的话 可以用MapStruct优化
BeanUtil.copyProperties(entity, this);
}
this.deptService = deptService;
} public void loadAddCommand(AddDeptCommand addCommand) {
this.setParentId(addCommand.getParentId());
this.setAncestors(addCommand.getAncestors());
this.setDeptName(addCommand.getDeptName());
this.setOrderNum(addCommand.getOrderNum());
this.setLeaderName(addCommand.getLeaderName());
this.setPhone(addCommand.getPhone());
this.setEmail(addCommand.getEmail());
} public void checkDeptNameUnique() {
if (deptService.isDeptNameDuplicated(getDeptName(), getDeptId(), getParentId())) {
throw new ApiException(ErrorCode.Business.DEPT_NAME_IS_NOT_UNIQUE, getDeptName());
}
} public void generateAncestors() {
if (getParentId() == 0) {
setAncestors(getParentId().toString());
return;
} SysDeptEntity parentDept = deptService.getById(getParentId()); if (parentDept == null || StatusEnum.DISABLE.equals(
BasicEnumUtil.fromValue(StatusEnum.class, parentDept.getStatus()))) {
throw new ApiException(ErrorCode.Business.DEPT_PARENT_DEPT_NO_EXIST_OR_DISABLED);
} setAncestors(parentDept.getAncestors() + "," + getParentId());
} }

在应用层我们就可以直接调用模型类来完成逻辑操作。整个代码的语义性非常强。

public void addDept(AddDeptCommand addCommand) {
DeptModel deptModel = deptModelFactory.create();
deptModel.loadAddCommand(addCommand); deptModel.checkDeptNameUnique();
deptModel.generateAncestors(); deptModel.insert();
}

批量保存数据

以上是单条数据的落库操作,那么多条数据循环去insert的话,显然不是一个明智之举。

Mybatis Plus提供了批量落库操作。

private boolean saveMenus() {
List<SysRoleMenuEntity> list = new ArrayList<>();
if (getMenuIds() != null) {
for (Long menuId : getMenuIds()) {
SysRoleMenuEntity rm = new SysRoleMenuEntity();
rm.setRoleId(getRoleId());
rm.setMenuId(menuId);
list.add(rm);
}
return roleMenuService.saveBatch(list);
}
return false;
}

按条件更新数据

JPA的方式有一个弊端就是需要先拿到数据实体类,才能调用save等操作。

还有一种情况,我们需要按照某些条件去更新数据,而不想先一条条获取数据再Save。

此时可以使用LambdaUpdate类。

LambdaUpdateWrapper<SysUserEntity> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.set(SysUserEntity::getRoleId, null).eq(SysUserEntity::getUserId, userId); userService.update(updateWrapper);

扩展

阻止全表操作

Mybatis Plus提供了安全方面的插件,比如阻止全标更新删除的插件。仅需要声明MybatisPlusInterceptor Bean,依次添加拦截插件即可。

@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
return interceptor;
}

动态数据源

Mybatis Plus提供@DS注解去动态选择从库还是主库来执行SQL.

@DS("slave")
@PreAuthorize("@permission.has('system:notice:list')")
@GetMapping("/listFromSlave")
public ResponseDTO<PageDTO<NoticeDTO>> listFromSlave(NoticeQuery query) {
PageDTO<NoticeDTO> pageDTO = noticeApplicationService.getNoticeList(query);
return ResponseDTO.ok(pageDTO);
}

比如打上了@DS("slave")的接口,就会去找slave这个从库进行操作。

多租户

    @Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() {
@Override
public Expression getTenantId() {
// 获取租户ID 实际应该从用户信息中获取
return new LongValue(1);
}
// 这是 default 方法,默认返回 false 表示所有表都需要拼多租户条件
@Override
public boolean ignoreTable(String tableName) {
return !"sys_user".equalsIgnoreCase(tableName);
}
}));
// 如果用了分页插件注意先 add TenantLineInnerInterceptor 再 add PaginationInnerInterceptor
// 用了分页插件必须设置 MybatisConfiguration#useDeprecatedExecutor = false
// interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return interceptor;
}
欢迎加入全栈技术交流群:1398880

最新文章

  1. 关于 REST
  2. 2014-2015 ACM-ICPC, NEERC, Moscow Subregional Contest D. Do it Right!
  3. 好玩的Prim算法
  4. Eclipse导入Maven项目时class not found
  5. Windows7下32位IE异常不能打开解决方法
  6. MongoDB GUI管理工具Mongo VUE
  7. 驱动07.USB驱动程序
  8. Jenkins集成源码静态分析工具
  9. python列表的操作
  10. 你的第一个Django程序
  11. Android 基本控件相关知识整理
  12. java.lang.NoClassDefFoundError: javax/servlet/AsyncListener解决方案
  13. ZCRM_AU_MAIL
  14. Spring MVC 自动为对象注入枚举类型
  15. 【亲测有效】运行docker ps 出现Got permission denied问题的解决方案
  16. 提升HTML5的性能体验系列之二 列表流畅滑动
  17. Le Chapitre VIII
  18. 常用的apache commons工具,直接使用,便于快速开发
  19. http://s22.app1105796624.qqopenapp.com/
  20. 【转】怎么用PHP发送HTTP请求(POST请求、GET请求)?

热门文章

  1. 兼容IE全版本及所有市面浏览器的网页变黑白处理方式
  2. linux基础第二部分
  3. java逻辑运算中 | | 和 | 的区别
  4. react 高效高质量搭建后台系统 系列 —— 脚手架搭建
  5. JavaScript:七大基础数据类型:布尔值boolean、空null、未定义undefined
  6. MongoDB从入门到实战之Docker快速安装MongoDB
  7. 分享项目中在用的asp.net下载业务的服务端基类(支持客户端显示下载百分比进度,支持并发数控制,支持限速)
  8. day01-ES6新特性
  9. python之路36 MySQL查询关键字
  10. 《Effective C++》资源管理章节