3

Mybatis Plus 框架项目落地实践总结 - CoderV的进阶笔记

 1 year ago
source link: https://www.cnblogs.com/valarchie/p/17139145.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Mybatis Plus 框架项目落地实践总结

在使用了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的形式。

图片.png

模型利用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

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK