JPA 에서 union 사용하기
JPA 환경에서 집합 연산자(UNION, UNION ALL, INTERSECT, EXCEPT)를 사용할 수 있을까요? JPA의 버전이 3.1까지 올라왔어도 아직 이에대한 명세, 지원은 없는 것으로 보입니다. native sql로 작성하면 안되는 쿼리는 없긴 합니다만, 추상화된 SQL을 사용할 수 없다는 단점이 존재합니다.
Hibernate가 이에 대한 해답이 될 수 있는데요, Hibernate6 버전부터는 집합 연산자들을 지원하기 시작했으며 QueryDSL은 JPASQLQuery
클래스를 사용하면 일반적인 native sql보다는 type-safe 하게 쿼리 작성이 가능합니다. 이 포스팅에서는 간단한 예제로 JPA 환경에서 UNION
을 사용하는 방법에 대해 알아보고자 합니다.
0. 문제 확인 & 예제 세팅
Order
라는 파티션 테이블이 존재해야 하는데 모종의 이유로 파티션 테이블을 사용하지 못하고 주기적으로 테이블을 직접 분리해야 하는 상황이라고 가정합니다.
요청받은 데이터의 조건은 다음과 같습니다.
- Order 테이블을 상속하는 모든 테이블의 주문을 합쳐 주세요.
- 22년 6월 데이터는 받는분의 성씨가 ‘김’씨인 주문만 조회해 주세요.
- 23년 1월 데이터는 상품가격이 10만원 이상인 주문만 조회해 주세요.
- 합쳐진 주문에서 상태값이
FAILED
인 주문은 제외해 주세요.
이를 sql로 표현하면 다음과 같이 간단히 표현할 수 있습니다. (PostgreSQL 기준)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
select o.*
from (
select *
from order_2206
where order_2206.toname like '김%'
union all
select *
from order_2209
union all
select *
from order_2301
where order_2301.itemprice >= 100000
) as o
where status <> 'FAILED'
FROM 절에 서브쿼리를 써야하고 그 서브쿼리는 UNION ALL 을 사용합니다. 이제 위 sql문을 JPA로 옮겨보려고 합니다.
다음은 테스트에 사용될 라이브러리 & 엔티티 입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
dependencies {
implementation group: 'org.postgresql', name: 'postgresql', version: '42.5.1'
implementation 'org.hibernate.orm:hibernate-core:6.2.0.Final'
implementation group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.19.0'
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
implementation 'com.querydsl:querydsl-sql:5.0.0'
annotationProcessor 'com.querydsl:querydsl-apt:5.0.0:jakarta'
annotationProcessor 'jakarta.persistence:jakarta.persistence-api:3.1.0'
implementation 'org.projectlombok:lombok:1.18.24'
annotationProcessor 'org.projectlombok:lombok:1.18.24'
testImplementation 'org.springframework.boot:spring-boot-starter-test:3.0.1'
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1'
}
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
@Getter
@Setter
@MappedSuperclass
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
Long id;
@Enumerated(EnumType.STRING)
protected OrderStatus status;
protected String fromAddress;
protected String fromName;
protected String fromPhoneNumber;
protected String toAddress;
protected String toName;
protected String toPhoneNumber;
protected String itemName;
protected int itemPrice;
protected LocalDateTime createdDateTime;
}
@Entity
@Getter
public class Order_2206 extends Order {
// Order_2209 Order_2301 동일
}
1. Hibernate
Hibernate는 6 버전부터 집합 연산자들에 대한 지원을 시작하였습니다. 이로써 JPQL과 Criteria API 에서 UNION을 사용할 수 있게 되었습니다. 이게 가능한 이유는 SQM (Semantic Query Model)
때문인데요, SQM에 대해서는 이 문서를 읽어보시기 바랍니다.
Hibernate6 이전 버전들의 경우 Criteria API -> JPQL / HQL -> SQL 로 변환되었던 것에 비해 Hibernate6부터는 Criteria APi와 JPQL / HQL이 모두 SQM으로 컴파일되고 최종적으로 SQM이 SQL로 변환된다고 하네요. 쿼리 모델을 통합 함으로써 Hibernate Criteria Query가 JPA (JPQL) 스펙에서 지원하지 않는 기능들을 지원할 수 있게 되었다고 합니다. 그중 하나가 UNION인 셈이죠.
1-1. HQL
첫번째는 HQL 입니다. 이제 FROM 절에서의 서브쿼리와 UNION을 사용할 수 있기 때문에 alias만 잘 지켜서 정직하게 sql을 작성하면 됩니다.
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
@Test
@DisplayName("Hibernate HQL UNION ALL")
void hibernateHQL() {
// o.id AS id, o.status AS status, o.fromAddress AS fromAddress, o.fromName AS fromName, o.fromPhoneNumber AS fromPhoneNumber, o.toAddress AS toAddress, o.toName AS toName, o.toPhoneNumber AS toPhoneNumber, o.itemName AS itemName, o.itemPrice AS itemPrice, o.createdDateTime AS createdDateTime
var fieldsWithAlias = Arrays.stream(Order.class.getDeclaredFields()).map(i -> "o." + i.getName() + " AS " + i.getName()).collect(Collectors.joining(", "));
var query = String.format("""
SELECT %1$s
FROM (
SELECT %1$s
FROM Order_2206 AS o
WHERE o.toName LIKE :name
UNION ALL
SELECT %1$s
FROM Order_2209 AS o
UNION ALL
SELECT %1$s
FROM Order_2301 AS o
WHERE o.itemPrice >= :price
) AS o
WHERE o.status <> :status
""", fieldsWithAlias);
var list = entityManager
.createQuery(query)
.setParameter("name", "김%")
.setParameter("price", 100000)
.setParameter("status", OrderStatus.FAILED)
.getResultList();
System.out.println(list);
}
생성되는 쿼리는 다음과 같습니다.
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
60
61
62
63
64
65
66
67
68
[DEBUG] [main] SQL -
select
o4_0.id,
o4_0.status,
o4_0.fromAddress,
o4_0.fromName,
o4_0.fromPhoneNumber,
o4_0.toAddress,
o4_0.toName,
o4_0.toPhoneNumber,
o4_0.itemName,
o4_0.itemPrice,
o4_0.createdDateTime
from
(select
o1_0.id,
o1_0.status,
o1_0.fromAddress,
o1_0.fromName,
o1_0.fromPhoneNumber,
o1_0.toAddress,
o1_0.toName,
o1_0.toPhoneNumber,
o1_0.itemName,
o1_0.itemPrice,
o1_0.createdDateTime
from
Order_2206 o1_0
where
o1_0.toName like ? escape ''
union
all select
o2_0.id,
o2_0.status,
o2_0.fromAddress,
o2_0.fromName,
o2_0.fromPhoneNumber,
o2_0.toAddress,
o2_0.toName,
o2_0.toPhoneNumber,
o2_0.itemName,
o2_0.itemPrice,
o2_0.createdDateTime
from
Order_2209 o2_0
union
all select
o3_0.id,
o3_0.status,
o3_0.fromAddress,
o3_0.fromName,
o3_0.fromPhoneNumber,
o3_0.toAddress,
o3_0.toName,
o3_0.toPhoneNumber,
o3_0.itemName,
o3_0.itemPrice,
o3_0.createdDateTime
from
Order_2301 o3_0
where
o3_0.itemPrice>=?
) o4_0(id,status,fromAddress,fromName,fromPhoneNumber,toAddress,toName,toPhoneNumber,itemName,itemPrice,createdDateTime)
where
o4_0.status!=?
[TRACE] [main] bind - binding parameter [1] as [VARCHAR] - [김%]
[TRACE] [main] bind - binding parameter [2] as [INTEGER] - [100000]
[TRACE] [main] bind - binding parameter [3] as [VARCHAR] - [FAILED]
sql문이 잘 생성되는 것을 확인할 수 있습니다.
참고로 이전 버전의 Hibernate와 달리 select문에 별칭이 들어가지 않아 sql문이 조금 예뻐(?) 졌는데요, 이는 Hibernate6의 새로운 데이터 추출 방식과 관련이 있습니다. JDBC API가 제공하는 ResultSet으로 데이터를 추출하는 방법에는 read-by-name
과 read-by-position
이 있는데요, Hibernate6 이전 버전들에서는 read-by-name
을 사용해 왔지만 이제는 read-by-position
을 사용한다고 합니다. Hibernate 릴리즈 노트 과 JDBC driver maintainer의 글에 따르면 read-by-position
방식이 더 빠르다고 하네요.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-- 이전
select
o1_0.id as id_1_0_0_,
o1_0.status as status_1_0_0,
o1_0.fromAddress as from_address_1_0_0,
o1_0.fromName as from_name_1_0_0,
o1_0.fromPhoneNumber as from_phone_number_1_0_0,
-- 현재
select
o1_0.id,
o1_0.status,
o1_0.fromAddress,
o1_0.fromName,
o1_0.fromPhoneNumber,
1-2. Criteria API (Hibernate 6.2 ++)
이번에는 Criteria API를 이용하여 HQL보다 type-safe 하게 쿼리를 작성해 보겠습니다. 당연한 이야기지만 어디까지나 Hibernate의 기능들을 사용하는 것이기 때문에 일반 CriteiaBuilder
을 사용하면 안되고 HibernateCriteriaBuilder
을 사용해야 합니다.
Hibernate 6.2 이전 버전에서는 from 절의 서브쿼리에서 union all을 사용할 수 없습니다. 왜냐하면 HibernateCriteriaBuilder
인터페이스에 위 메소드가 존재하지 않기 때문인데요, 위 메소드가 존재함으로써 union all의 결과값으로 JPASubQuery
타입을 리턴받을 수 있고 from 절의 서브쿼리에서 union all 을 사용할 수 있게 되었습니다. Hibernate 6.2 버전부터는 with cte as ( ... )
구문을 사용할 수 있게 되었는데 아마 이에 영향을 받아 추가되지 않았나 싶습니다.
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
@Test
@DisplayName("Hibernate Criteria API UNION ALL")
void hibernateCriteriaAPI() {
Function<JpaRoot<?>, Selection<?>[]> rootPath = (root) -> Arrays.stream(Order.class.getDeclaredFields()).map(field -> root.get(field.getName()).alias(field.getName())).toArray(Selection[]::new);
var scq = hibernateCriteriaBuilder.createQuery(Order.class);
BiFunction<Class<? extends Order>, Function<JpaRoot<? extends Order>, Expression<Boolean>>, JpaSubQuery<Tuple>> jpaCriteriaQuery = (clazz, condition) -> {
var sq = scq.subquery(Tuple.class);
var root = sq.from(clazz);
sq.multiselect(rootPath.apply(root));
if (condition != null) {
sq.where(condition.apply(root));
}
return sq;
};
var sq1 = jpaCriteriaQuery.apply(Order_2206.class, (root) -> hibernateCriteriaBuilder.ilike(root.get("toName"), "김%"));
var sq2 = jpaCriteriaQuery.apply(Order_2209.class, null);
var sq3 = jpaCriteriaQuery.apply(Order_2301.class, (root) -> hibernateCriteriaBuilder.ge(root.get("itemPrice"), 100000));
var cq = hibernateCriteriaBuilder.createQuery(Order.class);
var root = cq.from(hibernateCriteriaBuilder.unionAll(sq1, sq2, sq3));
cq.multiselect(rootPath.apply(root));
cq.where(hibernateCriteriaBuilder.notEqual(root.get("status"), OrderStatus.FAILED));
var list = entityManager.createQuery(cq).getResultList();
System.out.println(list);
}
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
60
61
62
63
64
65
66
67
select
derived1_0.id,
derived1_0.status,
derived1_0.fromAddress,
derived1_0.fromName,
derived1_0.fromPhoneNumber,
derived1_0.toAddress,
derived1_0.toName,
derived1_0.toPhoneNumber,
derived1_0.itemName,
derived1_0.itemPrice,
derived1_0.createdDateTime
from
(select
o1_0.id,
o1_0.status,
o1_0.fromAddress,
o1_0.fromName,
o1_0.fromPhoneNumber,
o1_0.toAddress,
o1_0.toName,
o1_0.toPhoneNumber,
o1_0.itemName,
o1_0.itemPrice,
o1_0.createdDateTime
from
Order_2206 o1_0
where
o1_0.toName ilike ? escape ''
union
all select
o2_0.id,
o2_0.status,
o2_0.fromAddress,
o2_0.fromName,
o2_0.fromPhoneNumber,
o2_0.toAddress,
o2_0.toName,
o2_0.toPhoneNumber,
o2_0.itemName,
o2_0.itemPrice,
o2_0.createdDateTime
from
Order_2209 o2_0
union
all select
o3_0.id,
o3_0.status,
o3_0.fromAddress,
o3_0.fromName,
o3_0.fromPhoneNumber,
o3_0.toAddress,
o3_0.toName,
o3_0.toPhoneNumber,
o3_0.itemName,
o3_0.itemPrice,
o3_0.createdDateTime
from
Order_2301 o3_0
where
o3_0.itemPrice>=?
) derived1_0(id,status,fromAddress,fromName,fromPhoneNumber,toAddress,toName,toPhoneNumber,itemName,itemPrice,createdDateTime)
where
derived1_0.status!=?
[TRACE] [main] bind - binding parameter [1] as [VARCHAR] - [김%]
[TRACE] [main] bind - binding parameter [2] as [INTEGER] - [100000]
[TRACE] [main] bind - binding parameter [3] as [VARCHAR] - [FAILED]
HQL로 작성한 쿼리와는 테이블 별칭만 다를뿐 다른 부분은 일치하는 것을 확인할 수 있습니다.
2. Spring Data JPA
예제 세팅에는 Spring Data JPA 디펜던시가 포함되어 있지 않습니다. 직접 테스트를 원하시는 분들은 3.x.x 버전 이상의 Spring Data JPA를 추가하여 주세요.
Spring Data JPA의 경우 3.x.x 버전부터 Hibernate 6 버전을 사용하기 시작하였습니다. 기존에는 @Query
어노테이션을 사용하여 union 쿼리를 작성했었는데요, 크게 달라진 것은 없지만 이제 Hibernate 차원에서 union을 지원하기 때문에 이제 native sql로 변환할 필요가 없게 되었습니다. 따라서 Spring Boot 어플리케이션 실행 단계에서 쿼리를 검증할 수 있어 안전한 쿼리 작성이 가능합니다.
@Query
어노테이션을 사용할 때 nativeQuery = true
옵션을 주지 않아도 됩니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Repository
public interface OrderRepository extends JpaRepository<Order_2206, Long> {
@Query(
value = """
SELECT u.toAddress, u.toName, u.itemName, u.itemPrice
FROM (
SELECT o1.status as status, o1.toAddress as toAddress, o1.toName as toName, o1.itemName as itemName, o1.itemPrice as itemPrice FROM Order_2206 o1
WHERE o1.toName like :name
UNION ALL
SELECT o2.status as status, o2.toAddress as toAddress, o2.toName as toName, o2.itemName as itemName, o2.itemPrice as itemPrice FROM Order_2209 o2
UNION ALL
SELECT o3.status as status, o3.toAddress as toAddress, o3.toName as toName, o3.itemName as itemName, o3.itemPrice as itemPrice FROM Order_2301 o3
where o3.itemPrice >= :itemPrice
) AS u
WHERE u.status != :status
"""
)
List<Tuple> selectResult(@Param("name") String name, @Param("itemPrice") int itemPrice, @Param("status") OrderStatus orderStatus);
}
3. QueryDSL
JPQL이 union을 지원하지 않기 때문에 흔히 사용하는 JPAQuery
인터페이스로는 서브쿼리와 union을 사용할 수 없습니다. Github의 이슈와 답변에 따르면 Hibernate6가 좀 더 큰 범위에서 사용되면 QueryDSL 6.0 버전부터는 JPQL대신 HQL을 직접 사용하게 될수도 있다고 하네요.
1. querydsl-sql
방법이 없는것은 아닙니다. JPQL을 사용하는 JPAQuery
대신 바로 native sql로 변환되는 JPASQLQuery
인터페이스를 사용하면 됩니다. 인터페이스를 찾을 수 없다면 종속성에 querydsl-sql
이 포함되어있는지 다시한번 확인해 보세요.
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
private static JPASQLQuery<?> query() {
return new JPASQLQuery<>(entityManager, new PostgreSQLTemplates());
}
@Test
@DisplayName("QueryDSL UNION ALL")
void queryDSL() {
var q = QOrder.order;
var q1 = QOrder_2206.order_2206;
var q2 = QOrder_2209.order_2209;
var q3 = QOrder_2301.order_2301;
var unionList = query().select(q.toName, q.toAddress, q.itemName, q.itemPrice).from(
query().unionAll(
query()
.select(q1.toName.as(q.toName), q1.toAddress.as(q.toAddress), q1.itemName.as(q.itemName), q1.itemPrice.as(q.itemPrice))
.from(q1)
.where(q1.toName.like("김%"))
,
query()
.select(q2.toName.as(q.toName), q2.toAddress.as(q.toAddress), q2.itemName.as(q.itemName), q2.itemPrice.as(q.itemPrice))
.from(q2),
query()
.select(q3.toName.as(q.toName), q3.toAddress.as(q.toAddress), q3.itemName.as(q.itemName), q3.itemPrice.as(q.itemPrice))
.from(q3)
.where(q3.itemPrice.goe(100000))
).as(String.valueOf(q))
).fetch();
System.out.println(unionList);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
select order1.toName, order1.toAddress, order1.itemName, order1.itemPrice
from ((select order_2206.toName as toName,
order_2206.toAddress as toAddress,
order_2206.itemName as itemName,
order_2206.itemPrice as itemPrice
from order_2206
where order_2206.toName like ?)
union all
(select order_2209.toName as toName,
order_2209.toAddress as toAddress,
order_2209.itemName as itemName,
order_2209.itemPrice as itemPrice
from order_2209)
union all
(select order_2301.toName as toName,
order_2301.toAddress as toAddress,
order_2301.itemName as itemName,
order_2301.itemPrice as itemPrice
from order_2301
where order_2301.itemPrice >= ?)) as order1;
코드와 sql 입니다. 쿼리를 직접 db에 날리기 전까지 에러(예. PostgreSQL - FROM 절 내의 subquery 에는 반드시 alias를 가져야만 합니다.
에러 발생) 를 잡을수 없지만 일단 돌아가기만 하면 위의 두 방식들보다는 더 type-safe한 쿼리를 작성할 수 있다는 장점이 있습니다.
다만 코드를 보시면 앞선 쿼리들과 달리 union한 결과물에서 FAILED
상태를 제외하지 않았는데요, 만약 상태를 제외하고자 조건을 추가한다면 JPQL로 변환되지 않기 때문에 status column (varchar)을 자바 enum과 매핑할 수 없어 에러가 발생하게 됩니다. 이처럼 querydsl을 사용하는 의미가 약간은 퇴색될 수 있다는 단점이 있습니다.
2. Blaze Persistence (QueryDSL Integration)
querydsl-sql을 사용할때 발생하는 문제를 해결할 수 있는 방법이 있습니다. 바로 Blaze Persistence
를 사용하는 것입니다.
Hibernate, QueryDSL의 깃헙 이슈나 PR들을 볼때 자주 등장하는 라이브러리인데 한번 사용해 보았습니다. 이게 무엇이고 어떻게 사용하는지는 문서에 상세하게 설명되어 있으니 한번 읽어보시길 바랍니다.
Blaze Persistence는 CTE를 지원합니다. 앞선 예제들에서 사용해왔던 서브쿼리 형태의 쿼리를 CTE로 변경하면 다음과 같습니다.
1
2
3
4
5
6
7
8
9
10
11
12
with cte as (
select *
from order_2206
where order_2206.toname like '김%'
union all
select *
from order_2209
union all
select *
from order_2301
where order_2301.itemprice >= 100000
) select * from cte where status <> 'FAILED'
이를 자바코드로 변환하면 다음과 같습니다.
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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
@CTE
@Entity
public class OrderCTE {
@Id
String id;
@Enumerated(EnumType.STRING)
OrderStatus status;
String fromAddress;
String fromName;
String fromPhoneNumber;
String toAddress;
String toName;
String toPhoneNumber;
String itemName;
int itemPrice;
LocalDateTime createdDateTime;
}
private BlazeJPAQueryFactory jpaQueryFactory() {
var config = Criteria.getDefault();
criteriaBuilderFactory = config.createCriteriaBuilderFactory(entityManagerFactory);
return new BlazeJPAQueryFactory(entityManager, criteriaBuilderFactory);
}
private <T extends EntityPathBase<? extends Order>> Expression<?>[] binding(T targetQ) {
var q = QOrderCTE.orderCTE;
return new Expression[] {
JPQLNextExpressions.bind(q.id, Expressions.stringPath(targetQ, "id")),
JPQLNextExpressions.bind(q.status, Expressions.path(OrderStatus.class, "status")),
JPQLNextExpressions.bind(q.fromAddress, Expressions.stringPath(targetQ, "fromAddress")),
JPQLNextExpressions.bind(q.fromName, Expressions.stringPath(targetQ, "fromName")),
JPQLNextExpressions.bind(q.fromPhoneNumber, Expressions.stringPath(targetQ, "fromPhoneNumber")),
JPQLNextExpressions.bind(q.toAddress, Expressions.stringPath(targetQ, "toAddress")),
JPQLNextExpressions.bind(q.toName, Expressions.stringPath(targetQ, "toName")),
JPQLNextExpressions.bind(q.toPhoneNumber, Expressions.stringPath(targetQ, "toPhoneNumber")),
JPQLNextExpressions.bind(q.itemName, Expressions.stringPath(targetQ, "itemName")),
JPQLNextExpressions.bind(q.itemPrice, Expressions.path(Integer.class, "itemPrice")),
JPQLNextExpressions.bind(q.createdDateTime, Expressions.path(LocalDateTime.class, "createdDateTime"))
};
}
@Test
@DisplayName("QueryDSL Blaze Persistence Integration")
void queryDSLBlazePersistenceIntegration() {
var q = QOrderCTE.orderCTE;
var q1 = QOrder_2206.order_2206;
var q2 = QOrder_2209.order_2209;
var q3 = QOrder_2301.order_2301;
var list = jpaQueryFactory()
.with(
q,
jpaQueryFactory().unionAll(
JPQLNextExpressions
.select(binding(q1))
.from(q1)
.where(q1.toName.like("김%")),
JPQLNextExpressions
.select(binding(q2))
.from(q2),
JPQLNextExpressions
.select(binding(q3))
.from(q3)
.where(q3.itemPrice.goe(100000))
)
)
.select(q)
.from(q)
.where(q.status.ne(OrderStatus.FAILED))
.fetch();
System.out.println(list.size());
}
OrderCTE
라는 클래스를 생성하고 @CTE 어노테이션과 QClass를 생성하기 위해 @Entity 어노테이션을 붙였습니다. OrderCTE
클래스는 일종의 임시 테이블 (db로 치면 view) 이라고 보시면 될것 같습니다.
주의해야 할 점은 A, B, C 3개의 엔티티를 union all 하고자 할 때 @Id가 붙은 컬럼의 값은 중복되지 않아야 한다는 것입니다. 만약 A id 1, B id 1, C id 1 3개의 테이블을 union all 하면 3개의 row가 리턴되는 것이 아닌 1개의 row만 리턴되게 됩니다.
JPA에서 @Id 어노테이션이 붙은 필드는 유지크해야 하기 때문에 어찌보면 당연한(?) 것이라 생각됩니다.
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
60
61
62
63
64
select
o1_0.id,
o1_0.createdDateTime,
o1_0.fromAddress,
o1_0.fromName,
o1_0.fromPhoneNumber,
o1_0.itemName,
o1_0.itemPrice,
o1_0.status,
o1_0.toAddress,
o1_0.toName,
o1_0.toPhoneNumber
from
(select
o1_0.id,
o1_0.status,
o1_0.fromAddress,
o1_0.fromName,
o1_0.fromPhoneNumber,
o1_0.toAddress,
o1_0.toName,
o1_0.toPhoneNumber,
o1_0.itemName,
o1_0.itemPrice,
o1_0.createdDateTime
from
Order_2206 o1_0
where
o1_0.toName like ? escape '!'
union
all select
o2_0.id,
o2_0.status,
o2_0.fromAddress,
o2_0.fromName,
o2_0.fromPhoneNumber,
o2_0.toAddress,
o2_0.toName,
o2_0.toPhoneNumber,
o2_0.itemName,
o2_0.itemPrice,
o2_0.createdDateTime
from
Order_2209 o2_0
union
all select
o3_0.id,
o3_0.status,
o3_0.fromAddress,
o3_0.fromName,
o3_0.fromPhoneNumber,
o3_0.toAddress,
o3_0.toName,
o3_0.toPhoneNumber,
o3_0.itemName,
o3_0.itemPrice,
o3_0.createdDateTime
from
Order_2301 o3_0
where
o3_0.itemPrice>=?
) o1_0(id,status,fromAddress,fromName,fromPhoneNumber,toAddress,toName,toPhoneNumber,itemName,itemPrice,createdDateTime)
where
o1_0.status!=?
생성되는 sql은 위와 같습니다. 자바 코드로는 CTE를 사용했더라도 마지막에는 서브쿼리 형태의 쿼리로 변환되는 것을 확인할 수 있습니다. 실무에서 꽤 복잡한 통계 쿼리에도 적용해 보았는데 결과값이 동일하게 나오는 것으로 보아 계속 사용해도 문제없을 것이라 생각됩니다.
결론
JPA 환경에서 union을 사용하는 방법들에 대해 알아보았습니다. QueryDSL을 사용한다면 Blaze Persistence를 붙여 사용하는것이 최고의 방법이라고 생각됩니다.
Hibernate 버전이 6까지 올라오면서 기존의 JPA로는 불가능했던 일들이 어느정도 가능해진 것으로 보입니다. (EclipseLink는 오래전부터 지원했던것…) 특정 상황에서 어쩔 수 없이 사용해 왔던 native sql을 순수 자바 코드로만 표현하게 될 날도 머지 않아 보입니다.
이 포스팅에 사용된 예제 코드는 이곳 에서 확인하실 수 있습니다.