范文健康探索娱乐情感热点
投稿投诉
热点动态
科技财经
情感日志
励志美文
娱乐时尚
游戏搞笑
探索旅游
历史星座
健康养生
美丽育儿
范文作文
教案论文

SpringBoot集成SpringDataJPA项目实操

  前言
  该说的在《SpringBoot集成Mybatis项目实操》一文中都讲了,本文只是将 Mybatis 换成了 Spring Data JPA,带大家将整个项目跑起来。
  本文将实现 SpringBoot+ Spring Data JPA 的项目搭建,项目特色是针对 JPA 专门写了一套动态链式查询工具类,进而实现一对多查询和连表查询。
  不说废话了,我们直接进入主题。数据库
  本项目采用的是 MySQL 数据库,版本为 8.x,建表语句如下:CREATE TABLE `customer` (   `id` int(11) NOT NULL AUTO_INCREMENT,   `name` varchar(20) DEFAULT NULL,   `age` int(11) DEFAULT NULL,   `address` varchar(100) DEFAULT NULL,   `created_date` timestamp NULL DEFAULT NULL,   `last_modified_date` timestamp NULL DEFAULT NULL,   `del_flag` int(2) NOT NULL DEFAULT "0",   `create_user_name` varchar(50) DEFAULT NULL,   `last_modified_name` varchar(50) DEFAULT NULL,   `version` int(11) NOT NULL DEFAULT "0",   PRIMARY KEY (`id`),   UNIQUE KEY `uk_name` (`name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT="父用户";  CREATE TABLE `subUser` (   `id` int(11) NOT NULL AUTO_INCREMENT,   `name` varchar(20) DEFAULT NULL,   `customer_id` varchar(36) NOT NULL,   `address` varchar(100) DEFAULT NULL,   `created_date` timestamp NULL DEFAULT NULL,   `last_modified_date` timestamp NULL DEFAULT NULL,   `del_flag` int(2) NOT NULL DEFAULT "0",   `create_user_name` varchar(50) DEFAULT NULL,   `last_modified_name` varchar(50) DEFAULT NULL,   `version` int(11) NOT NULL DEFAULT "0",   PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT="子用户"; 复制代码搭建SpringBoot项目
  使用 IDEA 新建一个 Maven 项目,叫做 jpa-springboot。
  一些共用的基础代码可以参考上篇文章,这里不做重复介绍,会介绍一些 JPA 相关的代码。引入依赖   org.springframework.boot   spring-boot-starter-parent   2.6.3      1.8   1.2.73   5.5.1   8.0.19   2.1.4   4.1.5   1.4.2.Final   1.18.20           org.springframework.boot     spring-boot-starter-web           org.springframework.boot     spring-boot-starter-aop           org.springframework.boot     spring-boot-starter-security           org.springframework.boot     spring-boot-starter-validation           com.alibaba     fastjson     ${fastjson.version}           cn.hutool     hutool-all     ${hutool.version}           org.projectlombok     lombok     ${org.projectlombok.version}     true           org.springframework.boot     spring-boot-starter-test     test           mysql     mysql-connector-java     ${mysql.version}     runtime           org.mybatis.spring.boot     mybatis-spring-boot-starter     ${mybatis.version}             org.springframework.boot       spring-boot-starter-data-jpa             org.springdoc     springdoc-openapi-ui     1.6.9           com.alibaba     druid-spring-boot-starter     1.1.18            org.mapstruct     mapstruct     ${org.mapstruct.version}           org.mapstruct     mapstruct-processor     ${org.mapstruct.version}                     org.springframework.boot       spring-boot-maven-plugin          复制代码
  有些依赖不一定是最新版本,而且你看到这篇文章时,可能已经发布了新版本,到时候可以先模仿着将项目跑起来后,再根据自己的需求来升级各项依赖,有问题咱再解决问题。分页处理
  某些业务场景是需要分页查询和排序功能的,所以我们需要考虑前端如何传递参数给后端,后端如何进行分页查询或者是排序查询。JPA 分页查询使用的是 Spring 自带的 Pageable。
  分页基础类public class SimplePageInfo {    private Integer pageNum = 1;   private Integer pageSize = 10;    public Integer getPageNum() {     return pageNum;   }    public void setPageNum(Integer pageNum) {     this.pageNum = pageNum;   }    public Integer getPageSize() {     return pageSize;   }    public void setPageSize(Integer pageSize) {     this.pageSize = pageSize;   } } 复制代码
  排序包装类@Getter @Setter public class OrderInfo {    private boolean asc = true;    private String column; } 复制代码
  分页且排序包装类@Getter @Setter @EqualsAndHashCode(callSuper = true) public class PageSortInfo extends SimplePageInfo {    @Schema(name = "排序信息")   private List orderInfos;    public String parseSort() {     if (CollectionUtils.isEmpty(orderInfos)) {       return null;     }     StringBuilder sb = new StringBuilder();     for (OrderInfo orderInfo : orderInfos) {       sb.append(orderInfo.getColumn()).append(" ");       sb.append(orderInfo.isAsc() ? " ASC," : " DESC,");     }     sb.deleteCharAt(sb.length() - 1);     return sb.toString();   } } 复制代码
  前端分页查询的请求体对象@Getter @Setter @AllArgsConstructor @NoArgsConstructor @Builder public class CustomerQueryPageDTO {      @JsonUnwrapped     private PageSortInfo pageSortInfo; } 复制代码
  服务层分页查询Pageable pageable = SpecificationBuilder.getPageable(dto.getPageSortInfo()); Page customerPage = customerRepository.findAll(pageable); 复制代码
  关于 PageHelper 的使用这里就不多做介绍了。
  我们得到的分页查询结果是 Page 对象,可以直接使用,也可以根据需要进行修改,比如下面这个文件:@Getter @Setter public class PageResult {    /**    * 总条数    */   private Long total;   /**    * 总页数    */   private Integer pageCount;   /**    * 每页数量    */   private Integer pageSize;   /**    * 当前页码    */   private Integer pageNum;    /**    * 分页数据    */   private List data;    /**    * 处理Jpa分页结果,Jpa页码默认从0开始,所以返回结果加1    */   public static  PageResult ok(org.springframework.data.domain.Page page) {     PageResult result = new PageResult();     result.setPageCount(page.getTotalPages());     result.setPageNum(page.getNumber() + 1);     result.setPageSize(page.getSize());     result.setTotal(page.getTotalElements());     result.setData(page.getContent());     return result;   } } 复制代码JPA基础实体类
  作为其他实体类的父类,封装了所有的公共字段,包括逻辑删除标志,版本号,创建人和修改人信息。到底是否需要那么多字段,结合实际情况,这里的示例代码比较全,其中@LogicDelete 和@Version 是 Mybatis 特有的注解,@CreatedBy、@CreatedDate 是Springframework 自带的注解,如果我们需要新建人和修改人姓名,则需要自定义注解。@MappedSuperclass @EntityListeners(AuditingEntityListener.class) @Getter @EqualsAndHashCode(of = "id") @SuperBuilder(toBuilder = true) @NoArgsConstructor @AllArgsConstructor public class BaseDomain implements Serializable {    @Id   @GeneratedValue(strategy = GenerationType.IDENTITY)   private Long id;    @Schema(name = "创建人姓名")   @CreatedBy   @Column(name = "create_user_name")   private String createUserName;    @CreatedDate   private LocalDateTime createdDate;    @LastModifiedBy   @Schema(name = "修改人姓名")   @Column(name = "last_modified_name")   private String lastModifiedName;    @LastModifiedDate   private LocalDateTime lastModifiedDate;    @Schema(name = "")   @Column(name = "del_flag")   private Integer delFlag = 0;    @Schema(name = "版本号")   @Version   @Column(name = "version")   private Integer version; } 复制代码
  可以发现,相较于 Mybatis 和 MybatisPlus 少了两个字段,分别是 createUserCode 和 lastModifiedCode,因为 Spring 提供的注解只有 CreatedBy 和 LastModifiedBy,我们可以基于 Spring Data 的 AuditorAware 审计功能来给这两个注解标识的字段赋值。JPA审计
  简单介绍一下审计功能:即由谁在什么时候创建或修改实体。Spring Data 提供了在实体类的属性上增加@CreatedBy,@LastModifiedBy,@CreatedDate,@LastModifiedDate 注解,并配置相应的配置项,即可实现审计功能,有系统自动记录createdBy 、CreatedDate 、lastModifiedBy 、lastModifiedDate四个属性的值。
  所以如果在 AuditorAware 实现类中根据@CreatedBy 拿到的值去数据库中查询 userCode 信息,也不是不可以,这里项目比较简单,所以就不过多介绍了。@Configuration public class JpaAutoConfiguration implements AuditorAware {    @Override   public Optional getCurrentAuditor() {     SecurityContext ctx = SecurityContextHolder.getContext();     Object principal = ctx.getAuthentication().getPrincipal();     if (principal.getClass().isAssignableFrom(String.class)) {       return Optional.of((String) principal);     } else {       return Optional.empty();     }   } } 复制代码
  因为我们没有使用 SpringSecurity 来配置 token 信息,所以这里获取的 principal 是默认值,值为 anonymousUser。动态链式查询
  利用 JPA 的Specification接口和元模型就实现动态查询,相较于这篇文章,本文实现借鉴了 Mybatis 中使用的 ExampleBuilder,个人觉得效果更佳。
  1、查询关系匹配枚举public enum MatchCondition {    /**    * equal-相等,notEqual-不等于,like-模糊匹配,notLike-, gt-大于,ge-大于等于,lt-小于,le-小于等于    */   EQUAL,   NOT_EQUAL,   LIKE,   NOT_LIKE,    GT,   GE,   LT,   LE,    IN,   NOT_IN,   BETWEEN,   NOT_BETWEEN } 复制代码
  2、查询条件连接符枚举public enum Operator {   AND,   OR } 复制代码
  3、查询条件注解@Target({ElementType.FIELD, ElementType.CONSTRUCTOR}) @Retention(RetentionPolicy.RUNTIME) public @interface QueryCondition {    /**    * 数据库中字段名,默认为空字符串,则Query类中的字段要与数据库中字段一致    */   String column() default "";    /**    * @see MatchCondition    */   MatchCondition func() default MatchCondition.EQUAL; } 复制代码
  4、连接符枚举public enum Connector {   ON,   WHERE } 复制代码
  5、查询条件包装类//理论上会有多个QueryParam对象,当Connector是where时,joinName和joinType为null,当Connector为on时,根据joinName的不同,会生成不同的QueryParam对象,joinType包括INNER、LEFT @Data @NoArgsConstructor @AllArgsConstructor @Builder public class QueryParam {    private Connector connector;    private List queryItems;    private String joinName;    private JoinType joinType; }  @Data @NoArgsConstructor @AllArgsConstructor @Builder public class QueryItem {    private String fieldName;   private Object fieldValue;   private MatchCondition matchCondition;    // between使用   private Object startValue;   private Object endValue;    // in查询   private Iterable iterable;    private Operator operator; } 复制代码
  6、集成工具类
  因代码篇幅受限,就只粘贴部分代码:public SpecificationBuilder andEqualTo(IFn fn, Object value) {   QueryItem queryItem = QueryItem.builder().fieldName(Reflections.fnToFieldName(fn))     .fieldValue(value).operator(Operator.AND).matchCondition(     MatchCondition.EQUAL).build();    addQueryItemToWhereParam(queryItem);   return this; }  private void addQueryItemToWhereParam(QueryItem queryItem) {   if (CollectionUtils.isEmpty(queryParams)) {     queryParams.add(addQueryItem(queryItem));   } else {     Optional queryParamOptional = queryParams.stream()       .filter(obj -> StringUtils.isEmpty(obj.getJoinName())).findFirst();     if (queryParamOptional.isPresent()) {       QueryParam queryParam = queryParamOptional.get();       queryParam.getQueryItems().add(queryItem);     } else {       queryParams.add(addQueryItem(queryItem));     }   } } 复制代码
  相较于 Mybatis 项目中的 ExampleBuilder,SpecificationBuilder 仅用于查询功能,暂时无法提供修改,删除等功能,不过也有自己的额特色:可以实现连表查询,比如说 left join 等,以及懒加载问题,避免连表查询时出现的 N+1 查询。
  至此,关于本项目中有价值的内容已经讲述完毕,因篇幅有限,未能展示所有代码。基于上述核心代码,我们只需要往项目中添加相关业务代码即可,接下来我们就可以运行之前写的脚本工具,根据数据库表信息快速生成模板代码。一键式生成模版代码
  运行 orm-generate 项目,在 swagger 上调用 /build 接口,调用参数如下:{     "database": "mysql_db",     "flat": true,     "type": "jpa",     "group": "hresh",     "host": "127.0.0.1",     "module": "orm",     "password": "root",     "port": 3306,     "table": [         "customer",         "sub_user"     ],     "username": "root",     "tableStartIndex":"0" } 复制代码
  代码文件直接移到项目中就行了,稍微修改一下引用就好了。功能实现请求日志输出
  比如说我们访问 customers/queryPage 接口,看看控制台输出情况:Request Info      : {"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:8803/customers/queryPage","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 } 复制代码异常处理
  下面简单演示一下参数异常的情况,在 add user 时校验参数值是否为空。  public CustomerVO add(CustomerDTO dto) {     if (StringUtils.isBlank(dto.getName())) {       BusinessException.validateFailed("userName不能为空");     }     Customer customer = customerRepository.save(customerStruct.dtoToModel(dto));     return customerStruct.modelToVO(customer);   } 复制代码
  如果传递的 name 值为空,则返回结果为:{ 	"data": null, 	"code": "400", 	"message": "userName不能为空", 	"success": false } 复制代码分组校验和自定义校验
  修改 CustomerDTO,当新增数据时,校验 name 不为空,修改数据时,address 不为空。public class CustomerDTO {    @NotBlank(groups = {Add.class})   @Schema(name = "")   private String name;    @EnumValidator(value = {"18", "20", "24"}, message = "age只能指定为18、20和24,其他值不合法")   @Schema(name = "")   private Integer age;    @NotBlank(groups = {Update.class})   @Schema(name = "")   private String address;  } 复制代码
  最后修改 controller 文件  @PostMapping   @Operation(description = "新增父用户")   public Result add(@Validated(Add.class) @RequestBody CustomerDTO dto) {     CustomerVO customerVO = customerService.add(dto);     return Result.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 } 复制代码批量操作
  这里简单演示一下关于批量新增的代码  public void batchAdd(CustomerDTO dto) {     List customers = new ArrayList<>();     for (int i = 0; i < 3; i++) {       Customer customer = new Customer();       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 		} 	] } 复制代码
  后端代码处理:  public Page queryPage(CustomerQueryPageDTO dto) {     Pageable pageable = SpecificationBuilder.getPageable(dto.getPageSortInfo());     Page customerPage = customerRepository.findAll(pageable);     return customerPage.map(customer -> customerStruct.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 } 复制代码动态查询
  查询方法如下:  public List queryList(CustomerDTO dto) {     List customers = SpecificationBuilder.create(CustomerRepository.class)         .andLike(Customer::getName, dto.getName() + "%")         .select();     return customerStruct.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 输出为:select customer0_.id as id1_0_, customer0_.create_user_name as create_u2_0_, customer0_.created_date as created_3_0_, customer0_.del_flag as del_flag4_0_, customer0_.last_modified_date as last_mod5_0_, customer0_.last_modified_name as last_mod6_0_, customer0_.version as version7_0_, customer0_.address as address8_0_, customer0_.age as age9_0_, customer0_.name as name10_0_ from customer customer0_ where ( customer0_.del_flag=0) and (customer0_.name like ?) 复制代码
  如果是分页查询,可以这样处理:  public Page queryPage(CustomerQueryPageDTO dto) {     Pageable pageable = SpecificationBuilder.getPageable(dto.getPageSortInfo());     Page customerPage = SpecificationBuilder.create(CustomerRepository.class)         .andLike(Customer::getName, dto.getName() + "%")         .select(pageable); //    Page customerPage = customerRepository.findAll(pageable);     return customerPage.map(customer -> customerStruct.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 输出为:select customer0_.id as id1_0_, customer0_.create_user_name as create_u2_0_, customer0_.created_date as created_3_0_, customer0_.del_flag as del_flag4_0_, customer0_.last_modified_date as last_mod5_0_, customer0_.last_modified_name as last_mod6_0_, customer0_.version as version7_0_, customer0_.address as address8_0_, customer0_.age as age9_0_, customer0_.name as name10_0_ from customer customer0_ where ( customer0_.del_flag=0) and (customer0_.name like ?) order by case when customer0_.name is null then 1 else 0 end, customer0_.name asc limit ? 复制代码一对多查询
  使用 JPA 的好处就是可以使用 @OneToMany 等注解,以及懒加载查询优化注解 @EntityGraph。
  1、修改实体类public class Customer extends BaseDomain {    private static final long serialVersionUID = 1L;    @Schema(name = "")   @Column(name = "name")   private String name;    @Schema(name = "")   @Column(name = "age")   private Integer age;    @Schema(name = "")   @Column(name = "address")   private String address;    @OneToMany(cascade = CascadeType.ALL)   @JoinColumn(name = "customer_id")   private List subUsers; }  public class SubUser extends BaseDomain {    private static final long serialVersionUID = 1L;    @Schema(name = "")   @Column(name = "name")   private String name;  //  @Schema(name = "") //  @Column(name = "customer_id") //  private String customerId;    @Schema(name = "")   @Column(name = "address")   private String address;    @ManyToOne   @JoinColumn   private Customer customer; } 复制代码
  2、更改 CustomerRepository,这点格外重要,只有如此,才能使得 SpecificationBuilder 更有意义,否则即使可以连表查询,也会出现 N+1 问题。@Repository public interface CustomerRepository extends JpaRepository,     JpaSpecificationExecutor {    @Override   @EntityGraph(       attributePaths = {"subUsers"}   )   List findAll(Specification spec);    @Override   @EntityGraph(       attributePaths = {"subUsers"}   )   Page findAll(Specification spec, Pageable pageable);    @Override   @EntityGraph(       attributePaths = {"subUsers"}   )   List findAll(Specification spec, Sort sort); } 复制代码
  3、Service 层修改查询方法  public List queryList(CustomerDTO dto) {     List customers = SpecificationBuilder.create(CustomerRepository.class)         .andLike(Customer::getName, dto.getName() + "%")         .select();     return customerStruct.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 的重写方法,则会出现 N+1 问题。
  连表查询
  除了上述查询方法的使用,我们还可以手动来增加 left join 的查询条件,比如说我们连表查询时,还要在 on 查询上增加额外的条件,也可以通过 SpecificationBuilder 来实现。
  假设 Customer 和 SubUser 没有使用@JoinColumn 进行关联,而你此时想进行关联查询,可以这样做。  public List queryList(CustomerDTO dto) {     List customers = SpecificationBuilder.create(CustomerRepository.class)         .andLike(Customer::getName, dto.getName() + "%")         .leftJoin(Customer::getSubUsers)// 表示 left join sub_user         .select();     return customerStruct.modelToVO(customers);   } 复制代码
  对应的 SQL 如下:select 	customer0_.id as id1_0_0_, 	subusers1_.id as id1_1_1_, 	customer0_.create_user_name as create_u2_0_0_, 	customer0_.created_date as created_3_0_0_, 	customer0_.del_flag as del_flag4_0_0_, 	customer0_.last_modified_date as last_mod5_0_0_, 	customer0_.last_modified_name as last_mod6_0_0_, 	customer0_.version as version7_0_0_, 	customer0_.address as address8_0_0_, 	customer0_.age as age9_0_0_, 	customer0_.name as name10_0_0_, 	subusers1_.create_user_name as create_u2_1_1_, 	subusers1_.created_date as created_3_1_1_, 	subusers1_.del_flag as del_flag4_1_1_, 	subusers1_.last_modified_date as last_mod5_1_1_, 	subusers1_.last_modified_name as last_mod6_1_1_, 	subusers1_.version as version7_1_1_, 	subusers1_.address as address8_1_1_, 	subusers1_.customer_id as custome10_1_1_, 	subusers1_.name as name9_1_1_, 	subusers1_.customer_id as custome10_1_0__, 	subusers1_.id as id1_1_0__ from 	customer customer0_ left outer join sub_user subusers1_ on 	customer0_.id = subusers1_.customer_id where 	( customer0_.del_flag = 0) 	and (customer0_.name like ?) 复制代码
  如果你想额外增加 on 查询条件,可以这样实现。  public List queryList(CustomerDTO dto) {     List customers = SpecificationBuilder.create(CustomerRepository.class)         .andLike(Customer::getName, dto.getName() + "%") //        .leftJoin(Customer::getSubUsers)         .leftJoinAndOnEqualTo(Customer::getSubUsers,"name","a1")         .select();     return customerStruct.modelToVO(customers);   } 复制代码
  对应的 SQL 为select 	customer0_.id as id1_0_0_, 	subusers1_.id as id1_1_1_, 	customer0_.create_user_name as create_u2_0_0_, 	customer0_.created_date as created_3_0_0_, 	customer0_.del_flag as del_flag4_0_0_, 	customer0_.last_modified_date as last_mod5_0_0_, 	customer0_.last_modified_name as last_mod6_0_0_, 	customer0_.version as version7_0_0_, 	customer0_.address as address8_0_0_, 	customer0_.age as age9_0_0_, 	customer0_.name as name10_0_0_, 	subusers1_.create_user_name as create_u2_1_1_, 	subusers1_.created_date as created_3_1_1_, 	subusers1_.del_flag as del_flag4_1_1_, 	subusers1_.last_modified_date as last_mod5_1_1_, 	subusers1_.last_modified_name as last_mod6_1_1_, 	subusers1_.version as version7_1_1_, 	subusers1_.address as address8_1_1_, 	subusers1_.customer_id as custome10_1_1_, 	subusers1_.name as name9_1_1_, 	subusers1_.customer_id as custome10_1_0__, 	subusers1_.id as id1_1_0__ from 	customer customer0_ left outer join sub_user subusers1_ on 	customer0_.id = subusers1_.customer_id 	and (subusers1_.name =?) where 	( customer0_.del_flag = 0) 	and (customer0_.name like ?) 复制代码
  关于 on 条件查询的实现,可以查看 SpecificationBuilder 中这段源码:List onQueryParams = queryParams.stream()   .filter(obj -> StringUtils.isNotEmpty(obj.getJoinName()))   .collect(Collectors.toList()); for (QueryParam onQueryParam : onQueryParams) {   List queryItems = onQueryParam.getQueryItems();   if (CollectionUtils.isEmpty(queryItems)) {     root.join(onQueryParam.getJoinName(), onQueryParam.getJoinType());   } else {     Join join = root       .join(onQueryParam.getJoinName(), onQueryParam.getJoinType());     for (QueryItem queryItem : queryItems) {       Object value = queryItem.getFieldValue();       switch (queryItem.getMatchCondition()) {         case EQUAL:           if (value instanceof String) {             // 关联表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));           } else if (value instanceof Integer) {             //                  andPredicates.add(criteriaBuilder             //                      .equal(join.get(queryItem.getFieldName()).as(Integer.class), value));             join.on(criteriaBuilder                     .equal(join.get(queryItem.getFieldName()).as(Integer.class), value));           } else if (value instanceof Long) {             //                  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;         case LIKE:           if (value instanceof String) {             join.on(criteriaBuilder                     .like(join.get(queryItem.getFieldName()).as(String.class),                           "%" + value + "%"));           }           break;         default:       }     }   } } 复制代码
  如果你感兴趣,可以取消注释代码,试一下将 subUser.name 查询放到 where 条件中。if (value instanceof String) {   // 关联表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 为:select 	customer0_.id as id1_0_0_, 	subusers1_.id as id1_1_1_, 	customer0_.create_user_name as create_u2_0_0_, 	customer0_.created_date as created_3_0_0_, 	customer0_.del_flag as del_flag4_0_0_, 	customer0_.last_modified_date as last_mod5_0_0_, 	customer0_.last_modified_name as last_mod6_0_0_, 	customer0_.version as version7_0_0_, 	customer0_.address as address8_0_0_, 	customer0_.age as age9_0_0_, 	customer0_.name as name10_0_0_, 	subusers1_.create_user_name as create_u2_1_1_, 	subusers1_.created_date as created_3_1_1_, 	subusers1_.del_flag as del_flag4_1_1_, 	subusers1_.last_modified_date as last_mod5_1_1_, 	subusers1_.last_modified_name as last_mod6_1_1_, 	subusers1_.version as version7_1_1_, 	subusers1_.address as address8_1_1_, 	subusers1_.customer_id as custome10_1_1_, 	subusers1_.name as name9_1_1_, 	subusers1_.customer_id as custome10_1_0__, 	subusers1_.id as id1_1_0__ from 	customer customer0_ left outer join sub_user subusers1_ on 	customer0_.id = subusers1_.customer_id where 	( customer0_.del_flag = 0) 	and (customer0_.name like ?) 	and subusers1_.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文件@Repository public interface CustomerRepository extends JpaRepository,     JpaSpecificationExecutor {    @Modifying   @Query(value = "update customer set age = :age where name = :name and del_flag=0", nativeQuery = true)   void updateByName(@Param("name") String name, @Param("age") int age);    Customer findByName(String name); } 复制代码
  Service 修改方法@Transactional @Override public CustomerVO edit(CustomerDTO dto) {   // 通过自定义修改方法的方式来达到修改数据,先修改再查询,version没有改变,修改时间也不变   //    customerRepository.updateByName(dto.getName(), dto.getAge());   //    return customerStruct.modelToVO(customerRepository.findByName(dto.getName()));    // 先查询,再修改,这种方式才会触发乐观锁,即where条件中有version条件,更新操作verison+1,修改时间也会变化   Customer customer = customerRepository.findByName(dto.getName());   customer.setAge(dto.getAge());   customerRepository.save(customer);   return customerStruct.modelToVO(customer); } 复制代码Swagger
  启动项目后,访问 swagger,页面展示如下:
  总结
  上文中本人对于实现的 SpecificationBuilder 类还算满意,一开始只是为了实现分页查询,慢慢想实现 Mybatis Plus 的那种链式查询,以及最后想要实现连接查询。代码实现目前就这样吧,如果大家有什么好玩的想法,欢迎大家留言评论。
  感兴趣的朋友可以去我的 Github 下载相关代码,如果对你有所帮助,不妨 Star 一下,谢谢大家支持!
  原文链接:https://juejin.cn/post/7169472183184719886
仲景牌六味地黄丸源自医圣张仲景的补肾经方,可调理肾阴亏损中医的九种体质,大家虽然说不具体,但日常生活中多少都会有点讲究。比如我这人爱上火,吃不了辣我怕冷,早早就穿上秋衣秋裤了我消化不好,多吃一点就会胃胀难受我青菜吃少了嗯嗯就不顺畅了其实这一类食物很便宜,既能帮助降糖,还能帮助降血压在的你印象里,是不是觉得,人在生病的时候,要饮食清淡,少吃辛辣?但真实情况可能未必是这样。研究发现,吃辣椒能降低血糖和血压水平,有糖尿病和高血压的人,反而可以适当吃点辣24。那辣椒总是如鲠在喉?小心反流性咽喉炎咽炎的病因林林总总,大家对于咽炎的症状也是相对熟悉了,但是有一种特殊的咽炎反流性咽喉炎,我们大部分人对它一无所知。虽然反流性咽喉炎在大众的认知里并不熟悉,但在临床工作中却并不陌生。百会穴为啥叫百,会?百会别名三阳五会天满泥丸宫巅上。百会位置在前顶后一寸五分,顶中央旋毛中,陷可容指。取法正坐,在后发际中点上7寸处或于头部中线与两耳尖连线的交点处取穴。局部解剖在帽状腱膜中有左右颞浅长期吃面食,与长期吃米饭的人,有什么区别?哪种人身体更健康最早在黄河流域文化刚刚诞生的时期,主食这一概念就已经存在了。考古学家从古文化遗迹中发现的谷物种子,都能够很好地证明这一点。直至今日,我国主食主要分化为米饭和面食两种流派,而对于两种中国6人出战仅丁俊晖输球,12人晋级德国正赛创纪录,种子大溃败11月26日,本赛季世界斯诺克巡回赛德国大师赛资格赛全部结束,晋级正赛32强的选手悉数诞生。中国军团在最后一个比赛日中表现出色,总计6人出战决胜轮,只有丁俊晖一人输球出局,其他5人广东队三消息赵睿升任队长,锋线小将被租借,第二阶段名单被曝赵睿升任球队队长。北京时间11月27日,CBA第一阶段休赛期,根据国内媒体爆料,广东男篮休赛期期间,在球队队长的人选是再次作出调整,老将任俊飞将不再担任球队队长一职,球队队长将会由浓眉缺阵,老詹狂砍39分11板3助1断1帽,湖人再胜马刺,411倒计时今天洛杉矶湖人客场背靠背继续挑战圣安东尼奥马刺队,詹姆斯全场出战35分钟21投11中,砍下39分11篮板3助攻1次抢断1封盖,今天只有4次失误,投篮手感爆棚,罚球线上今天10罚10老詹复出9次失误!刚踢世界杯赶回来,凑合着看吧!韩国瑜伽网红找找状态!老詹复出17中8砍下21分8板5助2断9失误NBA常规赛,湖人客场10594力克马刺。此役,老詹伤缺5场后复出,全场比赛出战35分钟,17中8得到21分8板5助2断,值1接受多场四官考验中国裁判等待主裁时机马宁出场担任第四官员。中国青年报客户端北京11月27日电(中青报中青网记者郭剑文刘占崑摄)27日凌晨,卡塔尔世界杯D组次轮,卫冕冠军法国队20战胜丹麦队成为第一支晋级16强球队,中火箭变主场龙!格林单节9分5助攻13分,双塔3632,圆脸4记三分费尔南多的防守并不比森贡强多少,塞拉斯估计也看出来了,于是让他休战。森贡身体恢复了,回归后继续出任首发中锋。戈登没问题,他属于背靠背轮休,回来之后仍然担任首发小前锋,小马丁虽然打得
美国科技巨头被赶出中国?曾霸占市场30年,为何会有如今这下场?中国地域辽阔,人口众多,自然而然也有广阔的市场。特别是中国劳动力还是相对廉价的,这就吸引了相当多国家的公司来中国占据市场。他们不仅用中国廉价的劳动力来节省成本,更是利用中国的市场挣央视主持曹晴嫁美国医生,婚后挨饿受虐,一包姨妈巾用一年当事业达到了高峰,成为了众人瞩目的对象,爱情的光芒便会悄然而至。然而有这样一个女人,金光闪闪,却深陷婚姻泥潭,她就是我们今天要说的主角曹晴。作为电视台知名主持人,她有一个漂亮的履历代价太大!胡明轩被肘击宏远两将为他出头一神射被禁赛无缘战辽宁北京时间12月12日,2223赛季CBA常规赛第12轮,广东宏远对阵天津男篮,双方实力相差悬殊,四节战罢,广东宏远以9379击败天津队,拿到3连胜。比赛的上半场双方打得还是波澜不惊空调开了后自动关机是什么原因?空调可以给我们带来舒适的温度,在炎热的夏季空调是非常重要的,但是空调使用过程中难免出现故障,比如开机会自动关机的问题,那空调开了后自动关机是什么原因?空调开了几秒钟自动停了怎么办?查了资料,我才知道孙机说司母戊鼎其实叫后母戊鼎的原因司母戊鼎其实叫后母戊鼎!93岁的文物专家考古学家中国国家博物馆终身研究员,孙机较真地说。我看到这条信息,很疑惑。24年前,我上初中的时候,学的是司母戊鼎。为什么现在文物专家孙机却说县委大院揭露官场潜规则,直面现实,它的成功有原因最近由正午阳光拍摄的关于官场生活的年度大剧县委大院正在热播当中。在短短播出的两集里就收视破2,在众多平台上收视率都很高。这部以基层干部带领群众脱贫的故事为什么这么受欢迎呢,今天就让魔兽wlk亚服哪个加速器不封号?魔兽怀旧服亚服封号原因最近因为魔兽国服停服导致很多玩家开始进入亚服台服,甚至连很多老玩家都又回到了魔兽世界游戏,想在停服之前好好玩玩这个游戏。不过最近很多玩家都反映一件事,刚进魔兽wlk亚服游戏不久就被中兴通讯联合中国移动智慧家庭运营中心首发FTTR移动高清方案近日,2022中国移动全球合作伙伴大会盛大开幕,作为中国移动的重要合作伙伴,中兴通讯携手中移(杭州)信息技术有限公司(以下简称智慧家庭运营中心)首发FTTRWiFi6机顶盒承载移动喜报国投特检科技数据驱动的溴系化学品全流程智能调控技术研究及应用获中国自动化学会科技进步二等奖大众网海报新闻报道近日,2022中国自动化学会科技进步奖完成评审,国投特检科技完成的数据驱动的溴系化学品全流程智能调控技术研究及应用荣获2022年度中国自动化学会科技进步奖二等奖。中国11月末M2同比增长12。4当月新增人民币贷款1。21万亿元中新社北京12月12日电(记者夏宾)中国央行12日发布金融数据显示,11月末,广义货币(M2)余额264。7万亿元(人民币,下同),同比增长12。4,增速分别比上月末和上年同期高0老干妈等中国老牌标杆企业到底该何去何从?最近大家又在说老干妈这家以前如神般存在的国民神器企业营业额现象问题贵州省工商业联合会与贵州省企业联合会共同发布了2022贵州民营企业100强榜单。令人意外的是,过去常年名列前十中游