Spring Data JPA 自定义存储库
介绍
Spring Data JPA 提供了 JpaRepository 接口,该接口提供 CRUD/List/Paging/Sorting 功能。然后,可以通过以下方式定义查询方法:
- 直接从方法名称派生的查询。例如
public List<Customer> findTop5ByStatusOrderByDateOfBirthAsc(
Customer.Status status);
- 手动定义查询。这可以使用 @Query 注释来完成
@Query("""
SELECT c FROM Customer c
WHERE (c.status = :status or :status is null)
and (c.name like :name or :name is null) """)
public List<Customer> findCustomerByStatusAndName(
@Param("status") Customer.Status status,
@Param("name") String name);
新的 Java 文本块功能提高了可读性,并且代码看起来非常干净。
然而,在某些情况下,上述选项都不符合我们的需求。例如,如果方法名称涉及很多字段,则方法名称可能会变得不可读。或者,如果查询是基于某些条件构建的,则每个组合都必须使用多个方法名称。另一方面,@Query 注解不适合动态查询。例如,如果需要检查许多字段是否为空,我们最终可能会遇到性能问题。
在这些情况下,我们需要为存储库方法编写自定义实现。
自定义存储库
Spring 允许创建具有自定义功能的存储库。实现这一目标的步骤是:
- 首先要做的是使用自定义存储库的特定方法定义片段接口。
- 通过提供方法功能来实现接口。
- 实体的 JPA 存储库接口必须扩展自定义接口。
案例研究:
假设我们有两个相关实体,如下所示
@Entity
public class Customer {
@Id
@SequenceGenerator(name = "customer_id_sequence", sequenceName
= "customer_id_sequence", allocationSize = 1)
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator
= "customer_id_sequence")
private Long id;
private String name;
private String email;
private LocalDate dateOfBirth;
@OneToOne(mappedBy = "customer", optional = false ,fetch =
FetchType.LAZY, cascade = CascadeType.ALL)
@PrimaryKeyJoinColumn()
private CustomerDetails details;
public CustomerDetails getDetails() {
return details;
}
public void setDetails(CustomerDetails details) {
this.details = details;
details.setCustomer(this);
}
...
}
客户类已在之前的文章中使用过,它只包含几个成员。
@Table(name = "CUSTOMER_DETAILS")
@Entity(name = "CustomerDetails")
public class CustomerDetails {
@Id
@Column(name = "customer_id")
private Long id;
@Column
private boolean vip;
@Lob
@Column
private String info;
@Column
private LocalDate createdOn;
@OneToOne(fetch = FetchType.LAZY)
@MapsId
@JoinColumn(name = "customer_id" )
@JsonIgnore
private Customer customer;
...
}
客户详细信息是与客户的双向一对一关系的一部分。它添加了更多字段并与父实体分离。如果客户需要进行大量写入操作,则此模型将具有更好的扩展性。
现在,让我们编写客户存储库,以便我们可以完全控制新方法。我们的界面将有一种方法根据所有字段(包括 CustomerDetails 字段)搜索客户。
public interface CustomizedCustomerRepository {
public List<Customer> findByAllFields(Customer customer);
}
下一步是实现该接口。这里有两件事需要考虑:
实现接口的类名必须是fragment接口名后跟Impl postfix(可以使用注解@EnableJpaRepositories更改Postfix)
该实现不绑定到 Spring JPA。这使得能够直接使用 EntityManager 或 Spring JdbcTemplate。甚至委托给第三方库。
在我们的例子中,将使用 JPA Criteria API 构建动态查询并按名称、状态、vip 和信息字段进行过滤。Criteria API 允许以编程方式创建查询。
代码如下所示
public class CustomizedCustomerRepositoryImpl implements
CustomizedCustomerRepository{
@PersistenceContext
private EntityManager entityManager;
public List<Customer> findByAllFields(Customer customer) {
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Customer> query =
cb.createQuery(Customer.class);
Root<Customer> root = query.from(Customer.class);
query.select(root).where(buildPredicates(customer,cb,
root));
return entityManager.createQuery(query).getResultList();
}
private Predicate[] buildPredicates(Customer customer,
CriteriaBuilder cb, Root<Customer> root) {
List<Predicate> predicates = new ArrayList<>();
var details = (Join<Object, Object>)root.fetch("details");
if (Objects.nonNull(customer.getName()))
predicates.add(cb.like(root.get("name"),
"%"+customer.getName()+"%"));
if (Objects.nonNull(customer.getStatus()))
predicates.add(cb.equal(root.get("status"),
customer.getStatus()));
if (Objects.nonNull(customer.getDetails().getInfo()))
predicates.add(cb.equal(details.get("info"),
customer.getDetails().getInfo()));
if (Objects.nonNull(customer.getDetails().isVip()))
predicates.add(cb.equal(details.get("vip"),
customer.getDetails().isVip()));
return predicates.toArray(new Predicate[0]);
}
}
让我们看一下上面代码中的一些行。首先注入实体管理器,因为我们将使用 Criteria API。然后,实例化构造查询对象所需的对象。
线
Root<Customer> root = query.from(Customer.class);
为 from 子句创建 Customer 类型的根。
线
query.select(root).where(buildPredicates(customer,cb,root));
设置 select 语句和 where 子句的根。where 子句在 buildPredicates 中生成,返回 Predicate 类型的数组。这是where方法的输入参数类型(varAgrs与数组兼容)。
最后,线
return entityManager.createQuery(query).getResultList();
执行查询并以列表形式返回结果。请注意,既不需要转换也不需要映射,因为 JPA 知道实体的类型。
关于 buildPredicates 方法的一些评论。它构建了一个在线连接
var details = (Join<Object, Object>) root.fetch("details");
连接获取会将 CustomerDetails 字段添加到选择中。它还通过连接两个表来防止 N + 1 查询问题(即使一对一关联是惰性的)。
然后,如果相应字段不为空,则将每个谓词添加到列表中。最终,列表被转换为数组。
最后一步是从自定义扩展单个存储库。
public interface CustomerRepo extends
JpaRepository<Customer, Long> , CustomizedCustomerRepository {
...
}
现在一切都完成了。让我们从控制器层调用该方法。
调用方法
我们将从 CustomerController 调用该方法。四个字段中的任何一个都可以通过 GET 请求参数传递到方法中。代码显示在以下片段中
public class CustomerController {
...
@GetMapping
public List<CustomerResponse> findCustomers(
@RequestParam(name="name", required=false) String name,
@RequestParam(name="status", required=false)
Customer.Status status,
@RequestParam(name="info", required=false) String info,
@RequestParam(name="vip", required=false) Boolean vip) {
return customerRepo
.findByAllFields(Customer.Builder
.newCustomer()
.name(name)
.status(status)
.withDetails(info, Boolean.valueOf(vip))
.build())
.stream()
.map(CustomerUtils::convertToCustomerResponse)
.collect(Collectors.toList());
}
...
所有参数都是可选的。如果他们没有被告知,他们将不会成为 where 子句的一部分。客户对象是通过 Builder 模式和 Fluent api 创建的。搜索结果将转换为记录类型的 DTO 对象 CustomerResponse。这样,发送回客户端的数据表示可以具有自己的结构,并且不依赖于实体模型。
是时候进行一些测试了。首先,一个不带参数的 GET 请求
生成的 SQL 代码选择两个实体的所有列并连接到共享主键。没有附加 where 子句。
select c1_0.id,c1_0.date_of_birth,d1_0.customer_id,
d1_0.created_on,d1_0.info,d1_0.vip,c1_0.email,
c1_0.name,c1_0.status
from customer c1_0
join customer_details d1_0 on c1_0.id=d1_0.customer_id
输出返回所有客户及其客户详细信息。DTO 对象只是一个具有两个嵌套记录组件的记录。
[
{
"id": 1,
"status": "DEACTIVATED",
"personInfo": {
"name": "name 1 surname 1",
"email": "organisation1@email.com",
"dateOfBirth": "03/01/1982"
},
"detailsInfo": {
"info": "Customer info details 1",
"vip": false
}
},
...
{
"id": 9,
"status": "ACTIVATED",
"personInfo": {
"name": "name 9 surname 9",
"email": "organisation9@email.com",
"dateOfBirth": "27/09/1998"
},
"detailsInfo": {
"info": "Customer info details 9",
"vip": false
}
}
]
第二个 URL 将具有三个参数
SQL 代码包含 where 子句以及输入参数的预期过滤器。下面的日志条目对此进行了演示
select c1_0.id,c1_0.date_of_birth,d1_0.customer_id,
d1_0.created_on,d1_0.info,d1_0.vip,c1_0.email,
c1_0.name,c1_0.status
from customer c1_0
join customer_details d1_0 on c1_0.id=d1_0.customer_id
where c1_0.name like ? escape '' and c1_0.status=? and d1_0.vip=?
binding parameter [1] as [VARCHAR] - [%name%]
binding parameter [2] as [INTEGER] - [1]
binding parameter [3] as [BOOLEAN] - [true]
从控制器发回的输出包含与过滤器匹配的唯一客户
[
{
"id": 6,
"status": "ACTIVATED",
"personInfo": {
"name": "name 6 surname 6",
"email": "organisation6@email.com",
"dateOfBirth": "18/06/1992"
},
"detailsInfo": {
"info": "Customer info details 6",
"vip": true
}
}
]
结论
本文介绍了如何在 Spring Data 中创建自定义的单独存储库。当我们需要用自定义功能丰富存储库并且 JpaRepository 提供的选项还不够时,这会派上用场。
更多精彩内容欢迎B站搜索千锋教育
- 点赞
- 收藏
- 关注作者
评论(0)