Skip to content

Commit

Permalink
Merge pull request #4 from tdilber/spring-boot-3.x
Browse files Browse the repository at this point in the history
Spring boot 3.x
  • Loading branch information
tdilber authored Sep 15, 2024
2 parents 934f9d7 + 2f67e7d commit 345f82c
Show file tree
Hide file tree
Showing 5 changed files with 380 additions and 95 deletions.
72 changes: 57 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ You can find the sample code from: https://github.com/tdilber/spring-jpa-dynamic
<dependency>
<groupId>io.github.tdilber</groupId>
<artifactId>spring-boot-starter-jpa-dynamic-query</artifactId>
<version>0.2.0</version>
<version>0.3.0</version>
</dependency>
```

Expand All @@ -108,7 +108,7 @@ You can find the sample code from: https://github.com/tdilber/spring-jpa-dynamic
<dependency>
<groupId>io.github.tdilber</groupId>
<artifactId>spring-jpa-dynamic-query</artifactId>
<version>0.5.0</version>
<version>0.6.0</version>
</dependency>
```

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

**Note:** Record class is supported for projection. You can use record class for projection.

#### A- Manual Projection
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
select clause. You can also use the `Pair` class to give an alias to the field.
Expand Down Expand Up @@ -507,54 +509,94 @@ where authorizat4_.menu_icon like ?

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


#### B- Auto Projection with Annotated Model
Model Annotations: `@JdqModel`, `@JdqField`, `@JdqIgnoreField`
#### B- Auto Projection with Annotated Model
Model Annotations: `@JdqModel`, `@JdqField`, `@JdqIgnoreField`, `@JdqSubModel`

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

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

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

_Examples:_
**Usage of `@JdqSubModel` annotation:**

`@JdqSubModel` annotation is used to include fields from a nested model in the select clause. This allows for more complex projections involving nested objects.

There are 2 usage of `@JdqSubModel` annotation:
- If you want to use nested model fields without join support, Use `@JdqSubModel()` annotation without any parameter.
- If you want to use nested model fields with join support, Use `@JdqSubModel("joined_column_name")` annotation with joined column name parameter.

_Examples:_

```java
@JdqModel // This annotation is required for using projection with joined column
@Data
public static class UserJdqModel {
@JdqField("name") // This annotation is not required. But if you want to use different field name in the result, you can use this annotation.
private String nameButDifferentFieldName;
@JdqField("user.name") // This annotation is required for using joined column in the projection
private String userNameWithJoin;
@JdqField("team.name") // This annotation is required for using joined column in the projection
private String teamNameWithJoin;

private Integer age; // This field is in the select clause. Because this field has not any annotation.

@JdqIgnoreField // This annotation is required for ignoring this field in the select clause.
private String surname;

@JdqSubModel // This annotation is used to include fields from a nested model without join support
private AddressJdqModel address;

@JdqSubModel("department") // This annotation is used to include fields from a nested model with join support
private DepartmentJdqModel departmentJdqModel;
}

@JdqModel
@Data
public static class AddressJdqModel {
@JdqField("address.street")
private String street;
@JdqField("address.city")
private String city;
}

@JdqModel
public record DepartmentJdqModel(@JdqField("id") Long departmentId, @JdqField String name) {

}

// USAGE EXAMPLE
List<UserJdqModel> result = customerRepository.findAll(dynamicQuery, UserJdqModel.class);
List<UserJdqModel> result = userRepository.findAll(dynamicQuery, UserJdqModel.class);
```
_Autofilled select Result If you fill Manuel:_
```java
select.add(Pair.of("name", "nameButDifferentFieldName"));
select.add(Pair.of("user.name", "userNameWithJoin"));
select.add(Pair.of("age", "age"));
select.add(Pair.of("address.street", "address.street"));
select.add(Pair.of("address.city", "address.city"));
select.add(Pair.of("department.id", "departmentJdqModel.departmentId"));
select.add(Pair.of("department.name", "departmentJdqModel.name"));
```

_Hibernate Query:_

