jpa 사용 시 join 및 동적 쿼리에 따른 문제에 대한 해결 내용을 공유하고자 한다.
시작은 hibernate namig 전략을 이용해 entity와 repository를 맵핑하여 쿼리를 생성하는 방식을 사용하였다.
이는 적응하면 굉장히 편한 쿼리 방식이다. 다만 많은 join과 세밀한 조회가 필요 시 원하는 결과를 도출하기 어려울 수 있다.
일반적으로 알려진 문게가 n+1 쿼리 동작이다.
Lazy Loding으로 인해 join 테이블의 쿼리가 n번 요청되는 문제이다.
위 문제의 경우 검색해 보면 많은 해결 방식이 나와있다.

fetch type을 eager로 변경 - 이 경우는 하나의 테이블 데이터만 필요할 때에도 join된 모든 테이블의 데이터를 가져오게 되어
불필요한 자원이 소모가 된다. 또한 oneToMany의 경우 n번의 쿼리가 발생하는 경우가 생긴다.
join fetch 사용 - 여러개의 join테이블이 필요할때는 사용에 제약이 있다.
EntityGraph 사용 - 실제 entity graph를 사용하여 문제를 해결한 부분도 있다. 하지만 동적 쿼리도 가능하게 하기 위해 다른 방식을 사용하기로 한다.

위의 문제들을 해결하기 위해 criteria query를 이용해 처리를 하였다. 물론 native query를 사용해도 되지만 jpa를 사용하는 의미가 없어진다.

TBL1, TBL2, TBL3 테이블의 엔티티가 존재하고 TBL1 테이블에 TBL2, TBL3이 조인 되어야 한다고 가정하고 예제를 작성하였다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
@Repository
public class Query {
@PersistenceContext
EntityManager em;

public List<Dto> list(String param1, String param2, String sortCol, String sortDir)
throws Exception {
//@ INIT BUILDER
CriteriaBuilder builder = em.getCriteriaBuilder();
CriteriaQuery<Dto> qry = builder.createQuery(Dto.class);
Root<TBL1> root = qry.from(TBL1.class);

//@ JOIN
Join<TBL1, TBL2> joinTBL2 = root.join(TBL1_.tbl2, JoinType.LEFT);
Join<TBL1, TBL3> joinTBL3 = root.join(TBL1_.tbl3, JoinType.LEFT);
Join<TBL1, TBL4> joinTBL4 = root.join(TBL1_.tbl4, JoinType.LEFT);

//@ GROUP BY
qry.groupBy(joinTBL4.get(TBL4_.id));

//@ WHERE
//qry.where(builder.equal(joinTBL4.get(TBL4_.user), param1));
qry.having(
builder.like(
builder.function("group_concat", String.class, joinTBL4.get(TBL4_.user))
, "%" + param2 + "%"
)
);

//@ SELECT
//# using dto constructor
qry.select(builder.construct(Dto.class, joinTBL1.get(TBL1_.id), joinTBL2.get(TBL2_.id)));

//@ ORDER BY
Order order;
Path<String> path = joinTBL1.get(sortCol);

if (sortDir.toUpperCase().equals("DESC")) order = builder.desc(path);
else order = builder.asc(path);

qry.orderBy(order);

//@ ENTITY GRAPH(by none group by)
//EntityGraph<TBL1> graph = em.createEntityGraph(TBL1.class);
//graph.addAttributeNodes("tbl2", "tbl3", "tbl4");

//@ QUERY
TypedQuery<Dto> query = em.createQuery(qry);
query
.setFirstResult(0)
.setMaxResults(10);

//# EntityGraph 사용 시 힌트 추가
//.setHint("javax.persistence.loadgraph", graph)

return query.getResultList();
}

}

먼저 CriteriaBuilder객체, 쿼리 생성 객체, from clause 객체를 생성한다.

1
2
3
CriteriaBuilder builder = em.getCriteriaBuilder();
CriteriaQuery<TBL1> qry = builder.createQuery(TBL1.class);
Root<TBL1> root = qry.from(TBL1.class);

쿼리 생성 객체의 제너럴 타입은 엔티티가 아닌 DTO나 MAP 등 조회 데이터에 맵핑 될 수 있는 타입일 수 있다.

JOIN

1
2
Join<TBL1, TBL2> joinTBL2 = root.join(TBL1_.tbl2, JoinType.LEFT);
Join<TBL1, TBL3> joinTBL3 = root.join(TBL1_.tbl3, JoinType.LEFT);

위의 언더바(_)는 jpa model generator를 통해 생성된 모델 객체이다. 검색해 보시길…
모델을 사용하지 않는 경우 TBL1_tbl2 대신 “tbl2” 와 같이 String으로 사용 가능하다.

GROUP BY

1
qry.groupBy(joinTBL3.get(TBL3_.id));

WHERE

1
qry.where(builder.equal(joinTBL2.get(TBL2_.Id), param1));

HAVING

1
qry.having(builder.like(joinTbl1.get(TBL1_.name), param2));

function이 필요한 경우.
avg, sum, max, min 등의 function은 builder에서 기본 제공.
특정 db의 function이 필요한 경우 MetadataBuilderContributor를 통해 등록 후 아래와 같이 사용 가능.

1
2
3
4
5
6
qry.having(
builder.like(
builder.function("group_concat", String.class, joinTBL1.get(TBL1_.email)),
"%" + clientId + "%"
)
);

SELECT

1
qry.select(joinTBL1.get(TBL1_.id));

dto construct를 이용하는 경우.

1
qry.select(builder.construct(Dto.class, cols));

ORDER

1
2
3
4
5
6
7
Order order;
Path<String> path = joinTBL2.get(sortCol);

if (sortDir.toUpperCase().equals("DESC")) order = builder.desc(path);
else order = builder.asc(path);

qry.orderBy(order);