@@ -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 );
0 commit comments