动态JPA查询

问题描述

我有两个实体Questions和UserAnswers。我需要在Spring Boot中创建一个api,该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还有吗?

解决方法

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

我假设实体看起来基本上是这样的(getters,setters,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,它可能相似或相同。

休眠通常需要将属性名称用作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();
    }
}
,

我开发了一个名为spring-dynamic-jpa的库,以使使用JPA实施动态查询更加容易。

您可以使用它来编写查询模板。调用该方法时,根据您的参数,在执行之前,查询模板将内置到不同的查询字符串中。