Skip to content

Commit a52fdb8

Browse files
author
talhadilber
committed
Merge branch 'spring-boot-2.x' of https://github.com/tdilber/jpa-generic-criteria-extension into spring-boot-3.x
# Conflicts: # README.md # pom.xml # src/main/java/com/beyt/jdq/query/DynamicQueryManager.java
2 parents a145b87 + d65774f commit a52fdb8

File tree

5 files changed

+377
-92
lines changed

5 files changed

+377
-92
lines changed

README.md

Lines changed: 57 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ You can find the sample code from: https://github.com/tdilber/spring-jpa-dynamic
9999
<dependency>
100100
<groupId>io.github.tdilber</groupId>
101101
<artifactId>spring-boot-starter-jpa-dynamic-query</artifactId>
102-
<version>0.2.0</version>
102+
<version>0.3.0</version>
103103
</dependency>
104104
```
105105

@@ -108,7 +108,7 @@ You can find the sample code from: https://github.com/tdilber/spring-jpa-dynamic
108108
<dependency>
109109
<groupId>io.github.tdilber</groupId>
110110
<artifactId>spring-jpa-dynamic-query</artifactId>
111-
<version>0.5.0</version>
111+
<version>0.6.0</version>
112112
</dependency>
113113
```
114114

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

463+
**Note:** Record class is supported for projection. You can use record class for projection.
464+
463465
#### A- Manual Projection
464466
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
465467
select clause. You can also use the `Pair` class to give an alias to the field.
@@ -507,54 +509,94 @@ where authorizat4_.menu_icon like ?
507509

508510
_Note: you can find the example on demo github repository._
509511

510-
511-
#### B- Auto Projection with Annotated Model
512-
Model Annotations: `@JdqModel`, `@JdqField`, `@JdqIgnoreField`
512+
#### B- Auto Projection with Annotated Model
513+
Model Annotations: `@JdqModel`, `@JdqField`, `@JdqIgnoreField`, `@JdqSubModel`
513514

514515
We are discovering select clause if model has `@JdqModel` annotation AND select clause is empty.
515-
Autofill Rules are Simple:
516+
Autofill Rules are Simple:
516517
- If field has `@JdqField` annotation, we are using this field name in the select clause.
517518
- If field has not any annotation, we are using field name in the select clause.
518519
- If field has `@JdqIgnoreField` annotation, we are ignoring this field in the select clause.
520+
- If field has `@JdqSubModel` annotation, we are including the sub-model fields in the select clause.
519521

520522
**Usage of `@JdqField` annotation:**
521523

522524
`@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.
523525

524-
_Examples:_
526+
**Usage of `@JdqSubModel` annotation:**
527+
528+
`@JdqSubModel` annotation is used to include fields from a nested model in the select clause. This allows for more complex projections involving nested objects.
529+
530+
There are 2 usage of `@JdqSubModel` annotation:
531+
- If you want to use nested model fields without join support, Use `@JdqSubModel()` annotation without any parameter.
532+
- If you want to use nested model fields with join support, Use `@JdqSubModel("joined_column_name")` annotation with joined column name parameter.
533+
534+
_Examples:_
525535

526536
```java
527537
@JdqModel // This annotation is required for using projection with joined column
528538
@Data
529539
public static class UserJdqModel {
530540
@JdqField("name") // This annotation is not required. But if you want to use different field name in the result, you can use this annotation.
531541
private String nameButDifferentFieldName;
532-
@JdqField("user.name") // This annotation is required for using joined column in the projection
533-
private String userNameWithJoin;
542+
@JdqField("team.name") // This annotation is required for using joined column in the projection
543+
private String teamNameWithJoin;
534544

535545
private Integer age; // This field is in the select clause. Because this field has not any annotation.
536-
546+
537547
@JdqIgnoreField // This annotation is required for ignoring this field in the select clause.
538548
private String surname;
549+
550+
@JdqSubModel // This annotation is used to include fields from a nested model without join support
551+
private AddressJdqModel address;
552+
553+
@JdqSubModel("department") // This annotation is used to include fields from a nested model with join support
554+
private DepartmentJdqModel departmentJdqModel;
555+
}
556+
557+
@JdqModel
558+
@Data
559+
public static class AddressJdqModel {
560+
@JdqField("address.street")
561+
private String street;
562+
@JdqField("address.city")
563+
private String city;
539564
}
540565

