Skip to content

Commit be16069

Browse files
authored
Enable @QueryProjection annotation support for KSP. (#997)
2 parents 3b22cf1 + db0e794 commit be16069

File tree

6 files changed

+165
-15
lines changed

6 files changed

+165
-15
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.querydsl.example.ksp
2+
3+
import com.querydsl.core.annotations.QueryProjection
4+
5+
data class PersonClassConstructorDTO @QueryProjection constructor(
6+
val id: Int,
7+
val name: String,
8+
)
9+
10+
@QueryProjection
11+
data class PersonClassDTO (val id: Int, val name: String)

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

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import com.querydsl.example.ksp.CatType
33
import com.querydsl.example.ksp.Person
44
import com.querydsl.example.ksp.QCat
55
import com.querydsl.example.ksp.QPerson
6+
import com.querydsl.example.ksp.QPersonClassDTO
7+
import com.querydsl.example.ksp.QPersonClassConstructorDTO
68
import com.querydsl.jpa.impl.JPAQueryFactory
79
import jakarta.persistence.EntityManagerFactory
810
import org.hibernate.cfg.AvailableSettings
@@ -111,6 +113,55 @@ class Tests {
111113
}
112114
}
113115

116+
@Test
117+
fun `select dto`() {
118+
val emf = initialize()
119+
120+
run {
121+
val em = emf.createEntityManager()
122+
em.transaction.begin()
123+
em.persist(Person(424, "John Smith"))
124+
em.transaction.commit()
125+
em.close()
126+
}
127+
128+
run {
129+
val em = emf.createEntityManager()
130+
val queryFactory = JPAQueryFactory(em)
131+
val q = QPerson.person
132+
val personDTO = queryFactory
133+
.select(QPersonClassConstructorDTO(q.id, q.name))
134+
.from(q)
135+
.where(q.name.eq("John Smith"))
136+
.fetchOne()
137+
if (personDTO == null) {
138+
fail<Any>("No personDTO was returned")
139+
} else {
140+
assertThat(personDTO.id).isEqualTo(424)
141+
assertThat(personDTO.name).isEqualTo("John Smith")
142+
}
143+
em.close()
144+
}
145+
146+
run {
147+
val em = emf.createEntityManager()
148+
val queryFactory = JPAQueryFactory(em)
149+
val q = QPerson.person
150+
val personDTO = queryFactory
151+
.select(QPersonClassDTO(q.id, q.name))
152+
.from(q)
153+
.where(q.name.eq("John Smith"))
154+
.fetchOne()
155+
if (personDTO == null) {
156+
fail<Any>("No personDTO was returned")
157+
} else {
158+
assertThat(personDTO.id).isEqualTo(424)
159+
assertThat(personDTO.name).isEqualTo("John Smith")
160+
}
161+
em.close()
162+
}
163+
}
164+
114165
private fun initialize(): EntityManagerFactory {
115166
val configuration = Configuration()
116167
.setProperty(AvailableSettings.JAKARTA_JDBC_DRIVER, org.h2.Driver::class.qualifiedName!!)

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

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

3+
import com.google.devtools.ksp.isConstructor
34
import com.google.devtools.ksp.processing.CodeGenerator
45
import com.google.devtools.ksp.processing.Resolver
56
import com.google.devtools.ksp.processing.SymbolProcessor
@@ -17,9 +18,32 @@ class QueryDslProcessor(
1718
if (settings.enable) {
1819
QueryModelType.entries.forEach { type ->
1920
resolver.getSymbolsWithAnnotation(type.associatedAnnotation)
20-
.map { it as KSClassDeclaration }
21-
.filter { isIncluded(it) }
22-
.forEach { declaration -> typeProcessor.add(declaration, type) }
21+
.map { declaration ->
22+
when {
23+
type == QueryModelType.QUERY_PROJECTION -> {
24+
val errorMessage = "${type.associatedAnnotation} annotation" +
25+
" must be declared on a constructor function or class"
26+
when (declaration) {
27+
is KSFunctionDeclaration -> {
28+
if (!declaration.isConstructor()) error(errorMessage)
29+
declaration.parent as? KSClassDeclaration
30+
?: error(errorMessage)
31+
}
32+
33+
is KSClassDeclaration -> declaration
34+
else -> error(errorMessage)
35+
}
36+
}
37+
38+
else -> declaration as KSClassDeclaration
39+
}
40+
}
41+
.filter {
42+
isIncluded(it)
43+
}
44+
.forEach { declaration ->
45+
typeProcessor.add(declaration, type)
46+
}
2347
}
2448
}
2549
return emptyList()

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

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,35 @@
11
package com.querydsl.ksp.codegen
22

3+
import com.querydsl.core.types.ConstructorExpression
34
import com.querydsl.core.types.Path
45
import com.querydsl.core.types.PathMetadata
6+
import com.querydsl.core.types.Expression
57
import com.querydsl.core.types.dsl.*
68
import com.querydsl.ksp.codegen.Naming.toCamelCase
79
import com.squareup.kotlinpoet.*
810
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
911

1012
object QueryModelRenderer {
1113
fun render(model: QueryModel): TypeSpec {
12-
return TypeSpec.classBuilder(model.className)
13-
.setEntitySuperclass(model)
14-
.addSuperProperty(model)
15-
.addProperties(model)
16-
.constructorForPath(model)
17-
.constructorForMetadata(model)
18-
.constructorForVariable(model)
19-
.constructorForTypeMetadata(model)
20-
.addInitializerCompanionObject(model)
21-
.addInheritedProperties(model)
22-
.build()
14+
return when (model.type) {
15+
QueryModelType.QUERY_PROJECTION -> TypeSpec.classBuilder(model.className)
16+
.setPrimaryConstructor(model)
17+
.setEntitySuperclass(model)
18+
.addSuperConstructorParameter(model)
19+
.build()
20+
21+
else -> TypeSpec.classBuilder(model.className)
22+
.setEntitySuperclass(model)
23+
.addSuperProperty(model)
24+
.addProperties(model)
25+
.constructorForPath(model)
26+
.constructorForMetadata(model)
27+
.constructorForVariable(model)
28+
.constructorForTypeMetadata(model)
29+
.addInitializerCompanionObject(model)
30+
.addInheritedProperties(model)
31+
.build()
32+
}
2333
}
2434

2535
private fun TypeSpec.Builder.setEntitySuperclass(model: QueryModel): TypeSpec.Builder {
@@ -33,6 +43,7 @@ object QueryModelRenderer {
3343
when (model.type) {
3444
QueryModelType.ENTITY, QueryModelType.SUPERCLASS -> EntityPathBase::class.asClassName().parameterizedBy(constraint)
3545
QueryModelType.EMBEDDABLE -> BeanPath::class.asClassName().parameterizedBy(constraint)
46+
QueryModelType.QUERY_PROJECTION -> ConstructorExpression::class.asClassName().parameterizedBy(constraint)
3647
}
3748
)
3849
return this
@@ -221,4 +232,28 @@ object QueryModelRenderer {
221232
addType(companionObject)
222233
return this
223234
}
235+
236+
private fun TypeSpec.Builder.setPrimaryConstructor(model: QueryModel): TypeSpec.Builder {
237+
val constructorSpec = FunSpec.constructorBuilder().apply {
238+
model.properties.forEach {
239+
addParameter(
240+
it.name,
241+
Expression::class.asClassName().parameterizedBy(it.type.originalTypeName)
242+
)
243+
}
244+
}.build()
245+
primaryConstructor(constructorSpec)
246+
return this
247+
}
248+
249+
private fun TypeSpec.Builder.addSuperConstructorParameter(model: QueryModel): TypeSpec.Builder {
250+
val paramTypes = model.properties.joinToString(", ", prefix = "arrayOf(", postfix = ")") {
251+
"${it.type.originalClassName}::class.java"
252+
}
253+
val paramNames = model.properties.joinToString(", ") { it.name }
254+
addSuperclassConstructorParameter(
255+
"${model.originalClassName}::class.java, $paramTypes, $paramNames"
256+
)
257+
return this
258+
}
224259
}

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.querydsl.ksp.codegen
22

33
import com.google.devtools.ksp.symbol.KSAnnotation
44
import com.google.devtools.ksp.symbol.KSClassDeclaration
5+
import com.querydsl.core.annotations.QueryProjection
56
import com.squareup.kotlinpoet.ksp.toTypeName
67
import jakarta.persistence.Embeddable
78
import jakarta.persistence.Entity
@@ -12,7 +13,8 @@ enum class QueryModelType(
1213
) {
1314
ENTITY(Entity::class.qualifiedName!!),
1415
EMBEDDABLE(Embeddable::class.qualifiedName!!),
15-
SUPERCLASS(MappedSuperclass::class.qualifiedName!!);
16+
SUPERCLASS(MappedSuperclass::class.qualifiedName!!),
17+
QUERY_PROJECTION(QueryProjection::class.qualifiedName!!);
1618

1719
companion object {
1820
fun autodetect(classDeclaration: KSClassDeclaration): QueryModelType? {

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,31 @@ class RenderTest {
176176
val features: com.querydsl.core.types.dsl.SetPath<kotlin.String, com.querydsl.core.types.dsl.StringPath> = createSet("features", kotlin.String::class.java, com.querydsl.core.types.dsl.StringPath::class.java, null)
177177
""".trimIndent())
178178
}
179+
180+
@Test
181+
fun queryProjection() {
182+
val model = QueryModel(
183+
originalClassName = ClassName("", "CatDTO"),
184+
typeParameterCount = 0,
185+
className = ClassName("", "QCatDTO"),
186+
type = QueryModelType.QUERY_PROJECTION,
187+
mockk()
188+
)
189+
val properties = listOf(
190+
QProperty("id", QPropertyType.Simple(SimpleType.QNumber(Int::class.asClassName()))),
191+
QProperty("name", QPropertyType.Simple(SimpleType.QString)),
192+
)
193+
model.properties.addAll(properties)
194+
val typeSpec = QueryModelRenderer.render(model)
195+
val code = typeSpec.toString()
196+
code.assertCompiles()
197+
code.assertContainAll("""
198+
public class QPersonDTO(
199+
id: com.querydsl.core.types.Expression<kotlin.Int>,
200+
name: com.querydsl.core.types.Expression<kotlin.String>,
201+
) : com.querydsl.core.types.ConstructorExpression<CatDTO>(CatDTO::class.java, arrayOf(kotlin.Int::class.java, kotlin.String::class.java), id, name)
202+
""".trimIndent())
203+
}
179204
}
180205

181206
private fun String.assertLines(expected: String) {
@@ -234,3 +259,5 @@ class QAnimal(
234259
}
235260

236261
class Cat
262+
263+
class CatDTO

0 commit comments

Comments
 (0)