【问题标题】:JPA Criteria API arbitrary number of joins/subqueriesJPA Criteria API 任意数量的连接/子查询
【发布时间】:2013-12-13 01:16:00
【问题描述】:

我需要用以下实体构造一个相交类型的查询(为清楚起见,减少了)。

@Entity // and other @ stuff
public class Member {
    @Id
    private Long id;
    private String name;
    ...
}

@Entity
public class Program {
     @Id
     private Long id;
     private Long programName;
     private List<ProgramLevel> levels;
     ...
}

@Entity
public class ProgramLevel {
    @Id
    private Long id
    private String levelName;
}

一个成员可以属于一个或多个程序,并且他总是有自己的程序级别,通过以下方式连接:

public class Membership {
    @Id
    private Long id;
    private Long memberId;     // this is the member
    private Long programId;    // in which program is he
    private Long programLevel; // and on what level
    ...
}

示例:我有三个课程,数学、英语、科学。他们每个人都有一些级别,比如,数学有代数、几何,英语有文学、拼写和语法,科学有实验和理论。

此外,示例用户 Joe 将拥有数学:代数、英语:语法程序和级别。示例用户 Max 可能有 English:literature。因此,会员可以拥有多个计划,但每个计划只有一个级别。

现在我需要计算或获取与一些程序和其中一些级别匹配的所有成员。 示例:我希望所有拥有数学:代数或几何、英语:文学或语法和科学:理论的用户。

我不是很喜欢 JPA 的东西,所以我被卡住了。

在 SQL 中,我会做一个相交。我将如何使用 JPA?

我有这样的事情:

HashMap<Long, List<ProgramLevel>> levels = new HashMap<Long, List<ProgramLevel>>();
// then I fetch the levels map.

CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Long> criteriaQuery = cb.createQuery(Long.class);
Root<Membership> root = query.from(Membership.class);

// this is the part where I'm stuck.

// I figured I could try with multiple inner joins?
for(Map.Entry<Long, List<ProgramLevel>> curentLevel: levels.entrySet()) {
    // cb.equal(root.join ??? what comes here?
    // what what what?
    // root.get("programId") = currentLevel.getKey()
    // AND root.get("programLevelId") IN currentLevel.getValue()

}
...

我怎么会得到这个?

除了作为多个内部连接之外,我不知道它是否可以作为 INTERSECT 来完成(如果重要,db 是 PostgreSQL)?

另外,我不确定我会在该查询中的哪个位置放置条件,以便仅在这些条件下让我获得不同的成员资格。

作为奖励,我必须创建一个 OR 查询。这个是我必须匹配所有程序的地方,下一个需要任何程序/级别匹配才能将成员包括在内。但是,一旦我想出了这个,我希望我会自己想出那个。

编辑:可能的(伪)SQL 选择如下所示:

SELECT count(membership) FROM membership m
  WHERE (m.program == :program1 and m.programLevel in :programLevels1ArrayOfLevels)
INTERSECT
SELECT count(membership) FROM membership m
  WHERE (m.program == :program2 and m.programLevel in :programLevels2ArrayOfLevels)
INTERSECT...
... /* this would go on for as many (Program, List<ProgramLevel>) pairs I have on input. Which is variable).

或者是这样的:

SELECT count(membership) FROM membership m
JOIN membership m1 ON (m1.program = :program1 AND m1.programLevel IN m1.programLevels1Aray)
JOIN membership m2 ON (m2.program = :program2 AND m2.programLevel IN m1.programLevels2Aray)
/* Continued to as many input pairs I have */

【问题讨论】:

  • 您通常会通过使用不同别名多次连接同一个表来做到这一点。 JPA 应该提供一种方法来做到这一点,但我很高兴大约一年来不必使用它或它的 Criteria API,所以我要为你指出大致正确的方向,然后尖叫着逃往山上。
  • 嗯。我将如何命名这些连接?因为我不知道他们有多少人。我不知道它是否显示,但 Java 是我的第二语言 :)
  • 您能否发布fiddle 或有关您想要的SQL 信息?您还需要多少个可能的连接。如果您只是改变一个值列表,您可以通过循环值列表并使用 Criteria API 按需创建连接来实现类似于this 的效果。这样您也可以按需使用别名(例如q1、q2、q3、qn)。
  • @AnthonyAccioly 我已经更新了问题,在那里添加了一些 SQL 以显示我想要的内容。

标签: java postgresql jpa join intersect


【解决方案1】:
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Tuple> criteriaQuery = cb.createQuery(Tuple.class);
Root<Membership> root = query.from(Membership.class);

Collection<Long> queredPrograms = levels.keySet()
Predicate[] queryPredicates = new Predicate[queredPrograms.size()];
int i = 0;
for(Long programId : queredPrograms) {
    queryPredicates[i++] = cb.and(
        cb.equal(root.get("programId"), programId),
        root.get("programLevel").in(levels.get(programId))
    );
}
criteriaQuery.where(
    cb.and(
        cb.or(queryPredicates),
        root.get("programId").in(queredPrograms)
    )
);

criteriaQuery.groupBy(root.get("programId"));
criteriaQuery.select(cb.tuple(cb.count(cb.distinct(root.get("memberId"))), root.get("programId")));
TypedQuery<Tuple> countSelection = entityManager.createQuery(criteriaQuery);

条件 API 的等效 SQL 将是这样的,(将返回每个 programId 的唯一用户数)

SELECT 
      COUNT(DISTINCT memberId),
      programId
FROM membership
WHERE 
      programId in (:PROGRAMS_ID_LIST)
      AND (
           (programId = :PROGRAM_ID AND programLevel IN (:PROGRAM_LEVEL_LIST))
           ...
           OR (programId = :PROGRAM_ID_N AND programLevel IN (:PROGRAM_N_LEVEL_LIST))
      )
GROUP BY programId

如果您不想按每个 programId 计数,只需从 group by 和 select 子句中删除 programId,然后在 where 子句中将 OR 更改为 AND。我希望这会对你有所帮助。

【讨论】:

    猜你喜欢
    • 2022-12-21
    • 2011-10-17
    • 1970-01-01
    • 1970-01-01
    • 2017-07-20
    • 1970-01-01
    • 2012-08-19
    • 2016-06-25
    相关资源
    最近更新 更多