首页 > 解决方案 > 动态 JPA 查询

问题描述

我有两个实体问题和用户答案。我需要在 spring boot 中创建一个 api,它根据某些条件返回两个实体的所有列。

条件是:

  1. 我将给出一个比较器,例如:>、<、=、>=、<=
  2. 列名,例如:last_answered_at、last_seen_at
  3. 上列的值 例如:28-09-2020 06:00:18

我将需要返回两个实体的内部连接并根据上述条件进行过滤。

基于上述条件的示例 sql 查询将如下所示:

SELECT q,ua from questions q INNER JOIN 
user_answers ua on q.id = ua.question_id 
WHERE ua.last_answered_at > 28-09-2020 06:00:18

我面临的问题是查询的列名和比较器需要是动态的。

有没有一种有效的方法可以使用spring boot和JPA来做到这一点,因为我不想为所有可能的列和运算符组合创建jpa查询方法,因为它可能是一个非常大的数字并且会广泛使用if else?

标签: springspring-boothibernatejpaspring-data-jpa

解决方案


这听起来像是存储库方法的清晰自定义实现。首先,我将对您的实体的实施做出一些假设。之后,我将介绍如何解决您的挑战的想法。

我假设实体看起来基本上是这样的(getter、setter、equals、hachCode...被忽略)。

@Entity
@Table(name = "questions")
public class Question {

    @Id
    @GeneratedValue
    private Long id;

    private LocalDateTime lastAnsweredAt;

    private LocalDateTime lastSeenAt;

    // other attributes you mentioned...

    @OneToMany(mappedBy = "question", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<UserAnswer> userAnswers = new ArrayList();

    // Add and remove methods added to keep bidirectional relationship synchronised
    public void addUserAnswer(UserAnswer userAnswer) {
        userAnswers.add(userAnswer);
        userAnswer.setQuestion(this);
    }

    public void removeUserAnswer(UserAnswer userAnswer) {
        userAnswers.remove(userAnswer);
        userAnswer.setQuestion(null);
    }
}


@Entity
@Table(name = "user_answers")
public class UserAnswer {

    @Id
    @GeneratedValue
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "task_release_id")
    private Question question;
}

我将使用 Hibernate 的 JPA 知识编写代码。对于其他 JPA,它的工作方式可能类似或相同。

Hibernate 通常需要将属性名称作为String. 为了规避未检测到的错误问题(尤其是在重构时),我建议使用该模块hibernate-jpamodelgen(请参阅带下划线后缀的类名)。您还可以使用它将属性名称作为参数传递给存储库方法。

存储库方法尝试与数据库通信。在 JPA 中,实现数据库请求的方式有多种:JPQL 作为查询语言和 Criteria API(更容易重构,不易出错)。由于我是 Criteria API 的粉丝,我将使用 Criteria API 和 modelgen 来告诉 ORM Hibernate 与数据库对话以检索相关对象。

public class QuestionRepositoryCustomImpl implements QuestionRepository {

    @PersistenceContext
    private EntityManager entityManager;

    @Override
    public List<Question> dynamicFind(String comparator, String attribute, String value) {

        CriteriaBuilder cb = entityManager.getCriteriaBuilder();
        CriteriaQuery<Question> cq = cb.createQuery(Question.class);
        // Root gets constructed for first, main class in the request (see return of method)
        Root<Question> root = cq.from(Question.class);
        // Join happens based on respective attribute within root
        root.join(Question_.USER_ANSWER);

        // The following ifs are not the nicest solution.
        // The ifs check what comparator String contains and adds respective where clause to query
        // This .where() is like WHERE in SQL
        if("==".equals(comparator)) {
            cq.where(cb.equal(root.get(attribute), value));
        }
        if(">".equals(comparator)) {
            cq.where(cb.gt(root.get(attribute), value));
        }
        if(">=".equals(comparator)) {
            cq.where(cb.ge(root.get(attribute), value));
        }
        if("<".equals(comparator)) {
            cq.where(cb.lt(root.get(attribute), value));
        }
        if("<=".equals(comparator)) {
            cq.where(cb.le(root.get(attribute), value));
        }

        // Finally, query gets created and result collected and returned as List
        // Hint for READ_ONLY is added as lists are often just for read and performance is better.
        return entityManager.createQuery(cq).setHint(QueryHints.READ_ONLY, true).getResultList();
    }
}

推荐阅读