Skip to content

Commit 7926140

Browse files
MouonMouoon
andauthored
Refactor: Promote SimpleDTOProjection to official NameBasedProjection feature with collection support (#1460)
* feat : 심플디티오 네이밍 수정 및 칼렌셕 기능 추가 * refactor: promote SimpleDTOProjection to official NameBasedProjection feature * fix : Reduce delta in SimpleDtoProjection so git can match original file * fix : Reduce delta in SimpleDtoProjection so git can match original file * fix : Rename SimpleDtoProjection to NameBasedProjection again --------- Co-authored-by: Mouoon <[email protected]>
1 parent 6b8b074 commit 7926140

File tree

4 files changed

+481
-220
lines changed

4 files changed

+481
-220
lines changed
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
package com.querydsl.core.types;
2+
3+
import com.querydsl.core.types.dsl.EntityPathBase;
4+
import java.lang.reflect.Constructor;
5+
import java.lang.reflect.Field;
6+
import java.lang.reflect.Parameter;
7+
import java.util.ArrayList;
8+
import java.util.HashMap;
9+
import java.util.List;
10+
import java.util.Map;
11+
12+
public class NameBasedProjection<T> extends FactoryExpressionBase<T> {
13+
14+
private final List<Expression<?>> expressions;
15+
private final Constructor<? extends T> constructor;
16+
17+
@SafeVarargs
18+
public NameBasedProjection(Class<? extends T> type, EntityPathBase<?>... entities) {
19+
super(type);
20+
Map<String, Expression<?>> exprByName = collectExpressions(entities);
21+
this.constructor = findMatchingConstructor(type, exprByName);
22+
this.expressions = buildArgsForConstructor(this.constructor, exprByName);
23+
}
24+
25+
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+
}
41+
}
42+
}
43+
return map;
44+
}
45+
46+
private Constructor<? extends T> findMatchingConstructor(
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;
59+
}
60+
}
61+
if (allMatch) {
62+
ctor.setAccessible(true);
63+
return (Constructor<? extends T>) ctor;
64+
}
65+
}
66+
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;
73+
}
74+
}
75+
}
76+
77+
if (fallback != null) {
78+
fallback.setAccessible(true);
79+
return (Constructor<? extends T>) fallback;
80+
}
81+
82+
throw new RuntimeException(
83+
"No constructor in "
84+
+ type.getSimpleName()
85+
+ " can be satisfied by entities: "
86+
+ exprByName.keySet());
87+
}
88+
89+
private List<Expression<?>> buildArgsForConstructor(
90+
Constructor<? extends T> ctor, Map<String, Expression<?>> exprByName) {
91+
List<Expression<?>> list = new ArrayList<>();
92+
93+
Parameter[] params = ctor.getParameters();
94+
95+
boolean unnamed = params.length > 0 && params[0].getName().startsWith("arg");
96+
Field[] fields = unnamed ? ctor.getDeclaringClass().getDeclaredFields() : new Field[0];
97+
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);
105+
}
106+
return list;
107+
}
108+
109+
@Override
110+
public T newInstance(Object... args) {
111+
try {
112+
return constructor.newInstance(args);
113+
} catch (Exception e) {
114+
throw new RuntimeException("Failed to create instance of " + getType().getSimpleName(), e);
115+
}
116+
}
117+
118+
@Override
119+
public List<Expression<?>> getArgs() {
120+
return expressions;
121+
}
122+
123+
@Override
124+
public <R, C> R accept(Visitor<R, C> v, C context) {
125+
return null;
126+
}
127+
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+
*/
195+
public static <T> NameBasedProjection<T> binder(
196+
Class<? extends T> type, EntityPathBase<?> entity) {
197+
return new NameBasedProjection<>(type, entity);
198+
}
199+
}

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

Lines changed: 0 additions & 72 deletions
This file was deleted.

0 commit comments

Comments
 (0)