Skip to content

Commit cf8172e

Browse files
authored
Merge pull request #14 from bee-produced/13-pagination-wrong-behaviour
bee.persistent.jpa: Pagination wrong behaviour
2 parents 9cbbfc3 + 07ffb60 commit cf8172e

File tree

11 files changed

+1464
-60
lines changed

11 files changed

+1464
-60
lines changed

bee.persistent/src/jpa/kotlin/com/beeproduced/bee/persistent/jpa/meta/EntityInfo.kt

+2-4
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
package com.beeproduced.bee.persistent.jpa.meta
22

3-
import jakarta.persistence.EmbeddedId
43
import jakarta.persistence.IdClass
54
import java.lang.reflect.Field
6-
import java.lang.reflect.Method
75

86

97
/**
@@ -21,7 +19,7 @@ class EntityInfo(
2119

2220
data class EntityToIdField(val entityField: Field, val idField: Field)
2321

24-
val compositeKeyMapping: Set<EntityToIdField>?
22+
val compositeKeyMapping: List<EntityToIdField>?
2523

2624
// https://www.baeldung.com/jpa-entity-table-names#defaultNames
2725
// https://stackoverflow.com/a/634629/12347616
@@ -33,7 +31,7 @@ class EntityInfo(
3331
if (type.isAnnotationPresent(IdClass::class.java)) {
3432
// Map id properties if composite key
3533
nonPrimitiveIdType = idType
36-
compositeKeyMapping = idType.declaredFields.mapTo(HashSet()) { idField ->
34+
compositeKeyMapping = idType.declaredFields.map { idField ->
3735
idField.isAccessible = true
3836
val entityField = type.getDeclaredField(idField.name)
3937
EntityToIdField(entityField, idField)

bee.persistent/src/jpa/kotlin/com/beeproduced/bee/persistent/jpa/repository/BaseDataRepository.kt

+37-9
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,11 @@ import com.beeproduced.bee.persistent.selection.EmptySelection
1111
import com.linecorp.kotlinjdsl.QueryFactoryImpl
1212
import com.linecorp.kotlinjdsl.query.creator.CriteriaQueryCreatorImpl
1313
import com.linecorp.kotlinjdsl.query.creator.SubqueryCreatorImpl
14+
import com.linecorp.kotlinjdsl.query.spec.expression.ColumnSpec
1415
import com.linecorp.kotlinjdsl.query.spec.expression.EntitySpec
1516
import com.linecorp.kotlinjdsl.querydsl.CriteriaDeleteQueryDsl
1617
import com.linecorp.kotlinjdsl.querydsl.CriteriaQueryDsl
18+
import com.linecorp.kotlinjdsl.selectQuery
1719
import jakarta.persistence.EntityManager
1820
import org.hibernate.metamodel.model.domain.internal.MappingMetamodelImpl
1921
import org.slf4j.Logger
@@ -41,8 +43,9 @@ abstract class BaseDataRepository<T : DataEntity<T>, ID : Any>(
4143
protected val relations: Relations
4244
protected val generated: Generated
4345

44-
protected val selectIdColumns: CriteriaQueryDsl<*>.() -> Unit
45-
protected val selectCount: CriteriaQueryDsl<Long>.() -> Unit
46+
protected val selectDistinctIdColumns: CriteriaQueryDsl<*>.() -> Unit
47+
protected val selectDistinctIdAndOrderColumns: CriteriaQueryDsl<*>.(List<ColumnSpec<*>>) -> Unit
48+
protected val selectCount: CriteriaQueryDsl<Long>.(forceDistinct: Boolean) -> Unit
4649
protected val queryFactory = QueryFactoryImpl(
4750
criteriaQueryCreator = CriteriaQueryCreatorImpl(entityManager),
4851
subqueryCreator = SubqueryCreatorImpl()
@@ -78,8 +81,11 @@ abstract class BaseDataRepository<T : DataEntity<T>, ID : Any>(
7881
// Is needed, as there is a difference between selecting entity with single primary key
7982
// and with composite key
8083
@Suppress("UNCHECKED_CAST")
81-
selectIdColumns = QueryBuilder.selectIdsFromEntity(this.type, ids)
82-
as CriteriaQueryDsl<*>.() -> Unit
84+
selectDistinctIdColumns = QueryBuilder.selectDistinctIdsFromEntity(this.type, ids)
85+
as CriteriaQueryDsl<*>.() -> Unit
86+
@Suppress("UNCHECKED_CAST")
87+
selectDistinctIdAndOrderColumns = QueryBuilder.selectDistinctIdAndOrderColumnsFromEntity(this.type, ids)
88+
as CriteriaQueryDsl<*>.(List<ColumnSpec<*>>) -> Unit
8389
selectCount = QueryBuilder.selectCount(this.type, ids)
8490
}
8591

@@ -199,15 +205,15 @@ abstract class BaseDataRepository<T : DataEntity<T>, ID : Any>(
199205
dsl: CriteriaQueryDsl<*>.() -> Unit = {}
200206
): List<T> {
201207
val testDsl = DummyCriteriaQueryDsl().apply(dsl)
202-
val query = if (testDsl.hasLimitClause) {
208+
val query = if (testDsl.hasLimitClause && !testDsl.hasOrderByClause) {
203209
// Query ids first, then query with where!
204210
// Hibernate cannot use `LIMIT` when joining tables!
205211
// To be precise, it returns a “limited” result but queries EVERYTHING and limits in memory!
206212
// https://vladmihalcea.com/fix-hibernate-hhh000104-entity-fetch-pagination-warning-message/
207213
// https://hibernate.atlassian.net/browse/HHH-15964?focusedCommentId=110593
208214
val resultIds: List<Any> = queryFactory.selectQuery(info.nonPrimitiveIdType) {
209215
// select(selectIdColumns)
210-
selectIdColumns(this)
216+
selectDistinctIdColumns(this)
211217
from(EntitySpec(type))
212218
dsl(this)
213219
}.resultList
@@ -218,7 +224,25 @@ abstract class BaseDataRepository<T : DataEntity<T>, ID : Any>(
218224
select(EntitySpec(type))
219225
from(EntitySpec(type))
220226
where(QueryBuilder.whereClauseFromIds(type, info.compositeKeyMapping, resultIds, ids))
227+
}
228+
} else if (testDsl.hasLimitClause && testDsl.hasOrderByClause) {
229+
val orderByColumns = testDsl.orderByColumns()
230+
val idsAndOrderBy: List<List<Any>> = queryFactory.selectQuery<List<Any>> {
231+
selectDistinctIdAndOrderColumns(this, orderByColumns)
232+
from(EntitySpec(type))
221233
dsl(this)
234+
}.resultList
235+
236+
if (idsAndOrderBy.isEmpty()) return emptyList()
237+
val resultIds = idsAndOrderBy.map {
238+
it.dropLast(orderByColumns.count())
239+
}
240+
241+
queryFactory.selectQuery(type) {
242+
select(EntitySpec(type))
243+
from(EntitySpec(type))
244+
where(QueryBuilder.whereClauseFromIds(type, info.compositeKeyMapping, resultIds, ids))
245+
orderBy(testDsl.orders)
222246
}
223247
} else {
224248
queryFactory.selectQuery(type) {
@@ -242,8 +266,12 @@ abstract class BaseDataRepository<T : DataEntity<T>, ID : Any>(
242266
}
243267

244268
open fun count(dsl: CriteriaQueryDsl<*>.() -> Unit = {}): Long {
269+
return count(true, dsl)
270+
}
271+
272+
open fun count(distinct: Boolean, dsl: CriteriaQueryDsl<*>.() -> Unit = {}): Long {
245273
val count = queryFactory.selectQuery(Long::class.javaObjectType) {
246-
selectCount(this)
274+
selectCount(this, distinct)
247275
from(EntitySpec(type))
248276
dsl(this)
249277
}
@@ -255,7 +283,7 @@ abstract class BaseDataRepository<T : DataEntity<T>, ID : Any>(
255283
*/
256284
open fun exists(id: ID): Boolean {
257285
val resultIds: List<Any> = queryFactory.selectQuery(info.nonPrimitiveIdType) {
258-
selectIdColumns(this)
286+
selectDistinctIdColumns(this)
259287
from(EntitySpec(type))
260288
where(QueryBuilder.whereClauseFromId(type, info.compositeKeyMapping, id, ids))
261289
}.resultList
@@ -270,7 +298,7 @@ abstract class BaseDataRepository<T : DataEntity<T>, ID : Any>(
270298
open fun existsAll(ids: Collection<ID>): Boolean {
271299
val uniqueIds = ids.toSet()
272300
val resultIds: List<Any> = queryFactory.selectQuery(info.nonPrimitiveIdType) {
273-
selectIdColumns(this)
301+
selectDistinctIdColumns(this)
274302
from(EntitySpec(type))
275303
where(
276304
QueryBuilder.whereClauseFromIds(

bee.persistent/src/jpa/kotlin/com/beeproduced/bee/persistent/jpa/repository/QueryBuilder.kt

+71-12
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ object QueryBuilder {
5353
*/
5454
fun whereClauseFromId(
5555
entityType: Class<*>,
56-
compositeKeyMapping: Set<EntityInfo.EntityToIdField>?,
56+
compositeKeyMapping: List<EntityInfo.EntityToIdField>?,
5757
id: Any,
5858
idInfo: Ids
5959
): PredicateSpec? {
@@ -82,9 +82,10 @@ object QueryBuilder {
8282
*/
8383
private fun whereClauseFromCompositeKey(
8484
entityType: Class<*>,
85-
compositeKeyMapping: Set<EntityInfo.EntityToIdField>,
85+
compositeKeyMapping: List<EntityInfo.EntityToIdField>,
8686
idInfo: Any,
8787
): PredicateSpec? {
88+
if (idInfo is List<*>) return whereClauseFromCompositeKeyList(entityType, compositeKeyMapping, idInfo)
8889
var whereClause: PredicateSpec? = null
8990
for ((entityField, idField) in compositeKeyMapping) {
9091
val entitySpec = EntitySpec(entityType)
@@ -101,10 +102,33 @@ object QueryBuilder {
101102
return whereClause
102103
}
103104

105+
private fun whereClauseFromCompositeKeyList(
106+
entityType: Class<*>,
107+
compositeKeyMapping: List<EntityInfo.EntityToIdField>,
108+
idInfo: List<*>,
109+
): PredicateSpec? {
110+
var whereClause: PredicateSpec? = null
111+
var i = 0
112+
for ((entityField) in compositeKeyMapping) {
113+
val entitySpec = EntitySpec(entityType)
114+
val columnSpec = ColumnSpec<Any?>(entitySpec, entityField.name)
115+
val idValue = requireNotNull(idInfo[i])
116+
val equalSpec = EqualValueSpec(columnSpec, idValue)
117+
118+
whereClause = if (whereClause == null) {
119+
equalSpec
120+
} else {
121+
AndSpec(listOf(whereClause, equalSpec))
122+
}
123+
i++
124+
}
125+
return whereClause
126+
}
127+
104128

105129
fun whereClauseFromIds(
106130
entityType: Class<*>,
107-
compositeKeyMapping: Set<EntityInfo.EntityToIdField>?,
131+
compositeKeyMapping: List<EntityInfo.EntityToIdField>?,
108132
ids: Collection<Any>,
109133
idInfo: Ids
110134
): PredicateSpec? {
@@ -125,7 +149,7 @@ object QueryBuilder {
125149

126150
private fun whereClauseFromCompositeKeys(
127151
entityType: Class<*>,
128-
compositeKeyMapping: Set<EntityInfo.EntityToIdField>,
152+
compositeKeyMapping: List<EntityInfo.EntityToIdField>,
129153
ids: Collection<Any>,
130154
): PredicateSpec {
131155
// TODO: Optimize? Is there are way to use `IN` instead of joining `AND` statements with `OR`
@@ -164,7 +188,7 @@ object QueryBuilder {
164188
//
165189
// }
166190

167-
fun selectIdsFromEntity(
191+
fun selectDistinctIdsFromEntity(
168192
entityType: Class<*>,
169193
ids: Ids
170194
): CriteriaQueryDsl<Any>.() -> Unit {
@@ -183,7 +207,7 @@ object QueryBuilder {
183207
val column: ExpressionSpec<Any> = ColumnSpec(entitySpec, member.name)
184208
val singleSelectQuery: CriteriaQueryDsl<Any>.() -> Unit = {
185209
// dsl.select(ColumnSpec<Any?>(entitySpec, member.name))
186-
this.select(column)
210+
this.selectDistinct(column)
187211
}
188212
singleSelectQuery
189213
} else {
@@ -193,23 +217,58 @@ object QueryBuilder {
193217
ColumnSpec<Any>(entitySpec, member.name)
194218
}
195219
val multiSelectQuery: CriteriaQueryDsl<*>.() -> Unit = {
196-
this.select(columns)
220+
this.selectDistinct(columns)
197221
}
198222
multiSelectQuery
199223
}
200224
}
201225

202-
fun selectCount(
226+
fun selectDistinctIdAndOrderColumnsFromEntity(
203227
entityType: Class<*>,
204228
ids: Ids
205-
): CriteriaQueryDsl<Long>.() -> Unit {
229+
): CriteriaQueryDsl<Any>.(orderBy: List<ColumnSpec<*>>) -> Unit {
230+
// Return correct corresponding select statement
231+
// If entity does not have a composite key
232+
// use CriteriaQueryDsl's SingleSelectClause
233+
// If entity has a composite key
234+
// use CriteriaQueryDsl's MultiSelectClause
235+
// Why? Using only MultiSelectClause can result in not found constructors
236+
// E.g., the UUID constructor cannot be invoked with a list
237+
return if (ids.values.count() == 1) {
238+
val id = ids.values.first()
239+
val member = id.field
240+
val entitySpec = EntitySpec(entityType)
241+
// SingleSelectClause(idType, false, ColumnSpec(entitySpec, member.name))
242+
val column: ExpressionSpec<Any> = ColumnSpec(entitySpec, member.name)
243+
val singleSelectQuery: CriteriaQueryDsl<Any>.(List<ColumnSpec<*>>) -> Unit = {
244+
// dsl.select(ColumnSpec<Any?>(entitySpec, member.name))
245+
this.selectDistinct(listOf(column) + it)
246+
}
247+
singleSelectQuery
248+
} else {
249+
val columns = ids.values.map { id ->
250+
val member = id.field
251+
val entitySpec = EntitySpec(entityType)
252+
ColumnSpec<Any>(entitySpec, member.name)
253+
}
254+
val multiSelectQuery: CriteriaQueryDsl<*>.(List<ColumnSpec<*>>) -> Unit = {
255+
this.selectDistinct(columns + it)
256+
}
257+
multiSelectQuery
258+
}
259+
}
260+
261+
fun selectCount(
262+
entityType: Class<*>,
263+
ids: Ids,
264+
): CriteriaQueryDsl<Long>.(forceDistinct: Boolean) -> Unit {
206265
val id = ids.values.first()
207266
val member = id.field
208267
val entitySpec = EntitySpec(entityType)
209268
val column: ExpressionSpec<Any> = ColumnSpec(entitySpec, member.name)
210-
val singleSelectQuery: CriteriaQueryDsl<Long>.() -> Unit = {
211-
// dsl.select(ColumnSpec<Any?>(entitySpec, member.name))
212-
this.select(listOf(count(column)))
269+
val singleSelectQuery: CriteriaQueryDsl<Long>.(Boolean) -> Unit = { forceDistinct ->
270+
if (!forceDistinct) this.select(listOf(count(column)))
271+
else this.select(listOf(countDistinct(column)))
213272
}
214273
return singleSelectQuery
215274
}

bee.persistent/src/jpa/kotlin/com/beeproduced/bee/persistent/jpa/repository/dsl/DummyCriteriaQueryDsl.kt

+23-5
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,16 @@ class DummyCriteriaQueryDsl : CriteriaQueryDsl<Any> {
4343
right: EntitySpec<R>,
4444
relation: Relation<T, R?>,
4545
joinType: JoinType
46-
) {}
46+
) {
47+
}
4748

4849
override fun <T, R> fetch(
4950
left: EntitySpec<T>,
5051
right: EntitySpec<R>,
5152
relation: Relation<T, R?>,
5253
joinType: JoinType
53-
) {}
54+
) {
55+
}
5456

5557
override fun from(entity: EntitySpec<*>) {}
5658

@@ -60,11 +62,26 @@ class DummyCriteriaQueryDsl : CriteriaQueryDsl<Any> {
6062

6163
override fun hints(hints: Map<String, Any>) {}
6264

63-
override fun <T, R> join(left: EntitySpec<T>, right: EntitySpec<R>, relation: Relation<T, R?>, joinType: JoinType) {}
65+
override fun <T, R> join(
66+
left: EntitySpec<T>,
67+
right: EntitySpec<R>,
68+
relation: Relation<T, R?>,
69+
joinType: JoinType
70+
) {
71+
}
6472

6573
override fun <T> join(entity: EntitySpec<T>, predicate: PredicateSpec) {}
6674

67-
override fun orderBy(orders: List<OrderSpec>) {}
75+
private val orderByClauses = mutableListOf<OrderSpec>()
76+
77+
val hasOrderByClause get() = orderByClauses.isNotEmpty()
78+
79+
val orders: List<OrderSpec> get() = orderByClauses
80+
fun orderByColumns() = orderByClauses.map { it.getColumnSpec() }
81+
82+
override fun orderBy(orders: List<OrderSpec>) {
83+
orderByClauses.addAll(orders)
84+
}
6885

6986
override fun select(distinct: Boolean, expression: ExpressionSpec<Any>): SingleSelectClause<Any> {
7087
throw IllegalAccessException("Should not be invoked")
@@ -81,7 +98,8 @@ class DummyCriteriaQueryDsl : CriteriaQueryDsl<Any> {
8198
parent: EntitySpec<P>,
8299
child: EntitySpec<C>,
83100
parentJoinType: JoinType
84-
) {}
101+
) {
102+
}
85103

86104
override fun where(predicate: PredicateSpec?) {}
87105

0 commit comments

Comments
 (0)