Skip to content

Commit d65774f

Browse files
author
talhadilber
committed
JdqSubModel annotation created for nested select query objects
1 parent d3ccd05 commit d65774f

File tree

5 files changed

+378
-94
lines changed

5 files changed

+378
-94
lines changed

README.md

Lines changed: 56 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ You can find the sample code from: https://github.com/tdilber/spring-jpa-dynamic
9898
<dependency>
9999
<groupId>io.github.tdilber</groupId>
100100
<artifactId>spring-jpa-dynamic-query</artifactId>
101-
<version>0.5.0</version>
101+
<version>0.6.0</version>
102102
</dependency>
103103
```
104104

@@ -450,6 +450,8 @@ where authorizat4_.menu_icon like ?
450450
Spring Data projections always boring. But this project projections are very simple.
451451
There are two ways to use projections. I suggested using the second way. Because the second way is easier and more reusable.
452452

453+
**Note:** Record class is supported for projection. You can use record class for projection.
454+
453455
#### A- Manual Projection
454456
When you want to use specific fields in the result, you can add selected fields on select list on `DynamicQuery` object. You can add multiple fields to the
455457
select clause. You can also use the `Pair` class to give an alias to the field.
@@ -497,54 +499,94 @@ where authorizat4_.menu_icon like ?
497499

498500
_Note: you can find the example on demo github repository._
499501

500-
501-
#### B- Auto Projection with Annotated Model
502-
Model Annotations: `@JdqModel`, `@JdqField`, `@JdqIgnoreField`
502+
#### B- Auto Projection with Annotated Model
503+
Model Annotations: `@JdqModel`, `@JdqField`, `@JdqIgnoreField`, `@JdqSubModel`
503504

504505
We are discovering select clause if model has `@JdqModel` annotation AND select clause is empty.
505-
Autofill Rules are Simple:
506+
Autofill Rules are Simple:
506507
- If field has `@JdqField` annotation, we are using this field name in the select clause.
507508
- If field has not any annotation, we are using field name in the select clause.
508509
- If field has `@JdqIgnoreField` annotation, we are ignoring this field in the select clause.
510+
- If field has `@JdqSubModel` annotation, we are including the sub-model fields in the select clause.
509511

510512
**Usage of `@JdqField` annotation:**
511513

512514
`@JdqField` annotation has a parameter. This parameter is a string. This string is a field name in the select clause. If you want to use different field name in the select clause, you can use this annotation. And also If you need to use joined column in the select clause, you can use this annotation.
513515

514-
_Examples:_
516+
**Usage of `@JdqSubModel` annotation:**
517+
518+
`@JdqSubModel` annotation is used to include fields from a nested model in the select clause. This allows for more complex projections involving nested objects.
519+
520+
There are 2 usage of `@JdqSubModel` annotation:
521+
- If you want to use nested model fields without join support, Use `@JdqSubModel()` annotation without any parameter.
522+
- If you want to use nested model fields with join support, Use `@JdqSubModel("joined_column_name")` annotation with joined column name parameter.
523+
524+
_Examples:_
515525

516526
```java
517527
@JdqModel // This annotation is required for using projection with joined column
518528
@Data
519529
public static class UserJdqModel {
520530
@JdqField("name") // This annotation is not required. But if you want to use different field name in the result, you can use this annotation.
521531
private String nameButDifferentFieldName;
522-
@JdqField("user.name") // This annotation is required for using joined column in the projection
523-
private String userNameWithJoin;
532+
@JdqField("team.name") // This annotation is required for using joined column in the projection
533+
private String teamNameWithJoin;
524534

525535
private Integer age; // This field is in the select clause. Because this field has not any annotation.
526-
536+
527537
@JdqIgnoreField // This annotation is required for ignoring this field in the select clause.
528538
private String surname;
539+
540+
@JdqSubModel // This annotation is used to include fields from a nested model without join support
541+
private AddressJdqModel address;
542+
543+
@JdqSubModel("department") // This annotation is used to include fields from a nested model with join support
544+
private DepartmentJdqModel departmentJdqModel;
545+
}
546+
547+
@JdqModel
548+
@Data
549+
public static class AddressJdqModel {
550+
@JdqField("address.street")
551+
private String street;
552+
@JdqField("address.city")
553+
private String city;
529554
}
530555

