Skip to content

Commit cd2e167

Browse files
Cole-SJvelo
andauthored
feat(@QueryProjection): support builder-based Q class generation (#1078)
* feature: add builder properties in QueryProjection annotation * feature: add builder properties in Constructor class * feature: add public static inner class write methods * feature: add QueryProjection Builder feature on annotation processor * feature: add inner builder class and factory method to generated QClass * test: add QueryProjectionBuilderTestEntity for test * feature, test: add QueryProjectionBuilderTestEntity for GenericExporter * question: how can i resolve binary incompatible change without this commit * feature: add duplicate builder name check in TypeElementHandler * docs: update javadoc for QueryProjection * feature: enhance builder name validation in TypeElementHandler * test: add unit tests for QueryProjectionBuilder * Skip API validation on tooling projects Signed-off-by: Marvin Froeder <[email protected]> * modify: remove inner class, methods binary compatibility validation bypass option --------- Signed-off-by: Marvin Froeder <[email protected]> Co-authored-by: Marvin Froeder <[email protected]>
1 parent b93ab5d commit cd2e167

File tree

14 files changed

+333
-53
lines changed

14 files changed

+333
-53
lines changed

pom.xml

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,9 @@
453453
<version>0.23.1</version>
454454
<configuration>
455455
<parameter>
456+
<excludes>
457+
<exclude>com.querydsl.core.annotations.QueryProjection</exclude>
458+
</excludes>
456459
<ignoreMissingOldVersion>true</ignoreMissingOldVersion>
457460
<onlyModified>true</onlyModified>
458461
<breakBuildOnBinaryIncompatibleModifications>true</breakBuildOnBinaryIncompatibleModifications>
@@ -466,14 +469,6 @@
466469
<accessModifier>public</accessModifier>
467470
</parameter>
468471
</configuration>
469-
<executions>
470-
<execution>
471-
<goals>
472-
<goal>cmp</goal>
473-
</goals>
474-
<phase>verify</phase>
475-
</execution>
476-
</executions>
477472
</plugin>
478473
<plugin>
479474
<groupId>org.apache.maven.plugins</groupId>

querydsl-libraries/pom.xml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,18 @@
138138
</execution>
139139
</executions>
140140
</plugin>
141+
<plugin>
142+
<groupId>com.github.siom79.japicmp</groupId>
143+
<artifactId>japicmp-maven-plugin</artifactId>
144+
<executions>
145+
<execution>
146+
<goals>
147+
<goal>cmp</goal>
148+
</goals>
149+
<phase>verify</phase>
150+
</execution>
151+
</executions>
152+
</plugin>
141153
</plugins>
142154
</build>
143155

querydsl-libraries/querydsl-core/src/main/java/com/querydsl/core/annotations/QueryProjection.java

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
*
3131
* private String firstName, lastName;
3232
*
33-
* {@code @QueryProjection}
33+
* {@code @QueryProjection(useBuilder = true, builderName = "new")}
3434
* public UserInfo(String firstName, String lastName) {
3535
* this.firstName = firstName;
3636
* this.lastName = lastName;
@@ -49,8 +49,27 @@
4949
* .select(new QUserInfo(user.firstName, user.lastName))
5050
* .fetch();
5151
* }</pre>
52+
*
53+
* or(with Builder)
54+
*
55+
* <pre>{@code
56+
* QUser user = QUser.user;
57+
* List <UserInfo> result = querydsl.from(user)
58+
* .where(user.valid.eq(true))
59+
* .select(QUserInfo.builderNew()
60+
* .firstName(user.firstName)
61+
* .lastName(user.lastName)
62+
* .build()
63+
* )
64+
* .fetch();
65+
* }</pre>
5266
*/
5367
@Documented
5468
@Target({ElementType.CONSTRUCTOR, ElementType.TYPE})
5569
@Retention(RUNTIME)
56-
public @interface QueryProjection {}
70+
public @interface QueryProjection {
71+
72+
boolean useBuilder() default false;
73+
74+
String builderName() default "";
75+
}

querydsl-tooling/querydsl-apt/src/main/java/com/querydsl/apt/AbstractQuerydslProcessor.java

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
import java.util.Set;
5858
import java.util.stream.Stream;
5959
import javax.annotation.processing.AbstractProcessor;
60+
import javax.annotation.processing.Messager;
6061
import javax.annotation.processing.RoundEnvironment;
6162
import javax.lang.model.SourceVersion;
6263
import javax.lang.model.element.AnnotationMirror;
@@ -164,7 +165,7 @@ protected void processAnnotations() {
164165
var altEntityAnn = conf.getAlternativeEntityAnnotation() != null;
165166
var superAnn = conf.getSuperTypeAnnotation() != null;
166167
for (TypeElement element : elements) {
167-
var entityType = elementHandler.handleEntityType(element);
168+
var entityType = elementHandler.handleEntityType(element, processingEnv.getMessager());
168169
registerTypeElement(entityType.getFullName(), element);
169170
if (typeFactory.isSimpleTypeEntity(element, conf.getEntityAnnotation())) {
170171
context.entityTypes.put(entityType.getFullName(), entityType);
@@ -189,7 +190,7 @@ protected void processAnnotations() {
189190
if (!context.allTypes.containsKey(fullName)) {
190191
var element = processingEnv.getElementUtils().getTypeElement(fullName);
191192
if (element != null) {
192-
elementHandler.handleEntityType(element);
193+
elementHandler.handleEntityType(element, processingEnv.getMessager());
193194
}
194195
}
195196
}
@@ -206,7 +207,7 @@ protected void processAnnotations() {
206207
addSupertypeFields(entityType, handled);
207208
}
208209

209-
processProjectionTypes(elements);
210+
processProjectionTypes(elements, processingEnv.getMessager());
210211

211212
// extend entity types
212213
typeFactory.extendTypes();
@@ -231,7 +232,8 @@ private void addExternalParents(EntityType entityType) {
231232
&& !TypeUtils.hasAnnotationOfType(typeElement, conf.getEntityAnnotations())) {
232233
continue;
233234
}
234-
var superEntityType = elementHandler.handleEntityType(typeElement);
235+
var superEntityType =
236+
elementHandler.handleEntityType(typeElement, processingEnv.getMessager());
235237
if (superEntityType.getSuperType() != null) {
236238
superTypes.push(superEntityType.getSuperType().getType());
237239
}
@@ -306,7 +308,7 @@ protected Set<TypeElement> collectElements() {
306308
// register found elements
307309
for (TypeElement element : embeddedElements) {
308310
if (!elements.contains(element)) {
309-
elementHandler.handleEntityType(element);
311+
elementHandler.handleEntityType(element, processingEnv.getMessager());
310312
}
311313
}
312314
}
@@ -343,20 +345,20 @@ private void registerTypeElement(String entityName, TypeElement element) {
343345
elements.add(element);
344346
}
345347

346-
private void processProjectionTypes(Set<TypeElement> elements) {
348+
private void processProjectionTypes(Set<TypeElement> elements, Messager messager) {
347349
Set<Element> visited = new HashSet<>();
348350
for (Element element : getElements(QueryProjection.class)) {
349351
if (element.getKind() == ElementKind.CONSTRUCTOR) {
350352
var parent = element.getEnclosingElement();
351353
if (!elements.contains(parent) && !visited.contains(parent)) {
352-
var model = elementHandler.handleProjectionType((TypeElement) parent, true);
354+
var model = elementHandler.handleProjectionType((TypeElement) parent, true, messager);
353355
registerTypeElement(model.getFullName(), (TypeElement) parent);
354356
context.projectionTypes.put(model.getFullName(), model);
355357
visited.add(parent);
356358
}
357359
}
358360
if (element.getKind().isClass() && !visited.contains(element)) {
359-
var model = elementHandler.handleProjectionType((TypeElement) element, false);
361+
var model = elementHandler.handleProjectionType((TypeElement) element, false, messager);
360362
registerTypeElement(model.getFullName(), (TypeElement) element);
361363
context.projectionTypes.put(model.getFullName(), model);
362364
visited.add(element);

querydsl-tooling/querydsl-apt/src/main/java/com/querydsl/apt/TypeElementHandler.java

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import com.querydsl.codegen.utils.model.TypeCategory;
2424
import com.querydsl.core.annotations.PropertyType;
2525
import com.querydsl.core.annotations.QueryInit;
26+
import com.querydsl.core.annotations.QueryProjection;
2627
import com.querydsl.core.annotations.QueryType;
2728
import com.querydsl.core.util.Annotations;
2829
import com.querydsl.core.util.BeanUtils;
@@ -34,12 +35,14 @@
3435
import java.util.List;
3536
import java.util.Map;
3637
import java.util.Set;
38+
import javax.annotation.processing.Messager;
3739
import javax.lang.model.element.Element;
3840
import javax.lang.model.element.ExecutableElement;
3941
import javax.lang.model.element.TypeElement;
4042
import javax.lang.model.element.VariableElement;
4143
import javax.lang.model.type.TypeMirror;
4244
import javax.lang.model.util.ElementFilter;
45+
import javax.tools.Diagnostic;
4346

4447
/**
4548
* {@code TypeElementHandler} is an APT visitor for entity types
@@ -67,7 +70,7 @@ public TypeElementHandler(
6770
this.queryTypeFactory = queryTypeFactory;
6871
}
6972

70-
public EntityType handleEntityType(TypeElement element) {
73+
public EntityType handleEntityType(TypeElement element, Messager messager) {
7174
EntityType entityType = typeFactory.getEntityType(element.asType(), true);
7275
List<? extends Element> elements = element.getEnclosedElements();
7376
var config = configuration.getConfig(element, elements);
@@ -78,7 +81,7 @@ public EntityType handleEntityType(TypeElement element) {
7881

7982
// constructors
8083
if (config.visitConstructors()) {
81-
handleConstructors(entityType, elements, true);
84+
handleConstructors(entityType, elements, true, messager);
8285
}
8386

8487
// fields
@@ -175,13 +178,14 @@ private Property toProperty(
175178
return new Property(entityType, name, propertyType, inits);
176179
}
177180

178-
public EntityType handleProjectionType(TypeElement e, boolean onlyAnnotatedConstructors) {
181+
public EntityType handleProjectionType(
182+
TypeElement e, boolean onlyAnnotatedConstructors, Messager messager) {
179183
Type c = typeFactory.getType(e.asType(), true);
180184
var entityType =
181185
new EntityType(c.as(TypeCategory.ENTITY), configuration.getVariableNameFunction());
182186
typeMappings.register(entityType, queryTypeFactory.create(entityType));
183187
List<? extends Element> elements = e.getEnclosedElements();
184-
handleConstructors(entityType, elements, onlyAnnotatedConstructors);
188+
handleConstructors(entityType, elements, onlyAnnotatedConstructors, messager);
185189
return entityType;
186190
}
187191

@@ -198,11 +202,40 @@ private Type getType(VariableElement element) {
198202
}
199203

200204
private void handleConstructors(
201-
EntityType entityType, List<? extends Element> elements, boolean onlyAnnotatedConstructors) {
205+
EntityType entityType,
206+
List<? extends Element> elements,
207+
boolean onlyAnnotatedConstructors,
208+
Messager messager) {
209+
var builderNameSet = new HashSet<String>();
202210
for (ExecutableElement constructor : ElementFilter.constructorsIn(elements)) {
203211
if (configuration.isValidConstructor(constructor, onlyAnnotatedConstructors)) {
204212
var parameters = transformParams(constructor.getParameters());
205-
entityType.addConstructor(new Constructor(parameters));
213+
QueryProjection projection = constructor.getAnnotation(QueryProjection.class);
214+
if (projection != null
215+
&& projection.useBuilder()
216+
&& projection.builderName().trim().isEmpty()) {
217+
messager.printMessage(
218+
Diagnostic.Kind.ERROR,
219+
"@QueryProjection with builder=true requires a non-empty builderName",
220+
constructor);
221+
return;
222+
}
223+
224+
Constructor constructorModel = new Constructor(parameters);
225+
constructorModel.setUseBuilder(projection != null && projection.useBuilder());
226+
constructorModel.setBuilderName(projection != null ? projection.builderName() : "");
227+
228+
if (constructorModel.useBuilder()
229+
&& builderNameSet.contains(constructorModel.getBuilderName())) {
230+
messager.printMessage(
231+
Diagnostic.Kind.ERROR,
232+
"Duplicate builderName found: " + constructorModel.getBuilderName(),
233+
constructor);
234+
return;
235+
}
236+
237+
builderNameSet.add(constructorModel.getBuilderName());
238+
entityType.addConstructor(constructorModel);
206239
}
207240
}
208241
}

querydsl-tooling/querydsl-apt/src/test/java/com/querydsl/apt/GenericExporterTest.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ public void execute() throws IOException {
5050
expected.add("QQueryProjectionTest_EntityWithProjection.java");
5151
expected.add("QEmbeddable3Test_EmbeddableClass.java");
5252
expected.add("QQueryEmbedded4Test_User.java");
53+
expected.add("QQueryProjectionBuilderTestEntity.java");
5354

5455
execute(expected, "GenericExporterTest", "QuerydslAnnotationProcessor");
5556
}
@@ -89,6 +90,7 @@ public void execute2() throws IOException {
8990
expected.add("QOneToOneTest_Person.java");
9091
expected.add("QGeneric16Test_HidaBez.java");
9192
expected.add("QGeneric16Test_HidaBezGruppe.java");
93+
expected.add("QQueryProjectionBuilderTestEntity.java");
9294

9395
execute(expected, "GenericExporterTest2", "HibernateAnnotationProcessor");
9496
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package com.querydsl.apt.domain;
2+
3+
import static org.junit.Assert.assertEquals;
4+
import static org.junit.Assert.assertNotNull;
5+
6+
import com.querydsl.core.types.dsl.Expressions;
7+
import org.junit.Test;
8+
9+
public class QueryProjectionBuilderTest {
10+
11+
@Test
12+
public void builder_buildsQClassCorrectly() {
13+
var property = Expressions.stringPath("x");
14+
QQueryProjectionBuilderTestEntity dto =
15+
QQueryProjectionBuilderTestEntity.builderTest1().setProperty(property).build();
16+
17+
assertNotNull(dto);
18+
assertEquals(QueryProjectionBuilderTestEntity.class, dto.getType());
19+
}
20+
21+
@Test
22+
public void builder_buildsQClassCorrectly2() {
23+
var property = Expressions.stringPath("x");
24+
var intProperty = Expressions.numberPath(Integer.class, "y");
25+
QQueryProjectionBuilderTestEntity dto =
26+
QQueryProjectionBuilderTestEntity.builderTest2()
27+
.setProperty(property)
28+
.setIntProperty(intProperty)
29+
.build();
30+
31+
assertNotNull(dto);
32+
assertEquals(QueryProjectionBuilderTestEntity.class, dto.getType());
33+
}
34+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package com.querydsl.apt.domain;
2+
3+
import com.querydsl.core.annotations.QueryProjection;
4+
5+
public class QueryProjectionBuilderTestEntity {
6+
private String property;
7+
private int intProperty;
8+
private Test test;
9+
10+
@QueryProjection(useBuilder = true, builderName = "Test1")
11+
public QueryProjectionBuilderTestEntity(String property) {
12+
this.property = property;
13+
}
14+
15+
@QueryProjection(useBuilder = true, builderName = "Test2")
16+
public QueryProjectionBuilderTestEntity(String property, int intProperty) {
17+
this.property = property;
18+
this.intProperty = intProperty;
19+
}
20+
21+
@QueryProjection(useBuilder = true, builderName = "Test3")
22+
public QueryProjectionBuilderTestEntity(String property, int intProperty, Test test) {
23+
this.property = property;
24+
this.intProperty = intProperty;
25+
this.test = test;
26+
}
27+
28+
public static class Test {
29+
private String property;
30+
private int intProperty;
31+
}
32+
}

querydsl-tooling/querydsl-codegen-utils/src/main/java/com/querydsl/codegen/utils/CodeWriter.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ public interface CodeWriter extends Appendable {
4141

4242
CodeWriter beginClass(Type type, Type superClass, Type... interfaces) throws IOException;
4343

44+
CodeWriter beginInnerStaticClass(Type type) throws IOException;
45+
46+
CodeWriter beginInnerStaticClass(Type type, Type superClass, Type... interfaces)
47+
throws IOException;
48+
4449
<T> CodeWriter beginConstructor(Collection<T> params, Function<T, Parameter> transformer)
4550
throws IOException;
4651

querydsl-tooling/querydsl-codegen-utils/src/main/java/com/querydsl/codegen/utils/JavaWriter.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ public final class JavaWriter extends AbstractCodeWriter<JavaWriter> {
5757

5858
private static final String PUBLIC_CLASS = "public class ";
5959

60+
private static final String PUBLIC_STATIC_CLASS = "public static class ";
61+
6062
private static final String PUBLIC_FINAL = "public final ";
6163

6264
private static final String PUBLIC_INTERFACE = "public interface ";
@@ -202,6 +204,33 @@ public JavaWriter beginClass(Type type, Type superClass, Type... interfaces) thr
202204
return this;
203205
}
204206

207+
@Override
208+
public CodeWriter beginInnerStaticClass(Type type) throws IOException {
209+
return beginInnerStaticClass(type, null);
210+
}
211+
212+
@Override
213+
public CodeWriter beginInnerStaticClass(Type type, Type superClass, Type... interfaces)
214+
throws IOException {
215+
beginLine(PUBLIC_STATIC_CLASS, type.getGenericName(false, packages, classes));
216+
if (superClass != null) {
217+
append(EXTENDS).append(superClass.getGenericName(false, packages, classes));
218+
}
219+
if (interfaces.length > 0) {
220+
append(IMPLEMENTS);
221+
for (int i = 0; i < interfaces.length; i++) {
222+
if (i > 0) {
223+
append(Symbols.COMMA);
224+
}
225+
append(interfaces[i].getGenericName(false, packages, classes));
226+
}
227+
}
228+
append(" {").nl().nl();
229+
goIn();
230+
types.push(type);
231+
return this;
232+
}
233+
205234
@Override
206235
public <T> JavaWriter beginConstructor(
207236
Collection<T> parameters, Function<T, Parameter> transformer) throws IOException {

0 commit comments

Comments
 (0)