介绍
Spring Data JPA提供JPAREPOSITORITIROTIRY接口,该接口提供CRUD/LIST/PAGIG/STAGING/SOTINGING功能。然后,查询方法可以通过:
定义- 查询直接从方法名称得出。例如
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注释不适合dinamic查询。如果需要检查许多字段,我们可能会出现性能问题。
在这些情况下,我们需要为存储库方法编写自定义实现。
定制存储库
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;
...
}
客户详细信息是与客户一对一关系的双向关系的一部分。它增加了几个字段,并与父实体分开。如果客户接受高写操作,则该模型将更好地扩展。
现在,让我们编写客户存储库,以便我们可以完全控制新方法。我们的界面将有一种基于所有字段搜索客户的方法
public interface CustomizedCustomerRepository {
public List<Customer> findByAllFields(Customer customer);
}
下一步是实现接口。这里有两件事要考虑:
-
实现接口的类的名称必须是片段接口名称,然后是Impl postfix(可以使用注释@enablejparepositories更改后三个)
-
实现不受Spring JPA的限制。这具有直接使用EntityManager或Spring JDBCtemplate的能力。甚至委派到第三方图书馆。
在我们的情况下,将使用JPA标准API来构建dinamic查询并按名称,状态,VIP和信息字段进行过滤。标准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]);
}
}
让我们看一下上述代码。首先,注入实体管理器,因为我们将使用标准API。然后,安排构造查询对象的必要对象。
线
Root<Customer> root = query.from(Customer.class);
为从子句创建类型客户的根源。
线
query.select(root).where(buildPredicates(customer,cb,root));
设置了选择语句和Where子句的根。在build -predicates中生成的Where子句返回类型谓词数组。这是其中方法的输入param类型(Varagrs与数组兼容)。
最后,线
return entityManager.createQuery(query).getResultList();
执行查询并将结果返回为列表。请注意,既不需要铸造也不需要映射,因为JPA知道实体的类型。
关于构建方法的几个评论。它在线构建了连接fecth
var details = (Join<Object, Object>) root.fetch("details");
JOIN FETCH会将CustomerDetails字段添加到Select。它还通过加入两个表(即使一对一的关联是懒惰)来防止N + 1查询问题。
。然后,如果相应的字段不是空的,则将每个谓词添加到列表中。最终,列表转换为数组。
最后一步是将单个存储库从自定义扩展。
public interface CustomerRepo extends
JpaRepository<Customer, Long> , CustomizedCustomerRepository {
...
}
现在就完成了。让我们从控制器层调用该方法。
调用该方法
我们将调用CustomerController的方法。四个字段中的任何一个都可以通过获取请求参数传递到该方法中。该代码显示在以下摘要中
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子句的一部分。客户对象是通过使用Fluent API的构建器图案创建的。搜索结果将转换为类型记录的DTO对象自定义范围。这样,发送给客户端的数据的表示形式可以具有自己的结构,而不是与实体模型相关的。
是时候进行几个测试了。首先,没有参数的get请求
http://localhost:8080/api/v1/customers
生成的SQL代码选择实体的所有列,并在共享主键上连接。没有附加条款的地方。
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将有三个参数
http://localhost:8080/api/v1/customers?vip=true&status=activated&name =%name%
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提供的选项还不够时。
本文中使用的代码可以在github存储库链接的here
中找到本周全部。与Spring和Java相关的新内容将有点发布。希望您喜欢阅读。