566+
@JdqModel
567+
public record DepartmentJdqModel(@JdqField("id") Long departmentId, @JdqField String name) {
568+
569+
}
570+
541571
// USAGE EXAMPLE
542-
List<UserJdqModel> result = customerRepository.findAll(dynamicQuery, UserJdqModel.class);
572+
List<UserJdqModel> result = userRepository.findAll(dynamicQuery, UserJdqModel.class);
543573
```
544574
_Autofilled select Result If you fill Manuel:_
545575
```java
546576
select.add(Pair.of("name", "nameButDifferentFieldName"));
547577
select.add(Pair.of("user.name", "userNameWithJoin"));
548578
select.add(Pair.of("age", "age"));
579+
select.add(Pair.of("address.street", "address.street"));
580+
select.add(Pair.of("address.city", "address.city"));
581+
select.add(Pair.of("department.id", "departmentJdqModel.departmentId"));
582+
select.add(Pair.of("department.name", "departmentJdqModel.name"));
549583
```
550584

551585
_Hibernate Query:_
552586

553587
```sql
554-
select customer0_.name as col_0_0_, user1_.name as col_1_0_, customer0_.age as col_2_0_
555-
from customer customer0_
556-
inner join test_user user1_ on customer0_.user_id = user1_.id
557-
where customer0_.age > 25
588+
select user0_.name as col_0_0_,
589+
team3_.name as col_1_0_,
590+
user0_.age as col_2_0_,
591+
address1_.street as col_3_0_,
592+
address1_.city as col_4_0_,
593+
department2_.id as col_5_0_,
594+
department2_.name as col_6_0_
595+
from test_user user0_
596+
inner join team team3_ on user0_.team_id = team3_.id
597+
inner join address address1_ on user0_.address_id = address1_.id
598+
inner join department department2_ on user0_.department_id = department2_.id
599+
where user0_.age > 25
558600
```
559601

560602
### 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-boot-starter-jpa-dynamic-query</artifactId>
16-
<version>0.2.0</version>
16+
<version>0.3.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: 120 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@
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;
1517
import org.hibernate.query.criteria.JpaRoot;
@@ -169,9 +171,25 @@ protected static <Entity, ResultType> Iterable<ResultType> getEntityListBySelect
169171
}
170172

171173
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()) {
174+
if (!resultTypeClass.isAnnotationPresent(JdqModel.class)) {
175+
return;
176+
}
177+
178+
List<Pair<String, String>> select = new ArrayList<>();
179+
recursiveSupModelFiller(resultTypeClass, select, new ArrayList<>(), "");
180+
dynamicQuery.setSelect(select);
181+
}
182+
183+
private static <ResultType> void recursiveSupModelFiller(Class<ResultType> resultTypeClass, List<Pair<String, String>> select, List<String> dbPrefixList, String entityPrefix) {
184+
for (Field declaredField : resultTypeClass.getDeclaredFields()) {
185+
if (declaredField.isAnnotationPresent(JdqSubModel.class)) {
186+
String subModelValue = declaredField.getAnnotation(JdqSubModel.class).value();
187+
ArrayList<String> newPrefixList = new ArrayList<>(dbPrefixList);
188+
if (StringUtils.isNotBlank(subModelValue)) {
189+
newPrefixList.add(subModelValue);
190+
}
191+
recursiveSupModelFiller(declaredField.getType(), select, newPrefixList, entityPrefix + declaredField.getName() + ".");
192+
} else if (FieldUtil.isSupportedType(declaredField.getType())) {
175193
if (declaredField.isAnnotationPresent(JdqIgnoreField.class)) {
176194
if (resultTypeClass.isRecord()) {
177195
throw new DynamicQueryIllegalArgumentException("Record class can not have @JdqIgnoreField annotation");
@@ -180,15 +198,26 @@ private static <ResultType> void extractIfJdqModel(DynamicQuery dynamicQuery, Cl
180198
}
181199

182200
if (declaredField.isAnnotationPresent(JdqField.class)) {
183-
select.add(Pair.of(declaredField.getAnnotation(JdqField.class).value(), declaredField.getName()));
201+
select.add(Pair.of(prefixCreator(dbPrefixList) + declaredField.getAnnotation(JdqField.class).value(), entityPrefix + declaredField.getName()));
184202
} else {
185-
select.add(Pair.of(declaredField.getName(), declaredField.getName()));
203+
select.add(Pair.of(prefixCreator(dbPrefixList) + declaredField.getName(), entityPrefix + declaredField.getName()));
204+
}
205+
} else {
206+
if (resultTypeClass.isRecord()) {
207+
throw new DynamicQueryIllegalArgumentException("Record didnt support nested model type: " + declaredField.getType().getName());
186208
}
187209
}
188-
dynamicQuery.setSelect(select);
189210
}
190211
}
191212

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

313342
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();
343+
Stream<Tuple> stream = isPage ? ((Page<Tuple>) entityListBySelectableFilter).stream() : ((List<Tuple>) entityListBySelectableFilter).stream();
318344

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

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

330-
List<ResultType> resultTypeList = stream.map(t -> {
375+
376+
if (modelType.isRecord()) {
331377
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())
378+
Constructor<ModelType> constructor = (Constructor<ModelType>) recordConstructorMap.get(modelType);
379+
if (Objects.isNull(constructor)) {
380+
constructor = modelType.getConstructor(Arrays.stream(modelType.getRecordComponents())
338381
.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()));
382+
.toArray(Class[]::new));
383+
recordConstructorMap.put(modelType, constructor);
384+
}
385+
386+
Parameter[] parameters = constructor.getParameters();
387+
Object[] args = new Object[parameters.length];
388+
for (int i = 0; i < parameters.length; i++) {
389+
if (selectsWithIndex.containsKey(parameters[i].getName())) {
390+
Integer index = selectsWithIndex.get(parameters[i].getName());
391+
args[i] = t.get(index);
392+
} else {
393+
args[i] = subModelMap.get(parameters[i].getName());
344394
}
345-
return resultObj;
346395
}
396+
397+
return constructor.newInstance(args);
347398
} catch (Exception e) {
348399
return null;
349400
}
350-
}).filter(Objects::nonNull).collect(Collectors.toList());
401+
} else {
402+
List<Map.Entry<String, Integer>> fieldList = selectsWithIndex.entrySet().stream().filter(e -> !e.getKey().contains(".")).distinct().sorted(Comparator.comparing(Map.Entry::getValue)).toList();
403+
Map<Integer, Method> setterMethods = getIntegerMethodMap(fieldList.stream().map(e -> Pair.of(e.getValue(), e.getKey())).collect(Collectors.toList()), modelType, classSetterMethodsMap);
404+
try {
405+
ModelType resultObj = modelType.getConstructor().newInstance();
406+
for (Map.Entry<Integer, Method> entry : setterMethods.entrySet()) {
407+
entry.getValue().invoke(resultObj, t.get(entry.getKey()));
408+
}
409+
for (Map.Entry<String, Object> stringObjectEntry : subModelMap.entrySet()) {
410+
Field declaredField = resultObj.getClass().getDeclaredField(stringObjectEntry.getKey());
411+
boolean canAccess = declaredField.canAccess(resultObj);
412+
declaredField.setAccessible(true);
413+
declaredField.set(resultObj, stringObjectEntry.getValue());
414+
declaredField.setAccessible(canAccess);
415+
}
416+
return resultObj;
417+
} catch (Exception e) {
418+
return null;
419+
}
420+
}
421+
}
351422

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

360445
@SuppressWarnings("unchecked")

0 commit comments

Comments
 (0)