春季数据JPA自定义存储库
#编程 #java #spring #data

介绍

Spring Data JPA提供JPAREPOSITORITIROTIRY接口,该接口提供CRUD/LIST/PAGIG/STAGING/SOTINGING功能。然后,查询方法可以通过:

定义
  1. 查询直接从方法名称得出。例如
public List<Customer> findTop5ByStatusOrderByDateOfBirthAsc( 
                         Customer.Status status);
  1. 手动查询。可以使用@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允许创建具有自定义功能的存储库。实现这一目标的步骤是:

  1. 要做的第一件事是用自定义回购的特定方法定义片段接口。
  2. 通过提供方法功能来实现接口。
  3. 实体的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);
}

下一步是实现接口。这里有两件事要考虑:

  1. 实现接口的类的名称必须是片段接口名称,然后是Impl postfix(可以使用注释@enablejparepositories更改后三个)

  2. 实现不受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相关的新内容将有点发布。希望您喜欢阅读。