556+
@JdqModel
557+
public record DepartmentJdqModel(@JdqField("id") Long departmentId, @JdqField String name) {
558+
559+
}
560+
531561
// USAGE EXAMPLE
532-
List<UserJdqModel> result = customerRepository.findAll(dynamicQuery, UserJdqModel.class);
562+
List<UserJdqModel> result = userRepository.findAll(dynamicQuery, UserJdqModel.class);
533563
```
534564
_Autofilled select Result If you fill Manuel:_
535565
```java
536566
select.add(Pair.of("name", "nameButDifferentFieldName"));
537567
select.add(Pair.of("user.name", "userNameWithJoin"));
538568
select.add(Pair.of("age", "age"));
569+
select.add(Pair.of("address.street", "address.street"));
570+
select.add(Pair.of("address.city", "address.city"));
571+
select.add(Pair.of("department.id", "departmentJdqModel.departmentId"));
572+
select.add(Pair.of("department.name", "departmentJdqModel.name"));
539573
```
540574

541575
_Hibernate Query:_
542576

543577
```sql
544-
select customer0_.name as col_0_0_, user1_.name as col_1_0_, customer0_.age as col_2_0_
545-
from customer customer0_
546-
inner join test_user user1_ on customer0_.user_id = user1_.id
547-
where customer0_.age > 25
578+
select user0_.name as col_0_0_,
579+
team3_.name as col_1_0_,
580+
user0_.age as col_2_0_,
581+
address1_.street as col_3_0_,
582+
address1_.city as col_4_0_,
583+
department2_.id as col_5_0_,
584+
department2_.name as col_6_0_
585+
from test_user user0_
586+
inner join team team3_ on user0_.team_id = team3_.id
587+
inner join address address1_ on user0_.address_id = address1_.id
588+
inner join department department2_ on user0_.department_id = department2_.id
589+
where user0_.age > 25
548590
```
549591

550592
### 9- Pagination Examples

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
<groupId>io.github.tdilber</groupId>
1515
<artifactId>spring-jpa-dynamic-query</artifactId>
16-
<version>0.5.0</version>
16+
<version>0.6.0</version>
1717
<packaging>jar</packaging>
1818
<name>Spring Jpa Dynamic Query</name>
1919
<description>Spring Jpa Dynamic Query (JDQ) Project</description>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.beyt.jdq.annotation.model;
2+
3+
4+
import java.lang.annotation.ElementType;
5+
import java.lang.annotation.Retention;
6+
import java.lang.annotation.RetentionPolicy;
7+
import java.lang.annotation.Target;
8+
9+
@Retention(RetentionPolicy.RUNTIME)
10+
@Target({ElementType.FIELD})
11+
public @interface JdqSubModel {
12+
String value() default "";
13+
}

src/main/java/com/beyt/jdq/query/DynamicQueryManager.java

Lines changed: 122 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,18 @@
33
import com.beyt.jdq.annotation.model.JdqModel;
44
import com.beyt.jdq.annotation.model.JdqField;
55
import com.beyt.jdq.annotation.model.JdqIgnoreField;
6+
import com.beyt.jdq.annotation.model.JdqSubModel;
67
import com.beyt.jdq.dto.Criteria;
78
import com.beyt.jdq.dto.DynamicQuery;
89
import com.beyt.jdq.dto.enums.CriteriaOperator;
910
import com.beyt.jdq.exception.DynamicQueryIllegalArgumentException;
1011
import com.beyt.jdq.query.rule.specification.*;
1112
import com.beyt.jdq.repository.DynamicSpecificationRepositoryImpl;
1213
import com.beyt.jdq.util.ApplicationContextUtil;
14+
import com.beyt.jdq.util.field.FieldUtil;
1315
import lombok.extern.slf4j.Slf4j;
1416
import org.apache.commons.collections4.IterableUtils;
17+
import org.apache.commons.lang3.StringUtils;
1518
import org.hibernate.metamodel.model.domain.internal.SingularAttributeImpl;
1619
import org.hibernate.query.criteria.internal.path.RootImpl;
1720
import org.hibernate.query.criteria.internal.path.SingularAttributePath;
@@ -32,9 +35,7 @@
3235
import javax.persistence.Tuple;
3336
import javax.persistence.TypedQuery;
3437
import javax.persistence.criteria.*;
35-
import java.lang.reflect.Field;
36-
import java.lang.reflect.Method;
37-
import java.lang.reflect.RecordComponent;
38+
import java.lang.reflect.*;
3839
import java.util.*;
3940
import java.util.stream.Collectors;
4041
import java.util.stream.Stream;
@@ -169,9 +170,25 @@ protected static <Entity, ResultType> Iterable<ResultType> getEntityListBySelect
169170
}
170171

171172
private static <ResultType> void extractIfJdqModel(DynamicQuery dynamicQuery, Class<ResultType> resultTypeClass) {
172-
if (resultTypeClass.isAnnotationPresent(JdqModel.class)) {
173-
List<Pair<String, String>> select = new ArrayList<>();
174-
for (Field declaredField : resultTypeClass.getDeclaredFields()) {
173+
if (!resultTypeClass.isAnnotationPresent(JdqModel.class)) {
174+
return;
175+
}
176+
177+
List<Pair<String, String>> select = new ArrayList<>();
178+
recursiveSupModelFiller(resultTypeClass, select, new ArrayList<>(), "");
179+
dynamicQuery.setSelect(select);
180+
}
181+
182+
private static <ResultType> void recursiveSupModelFiller(Class<ResultType> resultTypeClass, List<Pair<String, String>> select, List<String> dbPrefixList, String entityPrefix) {
183+
for (Field declaredField : resultTypeClass.getDeclaredFields()) {
184+
if (declaredField.isAnnotationPresent(JdqSubModel.class)) {
185+
String subModelValue = declaredField.getAnnotation(JdqSubModel.class).value();
186+
ArrayList<String> newPrefixList = new ArrayList<>(dbPrefixList);
187+
if (StringUtils.isNotBlank(subModelValue)) {
188+
newPrefixList.add(subModelValue);
189+
}
190+
recursiveSupModelFiller(declaredField.getType(), select, newPrefixList, entityPrefix + declaredField.getName() + ".");
191+
} else if (FieldUtil.isSupportedType(declaredField.getType())) {
175192
if (declaredField.isAnnotationPresent(JdqIgnoreField.class)) {
176193
if (resultTypeClass.isRecord()) {
177194
throw new DynamicQueryIllegalArgumentException("Record class can not have @JdqIgnoreField annotation");
@@ -180,15 +197,26 @@ private static <ResultType> void extractIfJdqModel(DynamicQuery dynamicQuery, Cl
180197
}
181198

182199
if (declaredField.isAnnotationPresent(JdqField.class)) {
183-
select.add(Pair.of(declaredField.getAnnotation(JdqField.class).value(), declaredField.getName()));
200+
select.add(Pair.of(prefixCreator(dbPrefixList) + declaredField.getAnnotation(JdqField.class).value(), entityPrefix + declaredField.getName()));
184201
} else {
185-
select.add(Pair.of(declaredField.getName(), declaredField.getName()));
202+
select.add(Pair.of(prefixCreator(dbPrefixList) + declaredField.getName(), entityPrefix + declaredField.getName()));
203+
}
204+
} else {
205+
if (resultTypeClass.isRecord()) {
206+
throw new DynamicQueryIllegalArgumentException("Record didnt support nested model type: " + declaredField.getType().getName());
186207
}
187208
}
188-
dynamicQuery.setSelect(select);
189209
}
190210
}
191211

212+
private static String prefixCreator(List<String> prefixList) {
213+
String collect = String.join(".", prefixList);
214+
if (StringUtils.isNotBlank(collect)) {
215+
collect += ".";
216+
}
217+
return collect;
218+
}
219+
192220
protected static <Entity, ResultType> Iterable<ResultType> getEntityListWithReturnClass(JpaSpecificationExecutor<Entity> repositoryExecutor, DynamicQuery dynamicQuery, Class<ResultType> resultTypeClass, boolean isPage) {
193221
Class<Entity> entityClass = getEntityClass(repositoryExecutor);
194222
EntityManager entityManager = ApplicationContextUtil.getEntityManager();
@@ -311,50 +339,106 @@ protected static long executeCountQuery(TypedQuery<Long> query) {
311339
}
312340

313341
protected static <ResultType> Iterable<ResultType> convertResultToResultTypeList(List<Pair<String, String>> querySelects, Class<ResultType> resultTypeClass, Iterable<Tuple> entityListBySelectableFilter, boolean isPage) {
314-
Map<Integer, Method> setterMethods = new HashMap<>();
315-
if (!resultTypeClass.isRecord()) {
316-
for (int i = 0; i < querySelects.size(); i++) {
317-
String select = querySelects.get(i).getSecond();
342+
Stream<Tuple> stream = isPage ? ((Page<Tuple>) entityListBySelectableFilter).stream() : ((List<Tuple>) entityListBySelectableFilter).stream();
318343

319-
Optional<Method> methodOptional = Arrays.stream(resultTypeClass.getMethods())
320-
.filter(c -> c.getName().equalsIgnoreCase("set" + select)
321-
&& c.getParameterCount() == 1).findFirst();
344+
List<ResultType> resultTypeList;
322345

323-
if (methodOptional.isPresent()) {
324-
setterMethods.put(i, methodOptional.get());
325-
}
346+
Map<String, Integer> selectsWithIndex = new HashMap<>();
347+
for (int i = 0; i < querySelects.size(); i++) {
348+
selectsWithIndex.put(querySelects.get(i).getSecond(), i);
349+
}
350+
351+
Map<Class<?>, Map<Integer, Method>> classSetterMethodsMap = new HashMap<>();
352+
Map<Class<?>, Constructor<?>> recordConstructorMap = new HashMap<>();
353+
354+
resultTypeList = stream.map(t -> fillModel(resultTypeClass, t, selectsWithIndex, classSetterMethodsMap, recordConstructorMap)).filter(Objects::nonNull).collect(Collectors.toList());
355+
356+
357+
if (isPage) {
358+
Page<Tuple> tuplePage = (Page<Tuple>) entityListBySelectableFilter;
359+
return new PageImpl<>(resultTypeList, tuplePage.getPageable(), tuplePage.getTotalElements());
360+
} else {
361+
return resultTypeList;
362+
}
363+
}
364+
365+
protected static <ModelType> ModelType fillModel(Class<ModelType> modelType, Tuple t, Map<String, Integer> selectsWithIndex, Map<Class<?>, Map<Integer, Method>> classSetterMethodsMap, Map<Class<?>, Constructor<?>> recordConstructorMap) {
366+
Map<String, Object> subModelMap = new HashMap<>();
367+
for (Field declaredField : modelType.getDeclaredFields()) {
368+
if (declaredField.isAnnotationPresent(JdqSubModel.class)) {
369+
subModelMap.put(declaredField.getName(), fillModel(declaredField.getType(), t, selectsWithIndex.entrySet().stream().filter(e -> e.getKey().startsWith(declaredField.getName() + "."))
370+
.collect(Collectors.toMap(k -> k.getKey().substring(declaredField.getName().length() + 1), Map.Entry::getValue)), classSetterMethodsMap, recordConstructorMap));
326371
}
327372
}
328-
Stream<Tuple> stream = isPage ? ((Page<Tuple>) entityListBySelectableFilter).stream() : ((List<Tuple>) entityListBySelectableFilter).stream();
329373

330-
List<ResultType> resultTypeList = stream.map(t -> {
374+
375+
if (modelType.isRecord()) {
331376
try {
332-
if (resultTypeClass.isRecord()) {
333-
Object[] args = new Object[querySelects.size()];
334-
for (int i = 0; i < querySelects.size(); i++) {
335-
args[i] = t.get(i);
336-
}
337-
return resultTypeClass.getDeclaredConstructor(Arrays.stream(resultTypeClass.getRecordComponents())
377+
Constructor<ModelType> constructor = (Constructor<ModelType>) recordConstructorMap.get(modelType);
378+
if (Objects.isNull(constructor)) {
379+
constructor = modelType.getConstructor(Arrays.stream(modelType.getRecordComponents())
338380
.map(RecordComponent::getType)
339-
.toArray(Class[]::new)).newInstance(args);
340-
} else {
341-
ResultType resultObj = resultTypeClass.getConstructor().newInstance();
342-
for (Map.Entry<Integer, Method> entry : setterMethods.entrySet()) {
343-
entry.getValue().invoke(resultObj, t.get(entry.getKey()));
381+
.toArray(Class[]::new));
382+
recordConstructorMap.put(modelType, constructor);
383+
}
384+
385+
Parameter[] parameters = constructor.getParameters();
386+
Object[] args = new Object[parameters.length];
387+
for (int i = 0; i < parameters.length; i++) {
388+
if (selectsWithIndex.containsKey(parameters[i].getName())) {
389+
Integer index = selectsWithIndex.get(parameters[i].getName());
390+
args[i] = t.get(index);
391+
} else {
392+
args[i] = subModelMap.get(parameters[i].getName());
344393
}
345-
return resultObj;
346394
}
395+
396+
return constructor.newInstance(args);
347397
} catch (Exception e) {
348398
return null;
349399
}
350-
}).filter(Objects::nonNull).collect(Collectors.toList());
400+
} else {
401+
List<Map.Entry<String, Integer>> fieldList = selectsWithIndex.entrySet().stream().filter(e -> !e.getKey().contains(".")).distinct().sorted(Comparator.comparing(Map.Entry::getValue)).toList();
402+
Map<Integer, Method> setterMethods = getIntegerMethodMap(fieldList.stream().map(e -> Pair.of(e.getValue(), e.getKey())).collect(Collectors.toList()), modelType, classSetterMethodsMap);
403+
try {
404+
ModelType resultObj = modelType.getConstructor().newInstance();
405+
for (Map.Entry<Integer, Method> entry : setterMethods.entrySet()) {
406+
entry.getValue().invoke(resultObj, t.get(entry.getKey()));
407+
}
408+
for (Map.Entry<String, Object> stringObjectEntry : subModelMap.entrySet()) {
409+
Field declaredField = resultObj.getClass().getDeclaredField(stringObjectEntry.getKey());
410+
boolean canAccess = declaredField.canAccess(resultObj);
411+
declaredField.setAccessible(true);
412+
declaredField.set(resultObj, stringObjectEntry.getValue());
413+
declaredField.setAccessible(canAccess);
414+
}
415+
return resultObj;
416+
} catch (Exception e) {
417+
return null;
418+
}
419+
}
420+
}
351421

352-
if (isPage) {
353-
Page<Tuple> tuplePage = (Page<Tuple>) entityListBySelectableFilter;
354-
return new PageImpl<>(resultTypeList, tuplePage.getPageable(), tuplePage.getTotalElements());
422+
423+
private static <ResultType> Map<Integer, Method> getIntegerMethodMap(List<Pair<Integer, String>> querySelects, Class<ResultType> resultTypeClass, Map<Class<?>, Map<Integer, Method>> classSetterMethodsMap) {
424+
Map<Integer, Method> setterMethods = new HashMap<>();
425+
if (classSetterMethodsMap.containsKey(resultTypeClass)) {
426+
setterMethods = classSetterMethodsMap.get(resultTypeClass);
355427
} else {
356-
return resultTypeList;
428+
for (int i = 0; i < querySelects.size(); i++) {
429+
String select = querySelects.get(i).getSecond();
430+
431+
Optional<Method> methodOptional = Arrays.stream(resultTypeClass.getMethods())
432+
.filter(c -> c.getName().equalsIgnoreCase("set" + select)
433+
&& c.getParameterCount() == 1).findFirst();
434+
435+
if (methodOptional.isPresent()) {
436+
setterMethods.put(querySelects.get(i).getFirst(), methodOptional.get());
437+
}
438+
}
439+
classSetterMethodsMap.put(resultTypeClass, setterMethods);
357440
}
441+
return setterMethods;
358442
}
359443

360444
@SuppressWarnings("unchecked")

0 commit comments

Comments
 (0)