Spring Data JPA 自定义存储库

举报
千锋教育 发表于 2023/07/10 12:55:45 2023/07/10
【摘要】 介绍Spring Data JPA 提供了 JpaRepository 接口,该接口提供 CRUD/List/Paging/Sorting 功能。然后,可以通过以下方式定义查询方法:直接从方法名称派生的查询。例如public List<Customer> findTop5ByStatusOrderByDateOfBirthAsc( Custo...

Spring Data JPA 自定义存储库.jpg

介绍

Spring Data JPA 提供了 JpaRepository 接口,该接口提供 CRUD/List/Paging/Sorting 功能。然后,可以通过以下方式定义查询方法:

  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 注解不适合动态查询。例如,如果需要检查许多字段是否为空,我们最终可能会遇到性能问题。

在这些情况下,我们需要为存储库方法编写自定义实现。

自定义存储库

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;
...
}

客户详细信息是与客户的双向一对一关系的一部分。它添加了更多字段并与父实体分离。如果客户需要进行大量写入操作,则此模型将具有更好的扩展性。

现在,让我们编写客户存储库,以便我们可以完全控制新方法。我们的界面将有一种方法根据所有字段(包括 CustomerDetails 字段)搜索客户。

public interface CustomizedCustomerRepository {
    public List<Customer> findByAllFields(Customer customer);
}

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

  1. 实现接口的类名必须是fragment接口名后跟Impl postfix(可以使用注解@EnableJpaRepositories更改Postfix)

  2. 该实现不绑定到 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站搜索千锋教育

【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。