문제

I need to construct an intersect-type query with the following entities (reduced down for clarity).

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

A member can belong to one or more programs, and he always has his program level, connected by this:

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

Example: I have three programs, Math, English, Science. Each of them has some levels, like, MAth has algebra, geometry, English has literature, spelling and grammar, Science has experiments and theory.

Also, example user Joe would have Math:algebra, English: grammar programs and levels. Example user Max would maybe have English:literature. So, member can have multiple programs, but only one level per program.

Now I need to count or fetch all members that match a few programs and some levels in them. Example: I want all users that have Math:algebra or geometry, and English:literature or grammar and Science: theory.

I'm not really into JPA stuff, so I'm getting stuck.

In SQL, I'd do an intersect. How would I do it with JPA?

I have something like this:

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()

}
...

How would I get this?

Besides doing this as multiple inner joins, I don't know if it can be done as INTERSECT (db is PostgreSQL if it matters)?

Also, I'm not sure where in that query I'd put the condition to only get me distinct memberships with these conditions.

As a bonus, I'll have to create an OR query. This one is where I must match ALL programs, the next one needs ANY program/level match for a member to be included there. But once I figure this one, I'll figure that one on my own, I hope.

EDIT: a possible (pseudo)SQL select would look like:

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).

Or something like this:

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 */
도움이 되었습니까?

해결책

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);

SQL equivalent for criteria API will be like this, (will return unique user count by each 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

If you don't want count by each programId just remove programId from group by and select clauses, and change OR to AND in where clause. I hope this will help you.

라이센스 : CC-BY-SA ~와 함께 속성
제휴하지 않습니다 StackOverflow
scroll top