Skip to content

Commit 2de4e25

Browse files
committed
refactor: promote SimpleDTOProjection to official NameBasedProjection feature
1 parent cbfa736 commit 2de4e25

File tree

2 files changed

+268
-70
lines changed

2 files changed

+268
-70
lines changed

querydsl-libraries/querydsl-core/src/main/java/com/querydsl/core/types/NameBasedProjection.java

Lines changed: 132 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -23,91 +23,88 @@ public NameBasedProjection(Class<? extends T> type, EntityPathBase<?>... entitie
2323
}
2424

2525
private Map<String, Expression<?>> collectExpressions(EntityPathBase<?>... entities) {
26-
Map<String, Expression<?>> map = new HashMap<>();
27-
for (EntityPathBase<?> entity : entities) {
28-
Class<?> clazz = entity.getClass();
29-
for (Field f : clazz.getFields()){
30-
String name = f.getName();
31-
if(!map.containsKey(name)){
32-
try{
33-
Object val = f.get(entity);
34-
if(val instanceof Expression){
35-
map.put(name, (Expression<?>) val);
36-
}
37-
}catch (IllegalAccessException e){
38-
39-
}
40-
}
26+
Map<String, Expression<?>> map = new HashMap<>();
27+
for (EntityPathBase<?> entity : entities) {
28+
Class<?> clazz = entity.getClass();
29+
for (Field f : clazz.getFields()) {
30+
String name = f.getName();
31+
if (!map.containsKey(name)) {
32+
try {
33+
Object val = f.get(entity);
34+
if (val instanceof Expression) {
35+
map.put(name, (Expression<?>) val);
36+
}
37+
} catch (IllegalAccessException e) {
38+
4139
}
40+
}
4241
}
43-
return map;
42+
}
43+
return map;
4444
}
4545

46-
47-
4846
private Constructor<? extends T> findMatchingConstructor(
49-
Class<? extends T> type,
50-
Map<String, Expression<?>> exprByName
51-
) {
52-
Constructor<?>[] ctors = type.getDeclaredConstructors();
53-
54-
for (Constructor<?> ctor : ctors) {
55-
Parameter[] params = ctor.getParameters();
56-
if (params.length == 0) continue;
57-
boolean allMatch = true;
58-
for (Parameter p : params) {
59-
String paramName = p.getName();
60-
if (!exprByName.containsKey(paramName)) {
61-
allMatch = false;
62-
break;
63-
}
64-
}
65-
if (allMatch) {
66-
ctor.setAccessible(true);
67-
return (Constructor<? extends T>) ctor;
68-
}
47+
Class<? extends T> type, Map<String, Expression<?>> exprByName) {
48+
Constructor<?>[] ctors = type.getDeclaredConstructors();
49+
50+
for (Constructor<?> ctor : ctors) {
51+
Parameter[] params = ctor.getParameters();
52+
if (params.length == 0) continue;
53+
boolean allMatch = true;
54+
for (Parameter p : params) {
55+
String paramName = p.getName();
56+
if (!exprByName.containsKey(paramName)) {
57+
allMatch = false;
58+
break;
6959
}
60+
}
61+
if (allMatch) {
62+
ctor.setAccessible(true);
63+
return (Constructor<? extends T>) ctor;
64+
}
65+
}
7066

71-
Constructor<?> fallback = null;
72-
for (Constructor<?> ctor : ctors) {
73-
int paramCount = ctor.getParameterCount();
74-
if (paramCount > 0 && paramCount <= exprByName.size()) {
75-
if(fallback == null || ctor.getParameterCount() > fallback.getParameterCount()){
76-
fallback = ctor;
77-
}
78-
}
67+
Constructor<?> fallback = null;
68+
for (Constructor<?> ctor : ctors) {
69+
int paramCount = ctor.getParameterCount();
70+
if (paramCount > 0 && paramCount <= exprByName.size()) {
71+
if (fallback == null || ctor.getParameterCount() > fallback.getParameterCount()) {
72+
fallback = ctor;
7973
}
74+
}
75+
}
8076

81-
if(fallback != null){
82-
fallback.setAccessible(true);
83-
return (Constructor<? extends T>) fallback;
84-
}
77+
if (fallback != null) {
78+
fallback.setAccessible(true);
79+
return (Constructor<? extends T>) fallback;
80+
}
8581

86-
throw new RuntimeException("No constructor in " + type.getSimpleName()
87-
+ " can be satisfied by entities: " + exprByName.keySet());
82+
throw new RuntimeException(
83+
"No constructor in "
84+
+ type.getSimpleName()
85+
+ " can be satisfied by entities: "
86+
+ exprByName.keySet());
8887
}
8988

9089
private List<Expression<?>> buildArgsForConstructor(
91-
Constructor<? extends T> ctor,
92-
Map<String, Expression<?>> exprByName
93-
) {
94-
List<Expression<?>> list = new ArrayList<>();
90+
Constructor<? extends T> ctor, Map<String, Expression<?>> exprByName) {
91+
List<Expression<?>> list = new ArrayList<>();
9592

96-
Parameter[] params = ctor.getParameters();
93+
Parameter[] params = ctor.getParameters();
9794

98-
boolean unnamed = params.length > 0 && params[0].getName().startsWith("arg");
99-
Field[] fields = unnamed ? ctor.getDeclaringClass().getDeclaredFields() : new Field[0];
95+
boolean unnamed = params.length > 0 && params[0].getName().startsWith("arg");
96+
Field[] fields = unnamed ? ctor.getDeclaringClass().getDeclaredFields() : new Field[0];
10097

101-
for (int i = 0; i < params.length; i++) {
102-
String name = unnamed ? fields[i].getName() : params[i].getName();
103-
Expression<?> expr = exprByName.get(name);
104-
if (expr == null) {
105-
throw new RuntimeException("No expression for parameter: " + name);
106-
}
107-
list.add(expr);
108-
}
109-
return list;
98+
for (int i = 0; i < params.length; i++) {
99+
String name = unnamed ? fields[i].getName() : params[i].getName();
100+
Expression<?> expr = exprByName.get(name);
101+
if (expr == null) {
102+
throw new RuntimeException("No expression for parameter: " + name);
103+
}
104+
list.add(expr);
110105
}
106+
return list;
107+
}
111108

112109
@Override
113110
public T newInstance(Object... args) {
@@ -128,6 +125,73 @@ public <R, C> R accept(Visitor<R, C> v, C context) {
128125
return null;
129126
}
130127

128+
/**
129+
* Creates a {@link NameBasedProjection} instance for the given DTO class and entity.
130+
*
131+
* <p>Author: <b>Mouon (munhyunjune)</b>
132+
*
133+
* <h3>Purpose</h3>
134+
*
135+
* Simplifies QueryDSL projection by automatically binding entity fields to DTO constructor or
136+
* field names. This eliminates the need to manually specify every field in {@code
137+
* Projections.fields()} or {@code Projections.constructor()}, significantly reducing boilerplate
138+
* code.
139+
*
140+
* <h3>Usage Example</h3>
141+
*
142+
* <pre>{@code
143+
* QAnimal animal = QAnimal.animal;
144+
*
145+
* // Instead of writing:
146+
* Projections.fields(AnimalDTO.class, animal.id, animal.name, animal.weight)
147+
*
148+
* // You can simply write:
149+
* AnimalDTO dto = NameBasedProjection.binder(AnimalDTO.class, animal)
150+
* .newInstance(1, "Lion", 120);
151+
* }</pre>
152+
*
153+
* <h3>Parameters</h3>
154+
*
155+
* <ul>
156+
* <li><b>type</b> — The DTO class type to project into.
157+
* <li><b>entity</b> — The QueryDSL {@link EntityPathBase} whose fields will be automatically
158+
* mapped.
159+
* </ul>
160+
*
161+
* <h3>Return</h3>
162+
*
163+
* A new {@link NameBasedProjection} instance capable of instantiating the given DTO type using
164+
* the entity's expressions.
165+
*
166+
* <h3>Matching Logic</h3>
167+
*
168+
* <ul>
169+
* <li>DTO constructor parameters are matched by <b>name</b> (not by order).
170+
* <li>Internally, {@code Map<String, Expression<?>> exprByName} is used to pair entity field
171+
* names with corresponding constructor parameter names.
172+
* <li>If compiled without {@code -parameters}, parameter names default to {@code arg0, arg1,
173+
* ...}, so the class fields’ order will be used as a fallback.
174+
* </ul>
175+
*
176+
* <h3>Caution</h3>
177+
*
178+
* <ul>
179+
* <li>The DTO must have either:
180+
* <ul>
181+
* <li>a constructor whose parameter names match entity field names, or
182+
* <li>accessible fields whose names match entity field names.
183+
* </ul>
184+
* <li>If there is no matching field or constructor parameter for an entity expression, a {@link
185+
* RuntimeException} will be thrown.
186+
* <li>When using multiple entities (e.g. {@code new NameBasedProjection<>(Dto.class, user,
187+
* order)}), duplicate field names are resolved by the <b>first</b> entity provided.
188+
* </ul>
189+
*
190+
* @param type the DTO class type to bind expressions to
191+
* @param entity the QueryDSL entity path whose field expressions are used
192+
* @param <T> the generic DTO type
193+
* @return a new {@link NameBasedProjection} ready to create DTO instances
194+
*/
131195
public static <T> NameBasedProjection<T> binder(
132196
Class<? extends T> type, EntityPathBase<?> entity) {
133197
return new NameBasedProjection<>(type, entity);

querydsl-libraries/querydsl-core/src/test/java/com/querydsl/core/NameBasedProjectionTest.java

Lines changed: 136 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
import static org.assertj.core.api.Assertions.assertThat;
44

55
import com.querydsl.core.domain.QAnimal;
6-
import com.querydsl.core.types.Projections;
76
import com.querydsl.core.types.NameBasedProjection;
7+
import com.querydsl.core.types.Projections;
88
import java.sql.Date;
99
import org.junit.Test;
1010

@@ -126,7 +126,7 @@ public void testSimpleDTOProjectionWithFields() {
126126
150 // weight
127127
);
128128

129-
// Create DTO using SimpleDTOProjection.fields()
129+
// Create DTO using NameBasedProjection.binder
130130
AnimalDTO dtoFromSimpleDTOProjection =
131131
NameBasedProjection.binder(AnimalDTO.class, animal)
132132
.newInstance(
@@ -145,4 +145,138 @@ public void testSimpleDTOProjectionWithFields() {
145145
.usingRecursiveComparison()
146146
.isEqualTo(dtoFromProjections);
147147
}
148+
149+
/**
150+
* DTO class that merges Animal and Cat entity fields. Used only for testing multi-entity
151+
* NameBasedProjection behavior.
152+
*/
153+
public static class AnimalCatDTO {
154+
private boolean alive;
155+
private double bodyWeight;
156+
private Date dateField;
157+
private int id;
158+
private String name;
159+
private int toes;
160+
private int weight;
161+
private int breed; // Field from Cat entity
162+
163+
// Constructor with all fields
164+
public AnimalCatDTO(
165+
boolean alive,
166+
double bodyWeight,
167+
Date dateField,
168+
int id,
169+
String name,
170+
int toes,
171+
int weight,
172+
int breed) {
173+
this.alive = alive;
174+
this.bodyWeight = bodyWeight;
175+
this.dateField = dateField;
176+
this.id = id;
177+
this.name = name;
178+
this.toes = toes;
179+
this.weight = weight;
180+
this.breed = breed;
181+
}
182+
183+
// Default constructor
184+
public AnimalCatDTO() {}
185+
186+
// Getters
187+
public boolean isAlive() {
188+
return alive;
189+
}
190+
191+
public double getBodyWeight() {
192+
return bodyWeight;
193+
}
194+
195+
public Date getDateField() {
196+
return dateField;
197+
}
198+
199+
public int getId() {
200+
return id;
201+
}
202+
203+
public String getName() {
204+
return name;
205+
}
206+
207+
public int getToes() {
208+
return toes;
209+
}
210+
211+
public int getWeight() {
212+
return weight;
213+
}
214+
215+
public int getBreed() {
216+
return breed;
217+
}
218+
219+
@Override
220+
public String toString() {
221+
return "AnimalCatDTO{"
222+
+ "alive="
223+
+ alive
224+
+ ", bodyWeight="
225+
+ bodyWeight
226+
+ ", dateField="
227+
+ dateField
228+
+ ", id="
229+
+ id
230+
+ ", name='"
231+
+ name
232+
+ '\''
233+
+ ", toes="
234+
+ toes
235+
+ ", weight="
236+
+ weight
237+
+ ", breed="
238+
+ breed
239+
+ '}';
240+
}
241+
}
242+
243+
/**
244+
* Test combining multiple entities (QAnimal and QCat) into one DTO using NameBasedProjection.
245+
* This verifies that: - Multiple EntityPathBase arguments are supported. - Fields from both
246+
* entities are mapped correctly. - When duplicate field names exist, the first entity's field
247+
* (QAnimal) takes precedence.
248+
*/
249+
@Test
250+
public void testMultiEntityProjectionWithAnimalAndCat() {
251+
QAnimal animal = QAnimal.animal;
252+
com.querydsl.core.domain.QCat cat = com.querydsl.core.domain.QCat.cat;
253+
254+
// Create a Date object for testing
255+
Date testDate = new Date(System.currentTimeMillis());
256+
257+
// Create a DTO that combines fields from both Animal and Cat entities
258+
AnimalCatDTO dto =
259+
new NameBasedProjection<>(AnimalCatDTO.class, animal, cat)
260+
.newInstance(
261+
true, // alive (from Animal)
262+
55.2, // bodyWeight (from Animal)
263+
testDate, // dateField (from Animal)
264+
10, // id (from Animal)
265+
"Kitty", // name (from Animal)
266+
4, // toes (from Animal)
267+
7, // weight (from Animal)
268+
2 // breed (from Cat)
269+
);
270+
271+
// Verify that all fields are correctly mapped
272+
assertThat(dto).isNotNull();
273+
assertThat(dto.isAlive()).isTrue();
274+
assertThat(dto.getBodyWeight()).isEqualTo(55.2);
275+
assertThat(dto.getDateField()).isEqualTo(testDate);
276+
assertThat(dto.getId()).isEqualTo(10);
277+
assertThat(dto.getName()).isEqualTo("Kitty");
278+
assertThat(dto.getToes()).isEqualTo(4);
279+
assertThat(dto.getWeight()).isEqualTo(7);
280+
assertThat(dto.getBreed()).isEqualTo(2);
281+
}
148282
}

0 commit comments

Comments
 (0)