Skip to content

Commit ca9f2fc

Browse files
authored
feat: Implement UNION and UNION ALL with dedicated query models and serializers (follow-up to #880) (#883)
* feat: implement UNION, UNION ALL set queries
1 parent 1e81b6f commit ca9f2fc

File tree

21 files changed

+1919
-0
lines changed

21 files changed

+1919
-0
lines changed

docs/en/jpql-with-kotlin-jdsl/statements.md

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,99 @@ having(
163163
)
164164
```
165165

166+
### Set Operations (`UNION`, `UNION ALL`)
167+
168+
JPQL allows combining the results of two or more `SELECT` queries using set operators. Kotlin JDSL supports `UNION` and `UNION ALL` operations, which are standard features in JPQL and are also part_of the JPA 3.2 specification.
169+
170+
* `UNION`: Combines the result sets of two queries and removes duplicate rows.
171+
* `UNION ALL`: Combines the result sets of two queries and includes all duplicate rows.
172+
173+
The `SELECT` statements involved in a `UNION` or `UNION ALL` operation must have the same number of columns in their select lists, and the data types of corresponding columns must be compatible.
174+
175+
**Using with Chained Selects:**
176+
177+
You can chain `union()` or `unionAll()` after a select query structure (e.g., after `select`, `from`, `where`, `groupBy`, or `having` clauses). The `orderBy()` clause, if used, applies to the final result of the set operation.
178+
179+
```kotlin
180+
// Example with UNION
181+
val unionQuery = jpql {
182+
select(
183+
path(Book::isbn)
184+
).from(
185+
entity(Book::class)
186+
).where(
187+
path(Book::price)(BookPrice::value).lessThan(BigDecimal.valueOf(20))
188+
).union( // The right-hand side query is also a select structure
189+
select(
190+
path(Book::isbn)
191+
).from(
192+
entity(Book::class)
193+
).where(
194+
path(Book::salePrice)(BookPrice::value).lessThan(BigDecimal.valueOf(15))
195+
)
196+
).orderBy(
197+
path(Book::isbn).asc()
198+
)
199+
}
200+
201+
// Example with UNION ALL
202+
val unionAllQuery = jpql {
203+
select(
204+
path(Author::name)
205+
).from(
206+
entity(Author::class)
207+
).where(
208+
path(Author::name).like("%Rowling%")
209+
).unionAll( // The right-hand side query is also a select structure
210+
select(
211+
path(Author::name)
212+
).from(
213+
entity(Author::class)
214+
).where(
215+
path(Author::name).like("%Tolkien%")
216+
)
217+
).orderBy(
218+
path(Author::name).desc()
219+
)
220+
}
221+
```
222+
223+
**Using as Top-Level Operations:**
224+
225+
You can also use `union()` and `unionAll()` as top-level operations within a `jpql` block, combining two `JpqlQueryable<SelectQuery<T>>` instances.
226+
227+
```kotlin
228+
val query1 = jpql {
229+
select(
230+
path(Book::isbn)
231+
).from(
232+
entity(Book::class)
233+
).where(
234+
path(Book::price)(BookPrice::value).eq(BigDecimal.valueOf(10))
235+
)
236+
}
237+
238+
val query2 = jpql {
239+
select(
240+
path(Book::isbn)
241+
).from(
242+
entity(Book::class)
243+
).where(
244+
path(Book::salePrice)(BookPrice::value).eq(BigDecimal.valueOf(10))
245+
)
246+
}
247+
248+
// Top-level UNION ALL
249+
val topLevelUnionAllQuery = jpql {
250+
unionAll(query1, query2)
251+
.orderBy(path(Book::isbn).asc())
252+
}
253+
```
254+
255+
**Important Note on `ORDER BY`:**
256+
257+
The `ORDER BY` clause is applied to the final result set of the `UNION` or `UNION ALL` operation. It cannot be applied to the individual `SELECT` queries that are part_of the set operation in a way that affects the set operation itself (though subqueries might have their own `ORDER BY` for other purposes like limiting results before the set operation, this is generally not how `ORDER BY` interacts with `UNION` in JPQL for final sorting). The sorting criteria in the `ORDER BY` clause usually refer to columns by their alias from the `SELECT` list of the first query, or by their position.
258+
166259
### Order by clause
167260

168261
Use `orderBy()` and pass [Sort](sorts.md) to return data in the declared order when building an order by clause in the select statement.

docs/ko/jpql-with-kotlin-jdsl/statements.md

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,99 @@ having(
168168
)
169169
```
170170

171+
### 집합 연산 (`UNION`, `UNION ALL`)
172+
173+
Jakarta Persistence 3.2부터 JPQL은 집합 연산자를 사용하여 둘 이상의 `SELECT` 쿼리 결과를 결합하는 기능을 공식적으로 지원합니다. Kotlin JDSL은 이러한 새로운 표준 기능인 `UNION``UNION ALL` 연산을 지원합니다. (`INTERSECT``EXCEPT` 또한 JPA 3.2에 추가되었습니다.)
174+
175+
* `UNION`: 두 쿼리의 결과 집합을 결합하고 중복된 행을 제거합니다.
176+
* `UNION ALL`: 두 쿼리의 결과 집합을 결합하고 모든 중복된 행을 포함합니다.
177+
178+
`UNION` 또는 `UNION ALL` 연산에 포함되는 `SELECT` 문들은 select 목록에 동일한 수의 열을 가져야 하며, 해당 열의 데이터 타입은 서로 호환되어야 합니다.
179+
180+
**연결된 Select 쿼리와 함께 사용:**
181+
182+
select 쿼리 구조(예: `select`, `from`, `where`, `groupBy`, 또는 `having` 절 뒤)에 `union()` 또는 `unionAll()`을 연결하여 사용할 수 있습니다. `orderBy()` 절이 사용되는 경우, 집합 연산의 최종 결과에 적용됩니다.
183+
184+
```kotlin
185+
// UNION 예제
186+
val unionQuery = jpql {
187+
select(
188+
path(Book::isbn)
189+
).from(
190+
entity(Book::class)
191+
).where(
192+
path(Book::price)(BookPrice::value).lessThan(BigDecimal.valueOf(20))
193+
).union( // 우측 쿼리 또한 select 구조입니다.
194+
select(
195+
path(Book::isbn)
196+
).from(
197+
entity(Book::class)
198+
).where(
199+
path(Book::salePrice)(BookPrice::value).lessThan(BigDecimal.valueOf(15))
200+
)
201+
).orderBy(
202+
path(Book::isbn).asc()
203+
)
204+
}
205+
206+
// UNION ALL 예제
207+
val unionAllQuery = jpql {
208+
select(
209+
path(Author::name)
210+
).from(
211+
entity(Author::class)
212+
).where(
213+
path(Author::name).like("%Rowling%")
214+
).unionAll( // 우측 쿼리 또한 select 구조입니다.
215+
select(
216+
path(Author::name)
217+
).from(
218+
entity(Author::class)
219+
).where(
220+
path(Author::name).like("%Tolkien%")
221+
)
222+
).orderBy(
223+
path(Author::name).desc()
224+
)
225+
}
226+
```
227+
228+
**최상위 레벨 연산으로 사용:**
229+
230+
`jpql` 블록 내에서 두 개의 `JpqlQueryable<SelectQuery<T>>` 인스턴스를 결합하여 `union()``unionAll()`을 최상위 레벨 연산으로 사용할 수도 있습니다.
231+
232+
```kotlin
233+
val query1 = jpql {
234+
select(
235+
path(Book::isbn)
236+
).from(
237+
entity(Book::class)
238+
).where(
239+
path(Book::price)(BookPrice::value).eq(BigDecimal.valueOf(10))
240+
)
241+
}
242+
243+
val query2 = jpql {
244+
select(
245+
path(Book::isbn)
246+
).from(
247+
entity(Book::class)
248+
).where(
249+
path(Book::salePrice)(BookPrice::value).eq(BigDecimal.valueOf(10))
250+
)
251+
}
252+
253+
// 최상위 레벨 UNION ALL
254+
val topLevelUnionAllQuery = jpql {
255+
unionAll(query1, query2)
256+
.orderBy(path(Book::isbn).asc())
257+
}
258+
```
259+
260+
**`ORDER BY`에 대한 중요 참고 사항:**
261+
262+
`ORDER BY` 절은 `UNION` 또는 `UNION ALL` 연산의 최종 결과 집합에 적용됩니다. 집합 연산 자체에 영향을 미치는 방식으로 집합 연산의 일부인 개별 `SELECT` 쿼리에 적용될 수 없습니다. (물론, 하위 쿼리가 집합 연산 전에 결과를 제한하는 등의 다른 목적으로 자체 `ORDER BY`를 가질 수 있지만, 일반적으로 JPQL에서 `UNION`과 최종 정렬을 위해 상호 작용하는 방식은 아닙니다.) `ORDER BY` 절의 정렬 기준은 일반적으로 첫 번째 쿼리의 `SELECT` 목록에 있는 열의 별칭 또는 위치를 참조합니다.
263+
171264
### Order by clause
172265

173266
select statment의 order by clause를 만들기 위해, `orderBy()`를 사용할 수 있습니다.

dsl/jpql/src/main/kotlin/com/linecorp/kotlinjdsl/dsl/jpql/Jpql.kt

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@ import com.linecorp.kotlinjdsl.dsl.jpql.join.impl.AssociationJoinDsl
1919
import com.linecorp.kotlinjdsl.dsl.jpql.join.impl.FetchJoinDsl
2020
import com.linecorp.kotlinjdsl.dsl.jpql.join.impl.JoinDsl
2121
import com.linecorp.kotlinjdsl.dsl.jpql.select.SelectQueryFromStep
22+
import com.linecorp.kotlinjdsl.dsl.jpql.select.SelectQueryOrderByStep
2223
import com.linecorp.kotlinjdsl.dsl.jpql.select.impl.SelectQueryFromStepDsl
24+
import com.linecorp.kotlinjdsl.dsl.jpql.select.impl.SetOperator
25+
import com.linecorp.kotlinjdsl.dsl.jpql.select.impl.SetOperatorQueryDsl
2326
import com.linecorp.kotlinjdsl.dsl.jpql.sort.SortNullsStep
2427
import com.linecorp.kotlinjdsl.dsl.jpql.sort.impl.SortDsl
2528
import com.linecorp.kotlinjdsl.dsl.jpql.update.UpdateQuerySetFirstStep
@@ -3323,6 +3326,62 @@ open class Jpql : JpqlDsl {
33233326
return DeleteQueryDsl(entity.toEntity())
33243327
}
33253328

3329+
/**
3330+
* Creates a UNION query with two select queries.
3331+
*/
3332+
@SinceJdsl("3.6.0")
3333+
@JvmName("union")
3334+
inline fun <reified T : Any> union(
3335+
left: JpqlQueryable<SelectQuery<T>>,
3336+
right: JpqlQueryable<SelectQuery<T>>,
3337+
): SelectQueryOrderByStep<T> {
3338+
return SetOperatorQueryDsl(
3339+
returnType = T::class,
3340+
leftQuery = left,
3341+
setOperator = SetOperator.UNION,
3342+
rightQuery = right,
3343+
)
3344+
}
3345+
3346+
/**
3347+
* Creates a UNION ALL query with two select queries.
3348+
*/
3349+
@JvmName("unionAll")
3350+
@SinceJdsl("3.6.0")
3351+
inline fun <reified T : Any> unionAll(
3352+
left: JpqlQueryable<SelectQuery<T>>,
3353+
right: JpqlQueryable<SelectQuery<T>>,
3354+
): SelectQueryOrderByStep<T> {
3355+
return SetOperatorQueryDsl(
3356+
returnType = T::class,
3357+
leftQuery = left,
3358+
setOperator = SetOperator.UNION_ALL,
3359+
rightQuery = right,
3360+
)
3361+
}
3362+
3363+
/**
3364+
* Creates a UNION query that represents the union of this query and the [right] query.
3365+
*/
3366+
@JvmName("unionExtension")
3367+
@SinceJdsl("3.6.0")
3368+
inline fun <reified T : Any> JpqlQueryable<SelectQuery<T>>.union(
3369+
right: JpqlQueryable<SelectQuery<T>>,
3370+
): SelectQueryOrderByStep<T> {
3371+
return union(this, right)
3372+
}
3373+
3374+
/**
3375+
* Creates a UNION ALL that represents the union all of this query and the [right] query.
3376+
*/
3377+
@JvmName("unionAllExtension")
3378+
@SinceJdsl("3.6.0")
3379+
inline fun <reified T : Any> JpqlQueryable<SelectQuery<T>>.unionAll(
3380+
right: JpqlQueryable<SelectQuery<T>>,
3381+
): SelectQueryOrderByStep<T> {
3382+
return unionAll(this, right)
3383+
}
3384+
33263385
private fun valueOrExpression(value: Any): Expression<*> {
33273386
return if (value is Expression<*>) {
33283387
value
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.linecorp.kotlinjdsl.dsl.jpql.select.impl
2+
3+
import com.linecorp.kotlinjdsl.SinceJdsl
4+
5+
@SinceJdsl("3.6.0")
6+
@PublishedApi
7+
internal enum class SetOperator {
8+
UNION,
9+
UNION_ALL,
10+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package com.linecorp.kotlinjdsl.dsl.jpql.select.impl
2+
3+
import com.linecorp.kotlinjdsl.dsl.jpql.select.SelectQueryOrderByStep
4+
import com.linecorp.kotlinjdsl.querymodel.jpql.JpqlQueryable
5+
import com.linecorp.kotlinjdsl.querymodel.jpql.select.SelectQuery
6+
import com.linecorp.kotlinjdsl.querymodel.jpql.sort.Sortable
7+
import kotlin.reflect.KClass
8+
9+
@PublishedApi
10+
internal class SetOperatorQueryDsl<T : Any>(
11+
private val builder: SetOperatorSelectQueryBuilder<T>,
12+
) : SelectQueryOrderByStep<T>, JpqlQueryable<SelectQuery<T>> {
13+
constructor(
14+
returnType: KClass<T>,
15+
leftQuery: JpqlQueryable<SelectQuery<T>>,
16+
rightQuery: JpqlQueryable<SelectQuery<T>>,
17+
setOperator: SetOperator,
18+
) : this(
19+
SetOperatorSelectQueryBuilder<T>(returnType, leftQuery, rightQuery, setOperator),
20+
)
21+
22+
override fun orderBy(vararg sorts: Sortable?): JpqlQueryable<SelectQuery<T>> {
23+
builder.orderBy(sorts.mapNotNull { it?.toSort() })
24+
return this
25+
}
26+
27+
override fun toQuery(): SelectQuery<T> {
28+
return builder.build()
29+
}
30+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package com.linecorp.kotlinjdsl.dsl.jpql.select.impl
2+
3+
import com.linecorp.kotlinjdsl.querymodel.jpql.JpqlQueryable
4+
import com.linecorp.kotlinjdsl.querymodel.jpql.select.SelectQueries
5+
import com.linecorp.kotlinjdsl.querymodel.jpql.select.SelectQuery
6+
import com.linecorp.kotlinjdsl.querymodel.jpql.sort.Sort
7+
import kotlin.reflect.KClass
8+
9+
internal data class SetOperatorSelectQueryBuilder<T : Any>(
10+
private val returnType: KClass<T>,
11+
private val leftQuery: JpqlQueryable<SelectQuery<T>>,
12+
private val rightQuery: JpqlQueryable<SelectQuery<T>>,
13+
private var setOperator: SetOperator,
14+
private var orderBy: MutableList<Sort>? = null,
15+
) {
16+
fun orderBy(sorts: Iterable<Sort>): SetOperatorSelectQueryBuilder<T> {
17+
this.orderBy = (this.orderBy ?: mutableListOf()).also { it.addAll(sorts) }
18+
19+
return this
20+
}
21+
22+
fun build(): SelectQuery<T> {
23+
return when (setOperator) {
24+
SetOperator.UNION -> SelectQueries.selectUnionQuery(
25+
returnType = returnType,
26+
left = leftQuery,
27+
right = rightQuery,
28+
orderBy = orderBy,
29+
)
30+
SetOperator.UNION_ALL -> SelectQueries.selectUnionAllQuery(
31+
returnType = returnType,
32+
left = leftQuery,
33+
right = rightQuery,
34+
orderBy = orderBy,
35+
)
36+
}
37+
}
38+
}

0 commit comments

Comments
 (0)