SpringBoot集成Spring Data JPA项目实操

news2024/11/15 14:09:33

《从零打造项目》系列文章

工具

  • 比MyBatis Generator更强大的代码生成器

ORM框架选型

  • SpringBoot项目基础设施搭建

  • SpringBoot集成Mybatis项目实操

  • SpringBoot集成MybatisPlus项目实操

  • SpringBoot集成Spring Data JPA项目实操

前言

该说的在《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 相关的代码。

引入依赖

<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>2.6.3</version>
</parent>

<properties>
  <java.version>1.8</java.version>
  <fastjson.version>1.2.73</fastjson.version>
  <hutool.version>5.5.1</hutool.version>
  <mysql.version>8.0.19</mysql.version>
  <mybatis.version>2.1.4</mybatis.version>
  <mapper.version>4.1.5</mapper.version>
  <org.mapstruct.version>1.4.2.Final</org.mapstruct.version>
  <org.projectlombok.version>1.18.20</org.projectlombok.version>
</properties>

<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
  </dependency>
  <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>${fastjson.version}</version>
  </dependency>
  <dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>${hutool.version}</version>
  </dependency>
  <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>${org.projectlombok.version}</version>
    <optional>true</optional>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
  </dependency>
  <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>${mysql.version}</version>
    <scope>runtime</scope>
  </dependency>
  <dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>${mybatis.version}</version>
  </dependency>
  <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
  <dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-ui</artifactId>
    <version>1.6.9</version>
  </dependency>
  <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.1.18</version>
  </dependency>

  <dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct</artifactId>
    <version>${org.mapstruct.version}</version>
  </dependency>
  <dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct-processor</artifactId>
    <version>${org.mapstruct.version}</version>
  </dependency>
</dependencies>

<build>
  <plugins>
    <plugin>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-maven-plugin</artifactId>
    </plugin>
  </plugins>
</build>

有些依赖不一定是最新版本,而且你看到这篇文章时,可能已经发布了新版本,到时候可以先模仿着将项目跑起来后,再根据自己的需求来升级各项依赖,有问题咱再解决问题。

分页处理

某些业务场景是需要分页查询和排序功能的,所以我们需要考虑前端如何传递参数给后端,后端如何进行分页查询或者是排序查询。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<OrderInfo> 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<Customer> customerPage = customerRepository.findAll(pageable);

关于 PageHelper 的使用这里就不多做介绍了。

我们得到的分页查询结果是 Page 对象,可以直接使用,也可以根据需要进行修改,比如下面这个文件:

@Getter
@Setter
public class PageResult<T> {

  /**
   * 总条数
   */
  private Long total;
  /**
   * 总页数
   */
  private Integer pageCount;
  /**
   * 每页数量
   */
  private Integer pageSize;
  /**
   * 当前页码
   */
  private Integer pageNum;

  /**
   * 分页数据
   */
  private List<T> data;