```sql
select customer0_.name as col_0_0_, user1_.name as col_1_0_, customer0_.age as col_2_0_
from customer customer0_
inner join test_user user1_ on customer0_.user_id = user1_.id
where customer0_.age > 25
select user0_.name as col_0_0_,
team3_.name as col_1_0_,
user0_.age as col_2_0_,
address1_.street as col_3_0_,
address1_.city as col_4_0_,
department2_.id as col_5_0_,
department2_.name as col_6_0_
from test_user user0_
inner join team team3_ on user0_.team_id = team3_.id
inner join address address1_ on user0_.address_id = address1_.id
inner join department department2_ on user0_.department_id = department2_.id
where user0_.age > 25
```

### 9- Pagination Examples
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

<groupId>io.github.tdilber</groupId>
<artifactId>spring-boot-starter-jpa-dynamic-query</artifactId>
<version>0.2.0</version>
<version>0.3.0</version>
<packaging>jar</packaging>
<name>Spring Jpa Dynamic Query</name>
<description>Spring Jpa Dynamic Query (JDQ) Project</description>
Expand Down
13 changes: 13 additions & 0 deletions src/main/java/com/beyt/jdq/annotation/model/JdqSubModel.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.beyt.jdq.annotation.model;


import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
public @interface JdqSubModel {
String value() default "";
}
161 changes: 123 additions & 38 deletions src/main/java/com/beyt/jdq/query/DynamicQueryManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,18 @@
import com.beyt.jdq.annotation.model.JdqModel;
import com.beyt.jdq.annotation.model.JdqField;
import com.beyt.jdq.annotation.model.JdqIgnoreField;
import com.beyt.jdq.annotation.model.JdqSubModel;
import com.beyt.jdq.dto.Criteria;
import com.beyt.jdq.dto.DynamicQuery;
import com.beyt.jdq.dto.enums.CriteriaOperator;
import com.beyt.jdq.exception.DynamicQueryIllegalArgumentException;
import com.beyt.jdq.query.rule.specification.*;
import com.beyt.jdq.repository.DynamicSpecificationRepositoryImpl;
import com.beyt.jdq.util.ApplicationContextUtil;
import com.beyt.jdq.util.field.FieldUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.IterableUtils;
import org.apache.commons.lang3.StringUtils;
import org.hibernate.query.criteria.JpaRoot;
import org.hibernate.query.sqm.tree.domain.AbstractSqmPath;
import org.springframework.core.GenericTypeResolver;
Expand All @@ -32,9 +35,8 @@
import jakarta.persistence.Tuple;
import jakarta.persistence.TypedQuery;
import jakarta.persistence.criteria.*;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.RecordComponent;

import java.lang.reflect.*;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
Expand Down Expand Up @@ -169,9 +171,25 @@ protected static <Entity, ResultType> Iterable<ResultType> getEntityListBySelect
}

