在使用了MybatisPlus框架进行项目重构之后,关于如何更好的利用Mybatisplus。在此做一些总结供大家参考。 主要总结了以下这几个方面的实践。基础设计BaseEntity逻辑删除自动填充字段代码生成类查询操作Query基类(复用PageQuery)普通QueryLambdaQuery复杂多表查询报表型查询保存操作模型利用JPA保存批量保存数据扩展阻止全表操作动态数据源多租户基础设计BaseEntity 对于数据库中表中的公共字段我们可以抽取出来做成基类继承。避免表映射的数据库实体类字段太过繁杂。 例如常用的创建时间、创建者、更新时间、更新者、逻辑删除字段。Entity基类authorvalarchieEqualsAndHashCode(callSupertrue)DatapublicclassBaseEntityTextendsM?extendsModelT{ApiModelProperty(创建者ID)TableField(valuecreatorid,fillFieldFill。INSERT)privateLongcreatorId;ApiModelProperty(创建时间)TableField(valuecreatetime,fillFieldFill。INSERT)privateDatecreateTApiModelProperty(更新者ID)TableField(valueupdaterid,fillFieldFill。UPDATE,updateStrategyFieldStrategy。NOTNULL)privateLongupdaterId;ApiModelProperty(更新时间)TableField(valueupdatetime,fillFieldFill。UPDATE)privateDateupdateTdeleted字段请在数据库中设置为tinyInt并且非null默认值为0ApiModelProperty(删除标志(0代表存在1代表删除))TableField(deleted)TableLogicprivateB} 通过继承了基类,实体类看起来就简洁了许多。p通知公告表authorvalarchiesince20221002GetterSetterTableName(sysnotice)ApiModel(valueSysNoticeEntity对象,description通知公告表)publicclassSysNoticeEntityextendsBaseEntitySysNoticeEntity{privatestaticfinallongserialVersionUID1L;ApiModelProperty(公告ID)TableId(valuenoticeid,typeIdType。AUTO)privateIntegernoticeId;ApiModelProperty(公告标题)TableField(noticetitle)privateStringnoticeTApiModelProperty(公告类型(1通知2公告))TableField(noticetype)privateIntegernoticeTApiModelProperty(公告内容)TableField(noticecontent)privateStringnoticeCApiModelProperty(公告状态(1正常0关闭))TableField(status)privateIApiModelProperty(备注)TableField(remark)privateSOverridepublicSerializablepkVal(){returnthis。noticeId;}} 既然抽取出了公共字段,我们可以更进一步将这些公共字段进行自动填值处理。 MybatisPlus提供了字段自动填充的插件。自动填充字段MybatisPlus允许在插入或者更新的时候自定义设定值authorvalarchieComponentSlf4jpublicclassCustomMetaObjectHandlerimplementsMetaObjectHandler{publicstaticfinalStringCREATETIMEFIELDcreateTpublicstaticfinalStringCREATORIDFIELDcreatorId;publicstaticfinalStringUPDATETIMEFIELDupdateTpublicstaticfinalStringUPDATERIDFIELDupdaterId;OverridepublicvoidinsertFill(MetaObjectmetaObject){if(metaObject。hasSetter(CREATETIMEFIELD)){this。setFieldValByName(CREATETIMEFIELD,newDate(),metaObject);}if(metaObject。hasSetter(CREATORIDFIELD)){this。strictInsertFill(metaObject,CREATORIDFIELD,Long。class,getUserIdSafely());}}OverridepublicvoidupdateFill(MetaObjectmetaObject){if(metaObject。hasSetter(UPDATETIMEFIELD)){this。setFieldValByName(UPDATETIMEFIELD,newDate(),metaObject);}if(metaObject。hasSetter(UPDATERIDFIELD)){this。strictUpdateFill(metaObject,UPDATERIDFIELD,Long。class,getUserIdSafely());}}publicLonggetUserIdSafely(){LonguserItry{LoginUserloginUserAuthenticationUtils。getLoginUser();userIdloginUser。getUserId();}catch(Exceptione){log。info(cannotfinduserincurrentthread。);}returnuserId;}} 使用自定义填充值时,需要在生成实体的时候加上配置。FieldFill。INSERT和FieldFill。INSERTUPDATEprivatevoidentityConfig(StrategyConfig。Builderbuilder){Entity。BuilderentityBuilderbuilder。entityBuilder();entityBuilder。enableLombok()。addTableFills(newColumn(createtime,FieldFill。INSERT))。addTableFills(newColumn(creatorid,FieldFill。INSERT))。addTableFills(newProperty(updateTime,FieldFill。INSERTUPDATE))。addTableFills(newProperty(updaterId,FieldFill。INSERTUPDATE))IDstrategyAUTO,NONE,INPUT,ASSIGNID,ASSIGNUUID;。idType(IdType。AUTO)。formatFileName(sEntity);if(isExtendsFromBaseEntity){entityBuilder。superClass(BaseEntity。class)。addSuperEntityColumns(creatorid,createtime,creatorname,updaterid,updatetime,updatername,deleted);}entityBuilder。build();}逻辑删除 数据库一般不进行真实删除操作。但是如果让我们手工处理这些逻辑删除的话,也是非常麻烦。MybatisPlus有提供这样的插件。仅需要在EntityConfig中设置逻辑删除的字段是哪个即可。entityBuilderdeleted的字段设置成tinyint长度为1。logicDeleteColumnName(deleted)。formatFileName(sEntity);代码生成类 Mybatisplus支持生成entity,mapper,service,controller这四层类。但是笔者认为生成类的时候还是不要直接覆盖原本的类比较好。 我将生成的类,固定放在一个目录让使用者自己copy类到指定的目录。 以下是我自己封装的CodeGenerator的代码片段。 需要填入的字段主要是:作者名包名表名是否需要继承基类(因为不是所有表都需要继承基类)publicstaticvoidmain(String〔〕args){默认读取applicationdevyml中的master数据库配置JSONymlJsonJSONUtil。parse(newYaml()。load(ResourceUtil。getStream(applicationdev。yml)));CodeGeneratorgeneratorCodeGenerator。builder()。databaseUrl(JSONUtil。getByPath(ymlJson,URLPATH)。toString())。username(JSONUtil。getByPath(ymlJson,USERNAMEPATH)。toString())。password(JSONUtil。getByPath(ymlJson,PASSWORDPATH)。toString())。author(valarchie)生成的类放在orm子模块下的targetgeneratedcode目录底下。module(agilebootormtargetgeneratedcode)。parentPackage(com。agileboot)。tableName(sysconfig)决定是否继承基类。isExtendsFromBaseEntity(true)。build();generator。generateCode();}查询操作Query基类 系统内的查询大部分有共用的逻辑。比如时间范围的查询、排序。我们可以抽取这部分逻辑放在基类。然后把具体查询条件的构造,放到子类去实现。AbstractQueryauthorvalarchieDatapublicabstractclassAbstractQueryT{protectedStringorderByCprotectedStringisAJsonFormat(shapeShape。STRING,patternyyyyMMdd)privateDatebeginTJsonFormat(shapeShape。STRING,patternyyyyMMdd)privateDateendTprivatestaticfinalStringASCprivatestaticfinalStringDESC生成queryconditionsreturnpublicabstractQueryWrapperTtoQueryWrapper();publicvoidaddSortCondition(QueryWrapperTqueryWrapper){if(queryWrapper!null){booleansortDirectionconvertSortDirection();queryWrapper。orderBy(StrUtil。isNotBlank(orderByColumn),sortDirection,StrUtil。toUnderlineCase(orderByColumn));}}publicvoidaddTimeCondition(QueryWrapperTqueryWrapper,StringfieldName){if(queryWrapper!null){queryWrapper。ge(beginTime!null,fieldName,DatePickUtil。getBeginOfTheDay(beginTime))。le(endTime!null,fieldName,DatePickUtil。getEndOfTheDay(endTime));}}publicbooleanconvertSortDirection(){booleanorderDif(StrUtil。isNotEmpty(isAsc)){if(ASC。equals(isAsc)){orderD}elseif(DESC。equals(isAsc)){orderD}}returnorderD}}PageQuery 分页是非常常见的查询条件,我们可以基于AbstractQuery再做一层封装。authorvalarchieDatapublicabstractclassAbstractPageQueryTextendsAbstractQueryT{publicstaticfinalintMAXPAGENUM200;publicstaticfinalintMAXPAGESIZE500;Max(MAXPAGENUM)protectedIntegerpageNum1;Max(MAXPAGESIZE)protectedIntegerpageSize10;publicPageTtoPage(){returnnewPage(pageNum,pageSize);}}普通Query 比如我们有个菜单查询列表,我们可以新建一个MenuQuery继承AbstractQuery。然后实现toQueryWrapper方法去构造查询条件。authorvalarchieDatapublicclassMenuQueryextendsAbstractQuerySysMenuEntity{privateStringmenuNprivateBooleanisVprivateIOverridepublicQueryWrapperSysMenuEntitytoQueryWrapper(){QueryWrapperSysMenuEntityqueryWrappernewQueryWrapperSysMenuEntity()。like(StrUtil。isNotEmpty(menuName),menuname,menuName)。eq(isVisible!null,isvisible,isVisible)。eq(status!null,status,status);queryWrapper。orderBy(true,true,Arrays。asList(parentid,ordernum));returnqueryW}} 如果有另外一个不同的菜单查询列表,查询的参数一样,但是查询条件的构造不一样。我们可以新建一个DifferentMenuQuery类继承MenuQuery类,再覆写toQueryWrapper方法即可。LambdaQuery 如果在项目中的查询明确是单表操作的话,我们可以使用LambdaQuery来构造查询。LambdaQueryWrapperSysMenuEntitymenuQueryWrappers。lambdaQuery();menuQuery。select(SysMenuEntity::getMenuId);ListSysMenuEntitymenuListmenuService。list(menuQuery);复杂多表查询 MybatisPlus支持Select注解,遇到简单的多表join查询的话,我们可以直接在代码中写SQL语句。 以下是Mapper中的实现。{ew。customSqlSegment}会渲染出QueryWrapper类生成的查询条件。根据条件分页查询用户列表parampage页码对象paramqueryWrapper查询对象return用户信息集合信息Select(SELECTu。,d。deptname,d。leadernameFROMsysuseruLEFTJOINsysdeptdONu。deptidd。deptid{ew。customSqlSegment})PageSearchUserDOgetUserList(PageSearchUserDOpage,Param(Constants。WRAPPER)WrapperSearchUserDOqueryWrapper); Service层中的实现。OverridepublicPageSearchUserDOgetUserList(AbstractPageQuerySearchUserDOquery){returnbaseMapper。getUserList(query。toPage(),query。toQueryWrapper());}报表型查询 如果遇到复杂的报表型查询,利用Select注解的话,可能SQL看起来还是非常的复杂。此时推荐使用XML的形式。 保存操作模型利用JPA保存 MybatisPlus支持activeRecord特性,我们可以直接在Entity上执行saveupdatedelete等操作。框架会自动帮我们落库。activeRecord需要在EntityConfig配置。entityBuilderoperateentitylikeJPA。。enableActiveRecord()deleted的字段设置成tinyint长度为1IDstrategyAUTO,NONE,INPUT,ASSIGNID,ASSIGNUUID;。idType(IdType。AUTO)。formatFileName(sEntity); 因为Entity都是生成的,我们不方便将业务逻辑直接放在Entity中。这样会和数据库实体过于耦合。 推荐新建一个模型类继承XxxxEntity,然后将逻辑填充在模型类中。publicclassDeptModelextendsSysDeptEntity{privateISysDeptServicedeptSpublicDeptModel(ISysDeptServicedeptService){this。deptServicedeptS}publicDeptModel(SysDeptEntityentity,ISysDeptServicedeptService){if(entity!null){如果大数据量的话可以用MapStruct优化BeanUtil。copyProperties(entity,this);}this。deptServicedeptS}publicvoidloadAddCommand(AddDeptCommandaddCommand){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());}publicvoidcheckDeptNameUnique(){if(deptService。isDeptNameDuplicated(getDeptName(),getDeptId(),getParentId())){thrownewApiException(ErrorCode。Business。DEPTNAMEISNOTUNIQUE,getDeptName());}}publicvoidgenerateAncestors(){if(getParentId()0){setAncestors(getParentId()。toString());}SysDeptEntityparentDeptdeptService。getById(getParentId());if(parentDeptnullStatusEnum。DISABLE。equals(BasicEnumUtil。fromValue(StatusEnum。class,parentDept。getStatus()))){thrownewApiException(ErrorCode。Business。DEPTPARENTDEPTNOEXISTORDISABLED);}setAncestors(parentDept。getAncestors(),getParentId());}} 在应用层我们就可以直接调用模型类来完成逻辑操作。整个代码的语义性非常强。publicvoidaddDept(AddDeptCommandaddCommand){DeptModeldeptModeldeptModelFactory。create();deptModel。loadAddCommand(addCommand);deptModel。checkDeptNameUnique();deptModel。generateAncestors();deptModel。insert();}批量保存数据 以上是单条数据的落库操作,那么多条数据循环去insert的话,显然不是一个明智之举。 MybatisPlus提供了批量落库操作。privatebooleansaveMenus(){ListSysRoleMenuEntitylistnewArrayList();if(getMenuIds()!null){for(LongmenuId:getMenuIds()){SysRoleMenuEntityrmnewSysRoleMenuEntity();rm。setRoleId(getRoleId());rm。setMenuId(menuId);list。add(rm);}returnroleMenuService。saveBatch(list);}}按条件更新数据 JPA的方式有一个弊端就是需要先拿到数据实体类,才能调用save等操作。 还有一种情况,我们需要按照某些条件去更新数据,而不想先一条条获取数据再Save。 此时可以使用LambdaUpdate类。LambdaUpdateWrapperSysUserEntityupdateWrappernewLambdaUpdateWrapper();updateWrapper。set(SysUserEntity::getRoleId,null)。eq(SysUserEntity::getUserId,userId);userService。update(updateWrapper);扩展阻止全表操作 MybatisPlus提供了安全方面的插件,比如阻止全标更新删除的插件。仅需要声明MybatisPlusInterceptorBean,依次添加拦截插件即可。BeanpublicMybatisPlusInterceptormybatisPlusInterceptor(){MybatisPlusInterceptorinterceptornewMybatisPlusInterceptor();interceptor。addInnerInterceptor(newBlockAttackInnerInterceptor());}动态数据源 MybatisPlus提供DS注解去动态选择从库还是主库来执行SQL。DS(slave)PreAuthorize(permission。has(system:notice:list))GetMapping(listFromSlave)publicResponseDTOPageDTONoticeDTOlistFromSlave(NoticeQueryquery){PageDTONoticeDTOpageDTOnoticeApplicationService。getNoticeList(query);returnResponseDTO。ok(pageDTO);} 比如打上了DS(slave)的接口,就会去找slave这个从库进行操作。多租户BeanpublicMybatisPlusInterceptormybatisPlusInterceptor(){MybatisPlusInterceptorinterceptornewMybatisPlusInterceptor();interceptor。addInnerInterceptor(newTenantLineInnerInterceptor(newTenantLineHandler(){OverridepublicExpressiongetTenantId(){获取租户ID实际应该从用户信息中获取returnnewLongValue(1);}这是default方法,默认返回false表示所有表都需要拼多租户条件OverridepublicbooleanignoreTable(StringtableName){return!sysuser。equalsIgnoreCase(tableName);}}));如果用了分页插件注意先addTenantLineInnerInterceptor再addPaginationInnerInterceptor用了分页插件必须设置MybatisConfigurationuseDeprecatedExecutorfalseinterceptor。addInnerInterceptor(newPaginationInnerInterceptor());}