  /**
   * 处理Jpa分页结果,Jpa页码默认从0开始,所以返回结果加1
   */
  public static <T> PageResult<T> ok(org.springframework.data.domain.Page<T> page) {
    PageResult<T> result = new PageResult<T>();
    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<String> {

  @Override
  public Optional<String> 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<T>接口和元模型就实现动态查询,相较于这篇文章,本文实现借鉴了 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<QueryItem> 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<Object> iterable;

  private Operator operator;
}

6、集成工具类

JPA动态查询工具类

因代码篇幅受限,就只粘贴部分代码:

public SpecificationBuilder<T> andEqualTo(IFn<T, Object> 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<QueryParam> 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<CustomerVO> 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<Customer> 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,否则插入数据时会报错。

执行效果如下:

sql查询结果

分页查询

前端参数传递:

{
	"pageNum": 1,
	"pageSize": 5,
	"orderInfos":[
		{
			"column": "name",
			"asc": true
		}
	]
}

后端代码处理:

  public Page<CustomerVO> queryPage(CustomerQueryPageDTO dto) {
    Pageable pageable = SpecificationBuilder.getPageable(dto.getPageSortInfo());
    Page<Customer> 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<CustomerVO> queryList(CustomerDTO dto) {
    List<Customer> 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<CustomerVO> queryPage(CustomerQueryPageDTO dto) {
    Pageable pageable = SpecificationBuilder.getPageable(dto.getPageSortInfo());
    Page<Customer> customerPage = SpecificationBuilder.create(CustomerRepository.class)
        .andLike(Customer::getName, dto.getName() + "%")
        .select(pageable);
//    Page<Customer> 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<SubUser> 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<Customer, Long>,
    JpaSpecificationExecutor<Customer> {

  @Override
  @EntityGraph(
      attributePaths = {"subUsers"}
  )
  List<Customer> findAll(Specification<Customer> spec);

  @Override
  @EntityGraph(
      attributePaths = {"subUsers"}
  )
  Page<Customer> findAll(Specification<Customer> spec, Pageable pageable);

  @Override
  @EntityGraph(
      attributePaths = {"subUsers"}
  )
  List<Customer> findAll(Specification<Customer> spec, Sort sort);
}

3、Service 层修改查询方法

  public List<CustomerVO> queryList(CustomerDTO dto) {
    List<Customer> 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<CustomerVO> queryList(CustomerDTO dto) {
    List<Customer> 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<CustomerVO> queryList(CustomerDTO dto) {
    List<Customer> 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<QueryParam> onQueryParams = queryParams.stream()
  .filter(obj -> StringUtils.isNotEmpty(obj.getJoinName()))
  .collect(Collectors.toList());
for (QueryParam onQueryParam : onQueryParams) {
  List<QueryItem> queryItems = onQueryParam.getQueryItems();
  if (CollectionUtils.isEmpty(queryItems)) {
    root.join(onQueryParam.getJoinName(), onQueryParam.getJoinType());
  } else {
    Join<Object, Object> 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<Customer, Long>,
    JpaSpecificationExecutor<Customer> {

  @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,页面展示如下:

swagger

总结

上文中本人对于实现的 SpecificationBuilder 类还算满意,一开始只是为了实现分页查询,慢慢想实现 Mybatis Plus 的那种链式查询,以及最后想要实现连接查询。代码实现目前就这样吧,如果大家有什么好玩的想法,欢迎大家留言评论。

感兴趣的朋友可以去我的 Github 下载相关代码,如果对你有所帮助,不妨 Star 一下,谢谢大家支持!

参考文献

Spring Data JPA中多表联合查询最佳实践

spring data jpa 动态查询(工具类封装)

springboot jpa 分页查询工具类

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/32619.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

HikariCP源码阅读笔记

加入HikariCP的maven依赖 <dependency><groupId>com.zaxxer</groupId><artifactId>HikariCP</artifactId><version>4.0.3</version> </dependency> <dependency><groupId>mysql</groupId><artifactId&g…

[附源码]java毕业设计疫情背景下社区公共卫生服务系统

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

10月BIOTREE协助发表文章再创新高,最高影响因子31.373

10月&#xff0c;BIOTREE在代谢组学、蛋白质组学、多组学方向共收录24篇客户文章&#xff0c;总影响因子&#xff1a;183.749&#xff0c;最高影响因子&#xff1a;31.373&#xff0c;IF>5分有19篇&#xff0c;发表杂志有Cell Metabolism、Clinical Immunology、Cell and Bi…

web网页设计期末课程大作业 HTML+CSS+JavaScript重庆火锅(代码质量好)

&#x1f380; 精彩专栏推荐&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb; ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 &#x1f482; 作者主页: 【主页——&#x1f680;获取更多优质源码】 &#x1f393; web前端期末大作业…

5、设计模式之工厂模式

工厂模式5.1 案例引入5.2简单工厂模式5.2.1 简单工厂模式结构5.2.2 实现5.2.3 优缺点分析5.3 工厂方法模式5.3.1 工厂方法模式结构5.3.2 新需求5.3.3 思路5.3.4 实现5.3.5 优缺点分析5.4 抽象工厂5.4.1 介绍5.4.2 结构5.4.3 实现5.4.4 使用场景5.5 工厂配置文件实现解耦相关文章…

【Vue】pc和移动端网页样式适配

在下面环节会讲解怎么做pc和移动端网页样式适配。 在当下有两种实现样式适配的&#xff1a;JS 适配方案和CSS 媒体查询适配。下面会具体讲解一下代码该怎么写。 &#x1f64f; 希望该文章能帮助到你。 1. JS 适配方案 比如在src/router/index.vue文件中有一个统一的Layout组件包…

《机器学习实战》9.树回归

目录 树回归 1 复杂数据的局部性建模 2 连续和离散型特征的树的构建 3 将CART算法用于回归 3.1 构建树 3.2 运行代码 4 树剪枝 4.1 预剪枝 4.2 后剪枝 5 模型树 6 示例&#xff1a;树回归与标准回归的比较 7 使用python的Tkinter库创建GUI 7.1 用Tkinter创建GUI …

UG旋钮盖模具设计与加工

文章目录三维造型设计创建草图旋转拉伸拉伸求差圆周阵列倒圆抽壳二板模模具设计分型面初始化项目定义模具坐标定义成形镶件定义一模四腔的布局创建型芯和型腔检测区域创建区域和分型线创建分型面创建型芯和型腔加载模架创建模架添加标准件添加定位圈添加浇口套添加旋钮盖顶杆修…

【JavaScript】浏览器调试控制台console的功能有了解多少

在我们学会JavaScript编程以后&#xff0c;编程调试通常使用console.log() &#xff0c;那有没有想过&#xff0c;console除了log&#xff0c;还有很多是没有用到的&#xff0c;久而久之&#xff0c;就忘记了怎么用&#xff0c;接下来熟悉一下&#xff0c;温故而知新&#xff0…

网页大作业代码自取

&#x1f468;‍&#x1f393;静态网站的编写主要是用HTML DIVCSS JS等来完成页面的排版设计&#x1f469;‍&#x1f393;,常用的网页设计软件有Dreamweaver、EditPlus、HBuilderX、VScode 、Webstorm、Animate等等&#xff0c;用的最多的还是DW&#xff0c;当然不同软件写出的…

Red Eye Camera开发日记之API 移植I2C 和关键接口函数

API 说明文件里面有官方的移植指导&#xff0c;但我觉得可以把重点放在与 MLX90640 具体操作有关的几个函数上&#xff0c;而与标准 I2C 相关的函数和文件结构还是按照自己习惯的套路实现。这样更符合我们开发人员的可控性的习惯。步骤如下&#xff1a; 建立标准 I2C 文件 IIC…

第P9周:YOLOv5-Backbone模块实现

● &#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客 ● &#x1f366; 参考文章&#xff1a;Pytorch实战 | 第P9周&#xff1a;YOLOv5-Backbone模块实现(训练营内部成员可读) ● &#x1f356; 原作者&#xff1a;K同学啊|接辅导、项目定制 说明&…

电脑技巧:推荐4个冷门且功能非常强大的效率工具

目录 1、Seafile 企业云盘神器 2、PPSSPP PSP模拟器神器 3、按键精灵 键盘鼠标模拟神器 4、BenVista PhotoZoom Pro 图片无损放大神器 今天给大家推荐4个冷门且功能非常强大的效率工具&#xff0c;一起来看看吧&#xff01; 1、Seafile 企业云盘神器 Seafile 是一个开源的网…

middlebury立体匹配评估使用方法总结(二)——python版离线教程

系列文章目录 middlebury立体匹配评估使用方法总结&#xff08;一&#xff09;——网站说明 middlebury立体匹配评估使用方法总结&#xff08;二&#xff09;——python版离线教程 文章目录系列文章目录前言1.下载内容2.配置环境3.elas编译4. 运行评估总结前言 最近想使用midd…

vim配置systemverilog环境

参考文章: vim中systemverilog的高亮显示 - 乔_木 - 博客园vim中systemverilog的高亮显示https://www.cnblogs.com/OneFri/p/6087847.html 一、.vimrc配置文件中添加 filetype on "打开文件类型检测 二、创建运行目录 mkdir -p ~/.vim/ftdetect mkdir -p ~/.vim/synta…

Python基础知识入门(一)

Python基础知识入门&#xff08;二&#xff09; 一、发展历程 Python的创始人为荷兰人吉多范罗苏姆&#xff08;Guido van Rossum&#xff09;。1989年圣诞节期间&#xff0c;在阿姆斯特丹&#xff0c;Guido为了打发圣诞节的无趣&#xff0c;决心开发一个新的脚本解释程序&…

第五章《类的继承》第2节:子类对象的构建过程

5.1小节讲解了类的继承机制。通过学习5.1小节的内容可知:子类能够继承父类的属性和方法,在此基础上,子类常常会扩展出属于自身特有的属性和方法,因此子类对象中的属性和方法可以分为两部分:从父类中继承而来的部分和自身所定义的部分。图5-3展示了Person和Student父子两个…

C++中的继承(上)

&#x1f9f8;&#x1f9f8;&#x1f9f8;各位大佬大家好&#xff0c;我是猪皮兄弟&#x1f9f8;&#x1f9f8;&#x1f9f8; 文章目录一、继承概述二、继承方式与访问限定符三、继承中的作用域四、基类和派生类对象赋值兼容转换五、继承当中默认成员函数的问题1.构造函数2.拷…

我做整车疲劳耐久分析9年积累的22条建模经验

作者&#xff1a;孙老师 对于承受交变应力的设备&#xff0c;疲劳分析在有限元分析占有重要地位。在进行疲劳分析时&#xff0c;有限元模型的正确搭建又至关重要。我们都知道&#xff0c;模型的质量直接影响分析的精度&#xff0c;所以好的建模方式是疲劳分析的第一步&#xf…

OpenGL原理与实践——核心模式(一):VBO、VAO等原理解析及项目初始设置

目录 序言——OpenGL在是什么&#xff1f;为什么&#xff1f;做什么? OpenGL实现了什么 OpenGL内模型数据的本质——顶点数据 我们需要研究什么——三角形&#xff0c;一个图形基元 MVP变换 OpenGL渲染流程的关键——摄像机变换 OpenGL渲染管线概览 准备——项目配置…