private static <ResultType> void extractIfJdqModel(DynamicQuery dynamicQuery, Class<ResultType> resultTypeClass) {
if (resultTypeClass.isAnnotationPresent(JdqModel.class)) {
List<Pair<String, String>> select = new ArrayList<>();
for (Field declaredField : resultTypeClass.getDeclaredFields()) {
if (!resultTypeClass.isAnnotationPresent(JdqModel.class)) {
return;
}

List<Pair<String, String>> select = new ArrayList<>();
recursiveSupModelFiller(resultTypeClass, select, new ArrayList<>(), "");
dynamicQuery.setSelect(select);
}

private static <ResultType> void recursiveSupModelFiller(Class<ResultType> resultTypeClass, List<Pair<String, String>> select, List<String> dbPrefixList, String entityPrefix) {
for (Field declaredField : resultTypeClass.getDeclaredFields()) {
if (declaredField.isAnnotationPresent(JdqSubModel.class)) {
String subModelValue = declaredField.getAnnotation(JdqSubModel.class).value();
ArrayList<String> newPrefixList = new ArrayList<>(dbPrefixList);
if (StringUtils.isNotBlank(subModelValue)) {
newPrefixList.add(subModelValue);
}
recursiveSupModelFiller(declaredField.getType(), select, newPrefixList, entityPrefix + declaredField.getName() + ".");
} else if (FieldUtil.isSupportedType(declaredField.getType())) {
if (declaredField.isAnnotationPresent(JdqIgnoreField.class)) {
if (resultTypeClass.isRecord()) {
throw new DynamicQueryIllegalArgumentException("Record class can not have @JdqIgnoreField annotation");
Expand All @@ -180,15 +198,26 @@ private static <ResultType> void extractIfJdqModel(DynamicQuery dynamicQuery, Cl
}

if (declaredField.isAnnotationPresent(JdqField.class)) {
select.add(Pair.of(declaredField.getAnnotation(JdqField.class).value(), declaredField.getName()));
select.add(Pair.of(prefixCreator(dbPrefixList) + declaredField.getAnnotation(JdqField.class).value(), entityPrefix + declaredField.getName()));
} else {
select.add(Pair.of(declaredField.getName(), declaredField.getName()));
select.add(Pair.of(prefixCreator(dbPrefixList) + declaredField.getName(), entityPrefix + declaredField.getName()));
}
} else {
if (resultTypeClass.isRecord()) {
throw new DynamicQueryIllegalArgumentException("Record didnt support nested model type: " + declaredField.getType().getName());
}
}
dynamicQuery.setSelect(select);
}
}

private static String prefixCreator(List<String> prefixList) {
String collect = String.join(".", prefixList);
if (StringUtils.isNotBlank(collect)) {
collect += ".";
}
return collect;
}

protected static <Entity, ResultType> Iterable<ResultType> getEntityListWithReturnClass(JpaSpecificationExecutor<Entity> repositoryExecutor, DynamicQuery dynamicQuery, Class<ResultType> resultTypeClass, boolean isPage) {
Class<Entity> entityClass = getEntityClass(repositoryExecutor);
EntityManager entityManager = ApplicationContextUtil.getEntityManager();
Expand Down Expand Up @@ -311,50 +340,106 @@ protected static long executeCountQuery(TypedQuery<Long> query) {
}

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

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

if (methodOptional.isPresent()) {
setterMethods.put(i, methodOptional.get());
}
Map<String, Integer> selectsWithIndex = new HashMap<>();
for (int i = 0; i < querySelects.size(); i++) {
selectsWithIndex.put(querySelects.get(i).getSecond(), i);
}

Map<Class<?>, Map<Integer, Method>> classSetterMethodsMap = new HashMap<>();
Map<Class<?>, Constructor<?>> recordConstructorMap = new HashMap<>();

resultTypeList = stream.map(t -> fillModel(resultTypeClass, t, selectsWithIndex, classSetterMethodsMap, recordConstructorMap)).filter(Objects::nonNull).collect(Collectors.toList());


if (isPage) {
Page<Tuple> tuplePage = (Page<Tuple>) entityListBySelectableFilter;
return new PageImpl<>(resultTypeList, tuplePage.getPageable(), tuplePage.getTotalElements());
} else {
return resultTypeList;
}
}

protected static <ModelType> ModelType fillModel(Class<ModelType> modelType, Tuple t, Map<String, Integer> selectsWithIndex, Map<Class<?>, Map<Integer, Method>> classSetterMethodsMap, Map<Class<?>, Constructor<?>> recordConstructorMap) {
Map<String, Object> subModelMap = new HashMap<>();
for (Field declaredField : modelType.getDeclaredFields()) {
if (declaredField.isAnnotationPresent(JdqSubModel.class)) {
subModelMap.put(declaredField.getName(), fillModel(declaredField.getType(), t, selectsWithIndex.entrySet().stream().filter(e -> e.getKey().startsWith(declaredField.getName() + "."))
.collect(Collectors.toMap(k -> k.getKey().substring(declaredField.getName().length() + 1), Map.Entry::getValue)), classSetterMethodsMap, recordConstructorMap));
}
}
Stream<Tuple> stream = isPage ? ((Page<Tuple>) entityListBySelectableFilter).stream() : ((List<Tuple>) entityListBySelectableFilter).stream();

List<ResultType> resultTypeList = stream.map(t -> {

if (modelType.isRecord()) {
try {
if (resultTypeClass.isRecord()) {
Object[] args = new Object[querySelects.size()];
for (int i = 0; i < querySelects.size(); i++) {
args[i] = t.get(i);
}
return resultTypeClass.getDeclaredConstructor(Arrays.stream(resultTypeClass.getRecordComponents())
Constructor<ModelType> constructor = (Constructor<ModelType>) recordConstructorMap.get(modelType);
if (Objects.isNull(constructor)) {
constructor = modelType.getConstructor(Arrays.stream(modelType.getRecordComponents())
.map(RecordComponent::getType)
.toArray(Class[]::new)).newInstance(args);
} else {
ResultType resultObj = resultTypeClass.getConstructor().newInstance();
for (Map.Entry<Integer, Method> entry : setterMethods.entrySet()) {
entry.getValue().invoke(resultObj, t.get(entry.getKey()));
.toArray(Class[]::new));
recordConstructorMap.put(modelType, constructor);
}

Parameter[] parameters = constructor.getParameters();
Object[] args = new Object[parameters.length];
for (int i = 0; i < parameters.length; i++) {
if (selectsWithIndex.containsKey(parameters[i].getName())) {
Integer index = selectsWithIndex.get(parameters[i].getName());
args[i] = t.get(index);
} else {
args[i] = subModelMap.get(parameters[i].getName());
}
return resultObj;
}

return constructor.newInstance(args);
} catch (Exception e) {
return null;
}
}).filter(Objects::nonNull).collect(Collectors.toList());
} else {
List<Map.Entry<String, Integer>> fieldList = selectsWithIndex.entrySet().stream().filter(e -> !e.getKey().contains(".")).distinct().sorted(Comparator.comparing(Map.Entry::getValue)).toList();
Map<Integer, Method> setterMethods = getIntegerMethodMap(fieldList.stream().map(e -> Pair.of(e.getValue(), e.getKey())).collect(Collectors.toList()), modelType, classSetterMethodsMap);
try {
ModelType resultObj = modelType.getConstructor().newInstance();
for (Map.Entry<Integer, Method> entry : setterMethods.entrySet()) {
entry.getValue().invoke(resultObj, t.get(entry.getKey()));
}
for (Map.Entry<String, Object> stringObjectEntry : subModelMap.entrySet()) {
Field declaredField = resultObj.getClass().getDeclaredField(stringObjectEntry.getKey());
boolean canAccess = declaredField.canAccess(resultObj);
declaredField.setAccessible(true);
declaredField.set(resultObj, stringObjectEntry.getValue());
declaredField.setAccessible(canAccess);
}
return resultObj;
} catch (Exception e) {
return null;
}
}
}

if (isPage) {
Page<Tuple> tuplePage = (Page<Tuple>) entityListBySelectableFilter;
return new PageImpl<>(resultTypeList, tuplePage.getPageable(), tuplePage.getTotalElements());

private static <ResultType> Map<Integer, Method> getIntegerMethodMap(List<Pair<Integer, String>> querySelects, Class<ResultType> resultTypeClass, Map<Class<?>, Map<Integer, Method>> classSetterMethodsMap) {
Map<Integer, Method> setterMethods = new HashMap<>();
if (classSetterMethodsMap.containsKey(resultTypeClass)) {
setterMethods = classSetterMethodsMap.get(resultTypeClass);
} else {
return resultTypeList;
for (int i = 0; i < querySelects.size(); i++) {
String select = querySelects.get(i).getSecond();

Optional<Method> methodOptional = Arrays.stream(resultTypeClass.getMethods())
.filter(c -> c.getName().equalsIgnoreCase("set" + select)
&& c.getParameterCount() == 1).findFirst();

if (methodOptional.isPresent()) {
setterMethods.put(querySelects.get(i).getFirst(), methodOptional.get());
}
}
classSetterMethodsMap.put(resultTypeClass, setterMethods);
}
return setterMethods;
}

@SuppressWarnings("unchecked")
Expand Down
Loading

0 comments on commit 345f82c

Please sign in to comment.