Skip to content

Commit 3f07ca2

Browse files
authored
Exclude properties in QueryProjection outside constructor (#1232)
1 parent 1be6776 commit 3f07ca2

File tree

7 files changed

+146
-27
lines changed

7 files changed

+146
-27
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.querydsl.example.ksp
2+
3+
import com.querydsl.core.annotations.QueryProjection
4+
import jakarta.persistence.Entity
5+
import jakarta.persistence.Id
6+
7+
@Entity
8+
class Bear(
9+
@Id
10+
val id: Int,
11+
val name: String,
12+
val location: String
13+
)
14+
15+
class BearSimplifiedProjection @QueryProjection constructor(
16+
val id: Int,
17+
val name: String,
18+
) {
19+
var age: Int = 0
20+
}

querydsl-examples/querydsl-example-ksp-codegen/src/test/kotlin/Tests.kt

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
import com.querydsl.example.ksp.Bear
12
import com.querydsl.example.ksp.Cat
23
import com.querydsl.example.ksp.CatType
34
import com.querydsl.example.ksp.Dog
45
import com.querydsl.example.ksp.Person
6+
import com.querydsl.example.ksp.QBear
7+
import com.querydsl.example.ksp.QBearSimplifiedProjection
58
import com.querydsl.example.ksp.QCat
69
import com.querydsl.example.ksp.QDog
710
import com.querydsl.example.ksp.QPerson
@@ -14,6 +17,8 @@ import org.hibernate.cfg.AvailableSettings
1417
import org.assertj.core.api.Assertions.assertThat
1518
import org.assertj.core.api.Assertions.fail
1619
import org.hibernate.cfg.Configuration
20+
import kotlin.reflect.full.declaredMemberProperties
21+
import kotlin.reflect.full.primaryConstructor
1722
import kotlin.test.Test
1823

1924
class Tests {
@@ -165,6 +170,43 @@ class Tests {
165170
}
166171
}
167172

173+
@Test
174+
fun `query projection with excluded property`() {
175+
val emf = initialize()
176+
177+
run {
178+
val em = emf.createEntityManager()
179+
em.transaction.begin()
180+
em.persist(Bear(424, "Winnie", "The forest"))
181+
em.transaction.commit()
182+
em.close()
183+
}
184+
185+
run {
186+
val em = emf.createEntityManager()
187+
val queryFactory = JPAQueryFactory(em)
188+
val q = QBear.bear
189+
val bearProjection = queryFactory
190+
.select(QBearSimplifiedProjection(q.id, q.name))
191+
.from(q)
192+
.where(q.name.eq("Winnie"))
193+
.fetchOne()
194+
if (bearProjection == null) {
195+
fail<Any>("No bear was returned")
196+
} else {
197+
assertThat(bearProjection.id).isEqualTo(424)
198+
assertThat(bearProjection.name).isEqualTo("Winnie")
199+
}
200+
em.close()
201+
}
202+
}
203+
204+
@Test
205+
fun `query projection exclude property from constructor`() {
206+
assertThat(Bear::class.declaredMemberProperties.count()).isEqualTo(3)
207+
assertThat(QBearSimplifiedProjection::class.primaryConstructor!!.parameters.count()).isEqualTo(2)
208+
}
209+
168210
@Test
169211
fun `create and query dog with jsonb tag`() {
170212
val emf = initialize()
@@ -215,6 +257,7 @@ class Tests {
215257
.addAnnotatedClass(Person::class.java)
216258
.addAnnotatedClass(Cat::class.java)
217259
.addAnnotatedClass(Dog::class.java)
260+
.addAnnotatedClass(Bear::class.java)
218261

219262
return configuration
220263
.buildSessionFactory()

querydsl-tooling/querydsl-ksp-codegen/src/main/kotlin/com/querydsl/ksp/codegen/QueryDslProcessor.kt

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,32 +18,38 @@ class QueryDslProcessor(
1818
if (settings.enable) {
1919
QueryModelType.entries.forEach { type ->
2020
resolver.getSymbolsWithAnnotation(type.associatedAnnotation)
21-
.map { declaration ->
21+
.forEach { declaration ->
2222
when {
2323
type == QueryModelType.QUERY_PROJECTION -> {
2424
val errorMessage = "${type.associatedAnnotation} annotation" +
2525
" must be declared on a constructor function or class"
2626
when (declaration) {
2727
is KSFunctionDeclaration -> {
2828
if (!declaration.isConstructor()) error(errorMessage)
29-
declaration.parent as? KSClassDeclaration
29+
val parentDeclaration = declaration.parent as? KSClassDeclaration
3030
?: error(errorMessage)
31+
if (isIncluded(parentDeclaration)) {
32+
typeProcessor.addConstructor(parentDeclaration, declaration)
33+
}
34+
}
35+
is KSClassDeclaration -> {
36+
if (isIncluded(declaration)) {
37+
typeProcessor.addClass(declaration, type)
38+
}
3139
}
32-
33-
is KSClassDeclaration -> declaration
3440
else -> error(errorMessage)
3541
}
3642
}
37-
38-
else -> declaration as KSClassDeclaration
43+
declaration is KSClassDeclaration -> {
44+
if (isIncluded(declaration)) {
45+
typeProcessor.addClass(declaration, type)
46+
}
47+
}
48+
else -> {
49+
error("Annotated element was expected to be class or constructor, instead got ${declaration}")
50+
}
3951
}
4052
}
41-
.filter {
42-
isIncluded(it)
43-
}
44-
.forEach { declaration ->
45-
typeProcessor.add(declaration, type)
46-
}
4753
}
4854
}
4955
return emptyList()

querydsl-tooling/querydsl-ksp-codegen/src/main/kotlin/com/querydsl/ksp/codegen/QueryModel.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
package com.querydsl.ksp.codegen
22

33
import com.google.devtools.ksp.symbol.KSFile
4+
import com.google.devtools.ksp.symbol.KSFunction
5+
import com.google.devtools.ksp.symbol.KSFunctionDeclaration
46
import com.squareup.kotlinpoet.ClassName
57

68
class QueryModel(
79
val originalClassName: ClassName,
810
val typeParameterCount: Int,
911
val className: ClassName,
1012
val type: QueryModelType,
13+
val constructor: KSFunctionDeclaration?,
1114
val originatingFile: KSFile?
1215
) {
1316
var superclass: QueryModel? = null

querydsl-tooling/querydsl-ksp-codegen/src/main/kotlin/com/querydsl/ksp/codegen/QueryModelExtractor.kt

Lines changed: 48 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package com.querydsl.ksp.codegen
33
import com.google.devtools.ksp.getDeclaredProperties
44
import com.google.devtools.ksp.symbol.ClassKind
55
import com.google.devtools.ksp.symbol.KSClassDeclaration
6+
import com.google.devtools.ksp.symbol.KSFunction
7+
import com.google.devtools.ksp.symbol.KSFunctionDeclaration
68
import com.google.devtools.ksp.symbol.KSPropertyDeclaration
79
import com.squareup.kotlinpoet.ClassName
810
import com.squareup.kotlinpoet.asClassName
@@ -15,17 +17,23 @@ class QueryModelExtractor(
1517
private val transientClassName = Transient::class.asClassName()
1618
private val processed = mutableMapOf<String, ModelDeclaration>()
1719

18-
fun add(classDeclaration: KSClassDeclaration, type: QueryModelType): QueryModel {
19-
return lookupOrInsert(classDeclaration) { addNew(classDeclaration, type) }
20+
fun addConstructor(classDeclaration: KSClassDeclaration, constructor: KSFunctionDeclaration): QueryModel {
21+
return lookupOrInsert(classDeclaration) { addNew(classDeclaration, QueryModelType.QUERY_PROJECTION, constructor) }
2022
.model
2123
}
2224

23-
fun add(classDeclaration: KSClassDeclaration): QueryModel {
25+
fun addClass(classDeclaration: KSClassDeclaration, type: QueryModelType): QueryModel {
26+
return lookupOrInsert(classDeclaration) { addNew(classDeclaration, type, null) }
27+
.model
28+
}
29+
30+
fun addClass(classDeclaration: KSClassDeclaration): QueryModel {
2431
return lookupOrInsert(classDeclaration) {
2532
addNew(
2633
classDeclaration,
2734
QueryModelType.autodetect(classDeclaration)
28-
?: error("Unable to resolve type of entity for ${classDeclaration.qualifiedName!!.asString()}")
35+
?: error("Unable to resolve type of entity for ${classDeclaration.qualifiedName!!.asString()}"),
36+
null
2937
)
3038
}.model
3139
}
@@ -50,32 +58,59 @@ class QueryModelExtractor(
5058

5159
private fun addNew(
5260
classDeclaration: KSClassDeclaration,
53-
type: QueryModelType
61+
type: QueryModelType,
62+
constructor: KSFunctionDeclaration?
5463
): QueryModel {
55-
val queryModel = toQueryModel(classDeclaration, type)
64+
val queryModel = toQueryModel(classDeclaration, type, constructor)
5665
val superclass = classDeclaration.superclassOrNull()
5766
if (superclass != null) {
58-
queryModel.superclass = add(superclass)
67+
queryModel.superclass = addClass(superclass)
5968
}
6069
return queryModel
6170
}
6271

6372
private fun processProperties() {
6473
processed.values.map { modelDeclaration ->
65-
val properties = extractProperties(modelDeclaration.declaration)
66-
modelDeclaration.model.properties.addAll(properties)
74+
val constructor = modelDeclaration.model.constructor
75+
if (constructor == null) {
76+
val properties = extractPropertiesForClass(modelDeclaration.declaration)
77+
modelDeclaration.model.properties.addAll(properties)
78+
} else {
79+
val properties = extractPropertiesForConstructor(constructor)
80+
modelDeclaration.model.properties.addAll(properties)
81+
}
6782
}
6883
}
6984

70-
private fun extractProperties(declaration: KSClassDeclaration): List<QProperty> {
85+
private fun extractPropertiesForConstructor(declaration: KSFunctionDeclaration): List<QProperty> {
86+
return declaration
87+
.parameters
88+
.map { parameter ->
89+
val paramName = parameter.name!!.asString()
90+
val extractor = TypeExtractor(
91+
settings,
92+
"${declaration.parent?.location} - $paramName",
93+
parameter.annotations
94+
)
95+
val type = extractor.extract(parameter.type.resolve())
96+
QProperty(paramName, type)
97+
}
98+
.toList()
99+
}
100+
101+
private fun extractPropertiesForClass(declaration: KSClassDeclaration): List<QProperty> {
71102
return declaration
72103
.getDeclaredProperties()
73104
.filter { !it.isTransient() }
74105
.filter { !it.isGetterTransient() }
75106
.filter { it.hasBackingField }
76107
.map { property ->
77108
val propName = property.simpleName.asString()
78-
val extractor = TypeExtractor(settings, property)
109+
val extractor = TypeExtractor(
110+
settings,
111+
property.simpleName.asString(),
112+
property.annotations
113+
)
79114
val type = extractor.extract(property.type.resolve())
80115
QProperty(propName, type)
81116
}
@@ -106,12 +141,13 @@ class QueryModelExtractor(
106141
return null
107142
}
108143

109-
private fun toQueryModel(classDeclaration: KSClassDeclaration, type: QueryModelType): QueryModel {
144+
private fun toQueryModel(classDeclaration: KSClassDeclaration, type: QueryModelType, constructor: KSFunctionDeclaration?): QueryModel {
110145
return QueryModel(
111146
originalClassName = classDeclaration.toClassName(),
112147
typeParameterCount = classDeclaration.typeParameters.size,
113148
className = queryClassName(classDeclaration, settings),
114149
type = type,
150+
constructor = constructor,
115151
originatingFile = classDeclaration.containingFile
116152
)
117153
}

querydsl-tooling/querydsl-ksp-codegen/src/main/kotlin/com/querydsl/ksp/codegen/TypeExtractor.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import jakarta.persistence.Convert
1010

1111
class TypeExtractor(
1212
private val settings: KspSettings,
13-
private val property: KSPropertyDeclaration
13+
private val fullPathName: String,
14+
private val annotations: Sequence<KSAnnotation>
1415
) {
1516
fun extract(type: KSType): QPropertyType {
1617
val declaration = type.declaration
@@ -115,7 +116,7 @@ class TypeExtractor(
115116
ClassName("org.hibernate.annotations", "JdbcTypeCode"),
116117
Convert::class.asClassName()
117118
)
118-
if (property.annotations.any { userTypeAnnotations.contains(it.annotationType.resolve().toClassName()) }) {
119+
if (annotations.any { userTypeAnnotations.contains(it.annotationType.resolve().toClassName()) }) {
119120
return QPropertyType.Unknown(type.toClassNameSimple(), type.toTypeName())
120121
} else {
121122
return null
@@ -129,7 +130,7 @@ class TypeExtractor(
129130
}
130131

131132
private fun throwError(message: String): Nothing {
132-
error("Error processing ${property.qualifiedName!!.asString()}: $message")
133+
error("Error processing $fullPathName: $message")
133134
}
134135
}
135136

querydsl-tooling/querydsl-ksp-codegen/src/test/kotlin/RenderTest.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ class RenderTest {
2020
typeParameterCount = 0,
2121
className = ClassName("", "QUser"),
2222
type = QueryModelType.ENTITY,
23+
null,
2324
mockk()
2425
)
2526
val properties = listOf(
@@ -55,13 +56,15 @@ class RenderTest {
5556
typeParameterCount = 0,
5657
className = ClassName("", "QCat"),
5758
type = QueryModelType.ENTITY,
59+
null,
5860
mockk()
5961
)
6062
val superClass = QueryModel(
6163
originalClassName = ClassName("", "Animal"),
6264
typeParameterCount = 0,
6365
className = ClassName("", "QAnimal"),
6466
type = QueryModelType.SUPERCLASS,
67+
null,
6568
mockk()
6669
)
6770
model.superclass = superClass
@@ -79,13 +82,15 @@ class RenderTest {
7982
typeParameterCount = 0,
8083
className = ClassName("", "QCat"),
8184
type = QueryModelType.ENTITY,
85+
null,
8286
mockk()
8387
)
8488
val superClass = QueryModel(
8589
originalClassName = ClassName("", "Animal"),
8690
typeParameterCount = 0,
8791
className = ClassName("", "QAnimal"),
8892
type = QueryModelType.SUPERCLASS,
93+
null,
8994
null
9095
)
9196
model.superclass = superClass
@@ -103,6 +108,7 @@ class RenderTest {
103108
typeParameterCount = 1,
104109
className = ClassName("", "QArticle"),
105110
type = QueryModelType.ENTITY,
111+
null,
106112
mockk()
107113
)
108114
val typeSpec = QueryModelRenderer.render(model)
@@ -133,6 +139,7 @@ class RenderTest {
133139
typeParameterCount = 0,
134140
className = ClassName("", "QAnimal"),
135141
type = QueryModelType.ENTITY,
142+
null,
136143
mockk()
137144
)
138145
animalModel.properties.add(
@@ -143,6 +150,7 @@ class RenderTest {
143150
typeParameterCount = 0,
144151
className = ClassName("", "QCat"),
145152
type = QueryModelType.ENTITY,
153+
null,
146154
mockk()
147155
)
148156
catModel.superclass = animalModel
@@ -159,6 +167,7 @@ class RenderTest {
159167
typeParameterCount = 0,
160168
className = ClassName("", "QAnimal"),
161169
type = QueryModelType.ENTITY,
170+
null,
162171
mockk()
163172
)
164173
model.properties.add(
@@ -184,6 +193,7 @@ class RenderTest {
184193
typeParameterCount = 0,
185194
className = ClassName("", "QCatDTO"),
186195
type = QueryModelType.QUERY_PROJECTION,
196+
null,
187197
mockk()
188198
)
189199
val properties = listOf(

0 commit comments

Comments
 (0)