前言 该说的在《SpringBoot集成Mybatis项目实操》一文中都讲了,本文只是将Mybatis换成了SpringDataJPA,带大家将整个项目跑起来。 本文将实现SpringBootSpringDataJPA的项目搭建,项目特色是针对JPA专门写了一套动态链式查询工具类,进而实现一对多查询和连表查询。 不说废话了,我们直接进入主题。数据库 本项目采用的是MySQL数据库,版本为8。x,建表语句如下:CREATETABLEcustomer(idint(11)NOTNULLAUTOINCREMENT,namevarchar(20)DEFAULTNULL,ageint(11)DEFAULTNULL,addressvarchar(100)DEFAULTNULL,createddatetimestampNULLDEFAULTNULL,lastmodifieddatetimestampNULLDEFAULTNULL,delflagint(2)NOTNULLDEFAULT0,createusernamevarchar(50)DEFAULTNULL,lastmodifiednamevarchar(50)DEFAULTNULL,versionint(11)NOTNULLDEFAULT0,PRIMARYKEY(id),UNIQUEKEYukname(name))ENGINEInnoDBDEFAULTCHARSETutf8COMMENT父用户;CREATETABLEsubUser(idint(11)NOTNULLAUTOINCREMENT,namevarchar(20)DEFAULTNULL,customeridvarchar(36)NOTNULL,addressvarchar(100)DEFAULTNULL,createddatetimestampNULLDEFAULTNULL,lastmodifieddatetimestampNULLDEFAULTNULL,delflagint(2)NOTNULLDEFAULT0,createusernamevarchar(50)DEFAULTNULL,lastmodifiednamevarchar(50)DEFAULTNULL,versionint(11)NOTNULLDEFAULT0,PRIMARYKEY(id))ENGINEInnoDBDEFAULTCHARSETutf8COMMENT子用户;复制代码搭建SpringBoot项目 使用IDEA新建一个Maven项目,叫做jpaspringboot。 一些共用的基础代码可以参考上篇文章,这里不做重复介绍,会介绍一些JPA相关的代码。引入依赖parentgroupIdorg。springframework。bootgroupIdspringbootstarterparentartifactIdversion2。6。3versionparentpropertiesjava。version1。8java。versionfastjson。version1。2。73fastjson。versionhutool。version5。5。1hutool。versionmysql。version8。0。19mysql。versionmybatis。version2。1。4mybatis。versionmapper。version4。1。5mapper。versionorg。mapstruct。version1。4。2。Finalorg。mapstruct。versionorg。projectlombok。version1。18。20org。projectlombok。versionpropertiesdependenciesdependencygroupIdorg。springframework。bootgroupIdspringbootstarterwebartifactIddependencydependencygroupIdorg。springframework。bootgroupIdspringbootstarteraopartifactIddependencydependencygroupIdorg。springframework。bootgroupIdspringbootstartersecurityartifactIddependencydependencygroupIdorg。springframework。bootgroupIdspringbootstartervalidationartifactIddependencydependencygroupIdcom。alibabagroupIdfastjsonartifactIdversion{fastjson。version}versiondependencydependencygroupIdcn。hutoolgroupIdhutoolallartifactIdversion{hutool。version}versiondependencydependencygroupIdorg。projectlombokgroupIdlombokartifactIdversion{org。projectlombok。version}versionoptionaltrueoptionaldependencydependencygroupIdorg。springframework。bootgroupIdspringbootstartertestartifactIdscopetestscopedependencydependencygroupIdmysqlgroupIdmysqlconnectorjavaartifactIdversion{mysql。version}versionscoperuntimescopedependencydependencygroupIdorg。mybatis。spring。bootgroupIdmybatisspringbootstarterartifactIdversion{mybatis。version}versiondependencydependencygroupIdorg。springframework。bootgroupIdspringbootstarterdatajpaartifactIddependencydependencygroupIdorg。springdocgroupIdspringdocopenapiuiartifactIdversion1。6。9versiondependencydependencygroupIdcom。alibabagroupIddruidspringbootstarterartifactIdversion1。1。18versiondependencydependencygroupIdorg。mapstructgroupIdmapstructartifactIdversion{org。mapstruct。version}versiondependencydependencygroupIdorg。mapstructgroupIdmapstructprocessorartifactIdversion{org。mapstruct。version}versiondependencydependenciesbuildpluginsplugingroupIdorg。springframework。bootgroupIdspringbootmavenpluginartifactIdpluginpluginsbuild复制代码 有些依赖不一定是最新版本,而且你看到这篇文章时,可能已经发布了新版本,到时候可以先模仿着将项目跑起来后,再根据自己的需求来升级各项依赖,有问题咱再解决问题。分页处理 某些业务场景是需要分页查询和排序功能的,所以我们需要考虑前端如何传递参数给后端,后端如何进行分页查询或者是排序查询。JPA分页查询使用的是Spring自带的Pageable。 分页基础类publicclassSimplePageInfo{privateIntegerpageNum1;privateIntegerpageSize10;publicIntegergetPageNum(){returnpageNum;}publicvoidsetPageNum(IntegerpageNum){this。pageNumpageNum;}publicIntegergetPageSize(){returnpageSize;}publicvoidsetPageSize(IntegerpageSize){this。pageSizepageSize;}}复制代码 排序包装类GetterSetterpublicclassOrderInfo{privatebooleanasctrue;privateStringcolumn;}复制代码 分页且排序包装类GetterSetterEqualsAndHashCode(callSupertrue)publicclassPageSortInfoextendsSimplePageInfo{Schema(name排序信息)privateListOrderInfoorderInfos;publicStringparseSort(){if(CollectionUtils。isEmpty(orderInfos)){returnnull;}StringBuildersbnewStringBuilder();for(OrderInfoorderInfo:orderInfos){sb。append(orderInfo。getColumn())。append();sb。append(orderInfo。isAsc()?ASC,:DESC,);}sb。deleteCharAt(sb。length()1);returnsb。toString();}}复制代码 前端分页查询的请求体对象GetterSetterAllArgsConstructorNoArgsConstructorBuilderpublicclassCustomerQueryPageDTO{JsonUnwrappedprivatePageSortInfopageSortInfo;}复制代码 服务层分页查询PageablepageableSpecificationBuilder。getPageable(dto。getPageSortInfo());PageCustomercustomerPagecustomerRepository。findAll(pageable);复制代码 关于PageHelper的使用这里就不多做介绍了。 我们得到的分页查询结果是Page对象,可以直接使用,也可以根据需要进行修改,比如下面这个文件:GetterSetterpublicclassPageResultT{总条数privateLongtotal;总页数privateIntegerpageCount;每页数量privateIntegerpageSize;当前页码privateIntegerpageNum;分页数据privateListTdata;处理Jpa分页结果,Jpa页码默认从0开始,所以返回结果加1publicstaticTPageResultTok(org。springframework。data。domain。PageTpage){PageResultTresultnewPageResultT();result。setPageCount(page。getTotalPages());result。setPageNum(page。getNumber()1);result。setPageSize(page。getSize());result。setTotal(page。getTotalElements());result。setData(page。getContent());returnresult;}}复制代码JPA基础实体类 作为其他实体类的父类,封装了所有的公共字段,包括逻辑删除标志,版本号,创建人和修改人信息。到底是否需要那么多字段,结合实际情况,这里的示例代码比较全,其中LogicDelete和Version是Mybatis特有的注解,CreatedBy、CreatedDate是Springframework自带的注解,如果我们需要新建人和修改人姓名,则需要自定义注解。MappedSuperclassEntityListeners(AuditingEntityListener。class)GetterEqualsAndHashCode(ofid)SuperBuilder(toBuildertrue)NoArgsConstructorAllArgsConstructorpublicclassBaseDomainimplementsSerializable{IdGeneratedValue(strategyGenerationType。IDENTITY)privateLongid;Schema(name创建人姓名)CreatedByColumn(namecreateusername)privateStringcreateUserName;CreatedDateprivateLocalDateTimecreatedDate;LastModifiedBySchema(name修改人姓名)Column(namelastmodifiedname)privateStringlastModifiedName;LastModifiedDateprivateLocalDateTimelastModifiedDate;Schema(name)Column(namedelflag)privateIntegerdelFlag0;Schema(name版本号)VersionColumn(nameversion)privateIntegerversion;}复制代码 可以发现,相较于Mybatis和MybatisPlus少了两个字段,分别是createUserCode和lastModifiedCode,因为Spring提供的注解只有CreatedBy和LastModifiedBy,我们可以基于SpringData的AuditorAware审计功能来给这两个注解标识的字段赋值。JPA审计 简单介绍一下审计功能:即由谁在什么时候创建或修改实体。SpringData提供了在实体类的属性上增加CreatedBy,LastModifiedBy,CreatedDate,LastModifiedDate注解,并配置相应的配置项,即可实现审计功能,有系统自动记录createdBy、CreatedDate、lastModifiedBy、lastModifiedDate四个属性的值。 所以如果在AuditorAware实现类中根据CreatedBy拿到的值去数据库中查询userCode信息,也不是不可以,这里项目比较简单,所以就不过多介绍了。ConfigurationpublicclassJpaAutoConfigurationimplementsAuditorAwareString{OverridepublicOptionalStringgetCurrentAuditor(){SecurityContextctxSecurityContextHolder。getContext();Objectprincipalctx。getAuthentication()。getPrincipal();if(principal。getClass()。isAssignableFrom(String。class)){returnOptional。of((String)principal);}else{returnOptional。empty();}}}复制代码 因为我们没有使用SpringSecurity来配置token信息,所以这里获取的principal是默认值,值为anonymousUser。动态链式查询 利用JPA的Specification接口和元模型就实现动态查询,相较于这篇文章,本文实现借鉴了Mybatis中使用的ExampleBuilder,个人觉得效果更佳。 1、查询关系匹配枚举publicenumMatchCondition{equal相等,notEqual不等于,like模糊匹配,notLike,gt大于,ge大于等于,lt小于,le小于等于EQUAL,NOTEQUAL,LIKE,NOTLIKE,GT,GE,LT,LE,IN,NOTIN,BETWEEN,NOTBETWEEN}复制代码 2、查询条件连接符枚举publicenumOperator{AND,OR}复制代码 3、查询条件注解Target({ElementType。FIELD,ElementType。CONSTRUCTOR})Retention(RetentionPolicy。RUNTIME)publicinterfaceQueryCondition{数据库中字段名,默认为空字符串,则Query类中的字段要与数据库中字段一致Stringcolumn()default;seeMatchConditionMatchConditionfunc()defaultMatchCondition。EQUAL;}复制代码 4、连接符枚举publicenumConnector{ON,WHERE}复制代码 5、查询条件包装类理论上会有多个QueryParam对象,当Connector是where时,joinName和joinType为null,当Connector为on时,根据joinName的不同,会生成不同的QueryParam对象,joinType包括INNER、LEFTDataNoArgsConstructorAllArgsConstructorBuilderpublicclassQueryParam{privateConnectorconnector;privateListQueryItemqueryItems;privateStringjoinName;privateJoinTypejoinType;}DataNoArgsConstructorAllArgsConstructorBuilderpublicclassQueryItem{privateStringfieldName;privateObjectfieldValue;privateMatchConditionmatchCondition;between使用privateObjectstartValue;privateObjectendValue;in查询privateIterableObjectiterable;privateOperatoroperator;}复制代码 6、集成工具类 因代码篇幅受限,就只粘贴部分代码:publicSpecificationBuilderTandEqualTo(IFnT,Objectfn,Objectvalue){QueryItemqueryItemQueryItem。builder()。fieldName(Reflections。fnToFieldName(fn))。fieldValue(value)。operator(Operator。AND)。matchCondition(MatchCondition。EQUAL)。build();addQueryItemToWhereParam(queryItem);returnthis;}privatevoidaddQueryItemToWhereParam(QueryItemqueryItem){if(CollectionUtils。isEmpty(queryParams)){queryParams。add(addQueryItem(queryItem));}else{OptionalQueryParamqueryParamOptionalqueryParams。stream()。filter(objStringUtils。isEmpty(obj。getJoinName()))。findFirst();if(queryParamOptional。isPresent()){QueryParamqueryParamqueryParamOptional。get();queryParam。getQueryItems()。add(queryItem);}else{queryParams。add(addQueryItem(queryItem));}}}复制代码 相较于Mybatis项目中的ExampleBuilder,SpecificationBuilder仅用于查询功能,暂时无法提供修改,删除等功能,不过也有自己的额特色:可以实现连表查询,比如说leftjoin等,以及懒加载问题,避免连表查询时出现的N1查询。 至此,关于本项目中有价值的内容已经讲述完毕,因篇幅有限,未能展示所有代码。基于上述核心代码,我们只需要往项目中添加相关业务代码即可,接下来我们就可以运行之前写的脚本工具,根据数据库表信息快速生成模板代码。一键式生成模版代码 运行ormgenerate项目,在swagger上调用build接口,调用参数如下:{database:mysqldb,flat:true,type:jpa,group:hresh,host:127。0。0。1,module:orm,password:root,port:3306,table:〔customer,subuser〕,username:root,tableStartIndex:0}复制代码 代码文件直接移到项目中就行了,稍微修改一下引用就好了。功能实现请求日志输出 比如说我们访问customersqueryPage接口,看看控制台输出情况:RequestInfo:{classMethod:com。msdn。orm。hresh。controller。CustomerController。queryPage,ip:127。0。0。1,requestParams:{dto:{pageSortInfo:{count:true,pageSize:5,orderInfos:〔{column:name,asc:true}〕,pageNum:1}}},httpMethod:GET,url:http:localhost:8803customersqueryPage,result:{code:200,message:操作成功,success:true},methodDesc:获取父用户分页列表,timeCost:268}复制代码 可以看到,日志输出中包含前端传来的请求体,请求API,返回结果,API描述,API耗时。统一返回格式 比如说分页查询,返回结果如下:{data:{total:9,pageCount:2,pageSize:5,pageNum:1,data:〔{name:acorn,age:38,address:湖北武汉},{name:acorn2,age:28,address:湖北武汉},{name:hresh,age:44,address:湖北武汉},{name:love0,age:26,address:湖北武汉},{name:love1,age:26,address:湖北武汉}〕},code:200,message:操作成功,success:true}复制代码 如果是新增请求,返回结果为:{data:{name:rookie3,age:26,address:湖北武汉},code:200,message:操作成功,success:true}复制代码异常处理 下面简单演示一下参数异常的情况,在adduser时校验参数值是否为空。publicCustomerVOadd(CustomerDTOdto){if(StringUtils。isBlank(dto。getName())){BusinessException。validateFailed(userName不能为空);}CustomercustomercustomerRepository。save(customerStruct。dtoToModel(dto));returncustomerStruct。modelToVO(customer);}复制代码 如果传递的name值为空,则返回结果为:{data:null,code:400,message:userName不能为空,success:false}复制代码分组校验和自定义校验 修改CustomerDTO,当新增数据时,校验name不为空,修改数据时,address不为空。publicclassCustomerDTO{NotBlank(groups{Add。class})Schema(name)privateStringname;EnumValidator(value{18,20,24},messageage只能指定为18、20和24,其他值不合法)Schema(name)privateIntegerage;NotBlank(groups{Update。class})Schema(name)privateStringaddress;}复制代码 最后修改controller文件PostMappingOperation(description新增父用户)publicResultCustomerVOadd(Validated(Add。class)RequestBodyCustomerDTOdto){CustomerVOcustomerVOcustomerService。add(dto);returnResult。ok(customerVO);}复制代码 调用新增接口,故意将name置为空,返回结果为:{data:null,code:400,message:name不能为空,success:false}复制代码 如果age不为18、20和24,则出现如下错误:{data:null,code:400,message:ageage只能指定为18、20和24,其他值不合法,success:false}复制代码批量操作 这里简单演示一下关于批量新增的代码publicvoidbatchAdd(CustomerDTOdto){ListCustomercustomersnewArrayList();for(inti0;i3;i){CustomercustomernewCustomer();customer。setName(dto。getName()i);customer。setAge(dto。getAge());customer。setAddress(dto。getAddress());customers。add(customer);}customerRepository。saveAll(customers);}复制代码 注意,delFlag没有对应的注解,所以只能手动赋值为0,否则插入数据时会报错。 执行效果如下: 分页查询 前端参数传递:{pageNum:1,pageSize:5,orderInfos:〔{column:name,asc:true}〕}复制代码 后端代码处理:publicPageCustomerVOqueryPage(CustomerQueryPageDTOdto){PageablepageableSpecificationBuilder。getPageable(dto。getPageSortInfo());PageCustomercustomerPagecustomerRepository。findAll(pageable);returncustomerPage。map(customercustomerStruct。modelToVO(customer));}复制代码 返回结果为:{data:{total:9,pageCount:2,pageSize:5,pageNum:1,data:〔{name:acorn,age:38,address:湖北武汉},{name:acorn2,age:28,address:湖北武汉},{name:hresh,age:44,address:湖北武汉},{name:love0,age:26,address:湖北武汉},{name:love1,age:26,address:湖北武汉}〕},code:200,message:操作成功,success:true}复制代码动态查询 查询方法如下:publicListCustomerVOqueryList(CustomerDTOdto){ListCustomercustomersSpecificationBuilder。create(CustomerRepository。class)。andLike(Customer::getName,dto。getName())。select();returncustomerStruct。modelToVO(customers);}复制代码 执行结果如下:{data:〔{name:rookie,age:26,address:湖北武汉},{name:rookie1,age:26,address:湖北武汉},{name:rookie2,age:26,address:湖北武汉},{name:rookie3,age:26,address:湖北武汉}〕,code:200,message:操作成功,success:true}复制代码 SQL输出为:selectcustomer0。idasid10,customer0。createusernameascreateu20,customer0。createddateascreated30,customer0。delflagasdelflag40,customer0。lastmodifieddateaslastmod50,customer0。lastmodifiednameaslastmod60,customer0。versionasversion70,customer0。addressasaddress80,customer0。ageasage90,customer0。nameasname100fromcustomercustomer0where(customer0。delflag0)and(customer0。namelike?)复制代码 如果是分页查询,可以这样处理:publicPageCustomerVOqueryPage(CustomerQueryPageDTOdto){PageablepageableSpecificationBuilder。getPageable(dto。getPageSortInfo());PageCustomercustomerPageSpecificationBuilder。create(CustomerRepository。class)。andLike(Customer::getName,dto。getName())。select(pageable);PageCustomercustomerPagecustomerRepository。findAll(pageable);returncustomerPage。map(customercustomerStruct。modelToVO(customer));}复制代码 查询结果为:{data:{total:4,pageCount:1,pageSize:5,pageNum:1,data:〔{name:rookie,age:26,address:湖北武汉},{name:rookie1,age:26,address:湖北武汉},{name:rookie2,age:26,address:湖北武汉},{name:rookie3,age:26,address:湖北武汉}〕},code:200,message:操作成功,success:true}复制代码 SQL输出为:selectcustomer0。idasid10,customer0。createusernameascreateu20,customer0。createddateascreated30,customer0。delflagasdelflag40,customer0。lastmodifieddateaslastmod50,customer0。lastmodifiednameaslastmod60,customer0。versionasversion70,customer0。addressasaddress80,customer0。ageasage90,customer0。nameasname100fromcustomercustomer0where(customer0。delflag0)and(customer0。namelike?)orderbycasewhencustomer0。nameisnullthen1else0end,customer0。nameasclimit?复制代码一对多查询 使用JPA的好处就是可以使用OneToMany等注解,以及懒加载查询优化注解EntityGraph。 1、修改实体类publicclassCustomerextendsBaseDomain{privatestaticfinallongserialVersionUID1L;Schema(name)Column(namename)privateStringname;Schema(name)Column(nameage)privateIntegerage;Schema(name)Column(nameaddress)privateStringaddress;OneToMany(cascadeCascadeType。ALL)JoinColumn(namecustomerid)privateListSubUsersubUsers;}publicclassSubUserextendsBaseDomain{privatestaticfinallongserialVersionUID1L;Schema(name)Column(namename)privateStringname;Schema(name)Column(namecustomerid)privateStringcustomerId;Schema(name)Column(nameaddress)privateStringaddress;ManyToOneJoinColumnprivateCustomercustomer;}复制代码 2、更改CustomerRepository,这点格外重要,只有如此,才能使得SpecificationBuilder更有意义,否则即使可以连表查询,也会出现N1问题。RepositorypublicinterfaceCustomerRepositoryextendsJpaRepositoryCustomer,Long,JpaSpecificationExecutorCustomer{OverrideEntityGraph(attributePaths{subUsers})ListCustomerfindAll(SpecificationCustomerspec);OverrideEntityGraph(attributePaths{subUsers})PageCustomerfindAll(SpecificationCustomerspec,Pageablepageable);OverrideEntityGraph(attributePaths{subUsers})ListCustomerfindAll(SpecificationCustomerspec,Sortsort);}复制代码 3、Service层修改查询方法publicListCustomerVOqueryList(CustomerDTOdto){ListCustomercustomersSpecificationBuilder。create(CustomerRepository。class)。andLike(Customer::getName,dto。getName())。select();returncustomerStruct。modelToVO(customers);}复制代码 4、查询结果为{data:〔{name:rookie,age:26,address:湖北武汉,subUserVOS:〔{name:a1,address:青藏高原},{name:a2,address:青藏高原}〕},{name:rookie1,age:26,address:湖北武汉,subUserVOS:〔{name:c1,address:黄土高原},{name:c2,address:黄土高原}〕},{name:rookie2,age:26,address:湖北武汉,subUserVOS:〔〕},{name:rookie3,age:26,address:湖北武汉,subUserVOS:〔〕}〕,code:200,message:操作成功,success:true}复制代码 5、SQL输出 如果没有CustomerRepository的重写方法,则会出现N1问题。 连表查询 除了上述查询方法的使用,我们还可以手动来增加leftjoin的查询条件,比如说我们连表查询时,还要在on查询上增加额外的条件,也可以通过SpecificationBuilder来实现。 假设Customer和SubUser没有使用JoinColumn进行关联,而你此时想进行关联查询,可以这样做。publicListCustomerVOqueryList(CustomerDTOdto){ListCustomercustomersSpecificationBuilder。create(CustomerRepository。class)。andLike(Customer::getName,dto。getName())。leftJoin(Customer::getSubUsers)表示leftjoinsubuser。select();returncustomerStruct。modelToVO(customers);}复制代码 对应的SQL如下:selectcustomer0。idasid100,subusers1。idasid111,customer0。createusernameascreateu200,customer0。createddateascreated300,customer0。delflagasdelflag400,customer0。lastmodifieddateaslastmod500,customer0。lastmodifiednameaslastmod600,customer0。versionasversion700,customer0。addressasaddress800,customer0。ageasage900,customer0。nameasname1000,subusers1。createusernameascreateu211,subusers1。createddateascreated311,subusers1。delflagasdelflag411,subusers1。lastmodifieddateaslastmod511,subusers1。lastmodifiednameaslastmod611,subusers1。versionasversion711,subusers1。addressasaddress811,subusers1。customeridascustome1011,subusers1。nameasname911,subusers1。customeridascustome1010,subusers1。idasid110fromcustomercustomer0leftouterjoinsubusersubusers1oncustomer0。idsubusers1。customeridwhere(customer0。delflag0)and(customer0。namelike?)复制代码 如果你想额外增加on查询条件,可以这样实现。publicListCustomerVOqueryList(CustomerDTOdto){ListCustomercustomersSpecificationBuilder。create(CustomerRepository。class)。andLike(Customer::getName,dto。getName())。leftJoin(Customer::getSubUsers)。leftJoinAndOnEqualTo(Customer::getSubUsers,name,a1)。select();returncustomerStruct。modelToVO(customers);}复制代码 对应的SQL为selectcustomer0。idasid100,subusers1。idasid111,customer0。createusernameascreateu200,customer0。createddateascreated300,customer0。delflagasdelflag400,customer0。lastmodifieddateaslastmod500,customer0。lastmodifiednameaslastmod600,customer0。versionasversion700,customer0。addressasaddress800,customer0。ageasage900,customer0。nameasname1000,subusers1。createusernameascreateu211,subusers1。createddateascreated311,subusers1。delflagasdelflag411,subusers1。lastmodifieddateaslastmod511,subusers1。lastmodifiednameaslastmod611,subusers1。versionasversion711,subusers1。addressasaddress811,subusers1。customeridascustome1011,subusers1。nameasname911,subusers1。customeridascustome1010,subusers1。idasid110fromcustomercustomer0leftouterjoinsubusersubusers1oncustomer0。idsubusers1。customeridand(subusers1。name?)where(customer0。delflag0)and(customer0。namelike?)复制代码 关于on条件查询的实现,可以查看SpecificationBuilder中这段源码:ListQueryParamonQueryParamsqueryParams。stream()。filter(objStringUtils。isNotEmpty(obj。getJoinName()))。collect(Collectors。toList());for(QueryParamonQueryParam:onQueryParams){ListQueryItemqueryItemsonQueryParam。getQueryItems();if(CollectionUtils。isEmpty(queryItems)){root。join(onQueryParam。getJoinName(),onQueryParam。getJoinType());}else{JoinObject,Objectjoinroot。join(onQueryParam。getJoinName(),onQueryParam。getJoinType());for(QueryItemqueryItem:queryItems){ObjectvaluequeryItem。getFieldValue();switch(queryItem。getMatchCondition()){caseEQUAL:if(valueinstanceofString){关联表where查询andPredicates。add(criteriaBuilder。equal(join。get(queryItem。getFieldName())。as(String。class),value));关联表on查询join。on(criteriaBuilder。equal(join。get(queryItem。getFieldName())。as(String。class),value));}elseif(valueinstanceofInteger){andPredicates。add(criteriaBuilder。equal(join。get(queryItem。getFieldName())。as(Integer。class),value));join。on(criteriaBuilder。equal(join。get(queryItem。getFieldName())。as(Integer。class),value));}elseif(valueinstanceofLong){andPredicates。add(criteriaBuilder。equal(join。get(queryItem。getFieldName())。as(Long。class),value));on查询join。on(criteriaBuilder。equal(join。get(queryItem。getFieldName())。as(Long。class),value));}break;caseLIKE:if(valueinstanceofString){join。on(criteriaBuilder。like(join。get(queryItem。getFieldName())。as(String。class),value));}break;default:}}}}复制代码 如果你感兴趣,可以取消注释代码,试一下将subUser。name查询放到where条件中。if(valueinstanceofString){关联表where查询andPredicates。add(criteriaBuilder。equal(join。get(queryItem。getFieldName())。as(String。class),value));关联表on查询join。on(criteriaBuilder。equal(join。get(queryItem。getFieldName())。as(String。class),value));}复制代码 对应的SQL为:selectcustomer0。idasid100,subusers1。idasid111,customer0。createusernameascreateu200,customer0。createddateascreated300,customer0。delflagasdelflag400,customer0。lastmodifieddateaslastmod500,customer0。lastmodifiednameaslastmod600,customer0。versionasversion700,customer0。addressasaddress800,customer0。ageasage900,customer0。nameasname1000,subusers1。createusernameascreateu211,subusers1。createddateascreated311,subusers1。delflagasdelflag411,subusers1。lastmodifieddateaslastmod511,subusers1。lastmodifiednameaslastmod611,subusers1。versionasversion711,subusers1。addressasaddress811,subusers1。customeridascustome1011,subusers1。nameasname911,subusers1。customeridascustome1010,subusers1。idasid110fromcustomercustomer0leftouterjoinsubusersubusers1oncustomer0。idsubusers1。customeridwhere(customer0。delflag0)and(customer0。namelike?)andsubusers1。name?复制代码 为什么要这样实现呢?首先我们要了解on和where的区别:连接查询中,on是用来确定两张表的关联关系,关联好之后生成一个临时表,之后where对这个临时表再进行过滤筛选。先执行on,后执行where;on是建立关联关系在生成临时表时候执行,where是在临时表生成后对数据进行筛选的。复制代码 所以优先执行on条件查询,效率更高。 不仅如此,上述SQL执行结果为:{data:〔{name:rookie,age:26,address:湖北武汉,subUserVOS:〔{name:a1,address:青藏高原}〕}〕,code:200,message:操作成功,success:true}复制代码 这明显不是我们想要的结果。修改数据 在本项目中,表结构中都包含了version这个字段,即每次更新操作,version都应该加1。 CustomerRepository文件RepositorypublicinterfaceCustomerRepositoryextendsJpaRepositoryCustomer,Long,JpaSpecificationExecutorCustomer{ModifyingQuery(valueupdatecustomersetage:agewherename:nameanddelflag0,nativeQuerytrue)voidupdateByName(Param(name)Stringname,Param(age)intage);CustomerfindByName(Stringname);}复制代码 Service修改方法TransactionalOverridepublicCustomerVOedit(CustomerDTOdto){通过自定义修改方法的方式来达到修改数据,先修改再查询,version没有改变,修改时间也不变customerRepository。updateByName(dto。getName(),dto。getAge());returncustomerStruct。modelToVO(customerRepository。findByName(dto。getName()));先查询,再修改,这种方式才会触发乐观锁,即where条件中有version条件,更新操作verison1,修改时间也会变化CustomercustomercustomerRepository。findByName(dto。getName());customer。setAge(dto。getAge());customerRepository。save(customer);returncustomerStruct。modelToVO(customer);}复制代码Swagger 启动项目后,访问swagger,页面展示如下: 总结 上文中本人对于实现的SpecificationBuilder类还算满意,一开始只是为了实现分页查询,慢慢想实现MybatisPlus的那种链式查询,以及最后想要实现连接查询。代码实现目前就这样吧,如果大家有什么好玩的想法,欢迎大家留言评论。 感兴趣的朋友可以去我的Github下载相关代码,如果对你有所帮助,不妨Star一下,谢谢大家支持! 原文链接:https:juejin。cnpost7169472183184719886