Skip to content

Commit

Permalink
quarkusio#36581 Support of Embedded and ManyToOne nested object proje…
Browse files Browse the repository at this point in the history
…ction + Support of @ProjectedFieldName on class fields
  • Loading branch information
humcqc committed Oct 20, 2023
1 parent 997cab3 commit 85764b1
Show file tree
Hide file tree
Showing 16 changed files with 527 additions and 70 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package io.quarkus.hibernate.orm.panache.common;

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

/**
* Define a field's path for the SELECT statement when using a projection DTO.
* It supports the "dot" notation for fields in referenced entities:
* the name is composed of the property name for the relationship, followed by a dot ("."), followed by the name of the field.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface ProjectedClass {
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
* the name is composed of the property name for the relationship, followed by a dot ("."), followed by the name of the field.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
@Target({ ElementType.PARAMETER, ElementType.FIELD })
public @interface ProjectedFieldName {
String value();
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
package io.quarkus.hibernate.orm.panache.common.runtime;

import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Parameter;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import jakarta.persistence.EntityManager;
Expand All @@ -18,6 +17,7 @@
import org.hibernate.Filter;
import org.hibernate.Session;

import io.quarkus.hibernate.orm.panache.common.ProjectedClass;
import io.quarkus.hibernate.orm.panache.common.ProjectedFieldName;
import io.quarkus.panache.common.Page;
import io.quarkus.panache.common.Range;
Expand Down Expand Up @@ -121,39 +121,75 @@ public <T> CommonPanacheQueryImpl<T> project(Class<T> type) {
return new CommonPanacheQueryImpl<>(this, newQuery.toString(), "select count(*) " + from);
}

// build select clause with a constructor expression
StringBuilder selectClause = new StringBuilder("SELECT ");
selectClause.append(getParameterFromClass(type, null));
System.out.println(selectClause);
return new CommonPanacheQueryImpl<>(this, selectClause + selectQuery,
"select count(*) " + selectQuery);
}

private StringBuilder getParameterFromClass(Class<?> type, String parentParameter) {
// TODO: Should we check annotation @ProjectedClass on type ?
StringBuilder selectClause = new StringBuilder();
// We use the first constructor that we found and use the parameter names,
// so the projection class must have only one constructor,
// and the application must be built with parameter names.
// Maybe this should be improved some days ...
Constructor<?> constructor = type.getDeclaredConstructors()[0];
// TODO: Maybe this should be improved some days ...
Constructor<?> constructor = getConstructor(type); //type.getDeclaredConstructors()[0];
selectClause.append("new ").append(type.getName()).append(" (");
String parametersListStr = Stream.of(constructor.getParameters())
.map(parameter -> getParameterName(type, parentParameter, parameter))
.collect(Collectors.joining(","));
selectClause.append(parametersListStr);
selectClause.append(") ");
return selectClause;
}

// build select clause with a constructor expression
StringBuilder select = new StringBuilder("SELECT new ").append(type.getName()).append(" (");
int selectInitialLength = select.length();
for (Parameter parameter : constructor.getParameters()) {
String parameterName;
if (parameter.isAnnotationPresent(ProjectedFieldName.class)) {
final String name = parameter.getAnnotation(ProjectedFieldName.class).value();
if (name.isEmpty()) {
throw new PanacheQueryException("The annotation ProjectedFieldName must have a non-empty value.");
}
parameterName = name;
} else if (!parameter.isNamePresent()) {
throw new PanacheQueryException(
"Your application must be built with parameter names, this should be the default if" +
" using Quarkus project generation. Check the Maven or Gradle compiler configuration to include '-parameters'.");
} else {
parameterName = parameter.getName();
}
private Constructor<?> getConstructor(Class<?> type) {
return type.isPrimitive() || type.isAssignableFrom(String.class) ? type.getDeclaredConstructors()[1]
: type.getDeclaredConstructors()[0];
}

if (select.length() > selectInitialLength) {
select.append(", ");
private String getParameterName(Class<?> parentType, String parentParameter, Parameter parameter) {
String parameterName;
// Check if constructor param is annotated with ProjectedFieldName
if (hasProjectedFieldName(parameter)) {
parameterName = getNameFromProjectedFieldName(parameter);
} else if (!parameter.isNamePresent()) {
throw new PanacheQueryException(
"Your application must be built with parameter names, this should be the default if" +
" using Quarkus project generation. Check the Maven or Gradle compiler configuration to include '-parameters'.");
} else {
// Check if class field with same parameter name exists and contains @ProjectFieldName annotation
try {
Field field = parentType.getDeclaredField(parameter.getName());
parameterName = hasProjectedFieldName(field) ? getNameFromProjectedFieldName(field) : parameter.getName();
} catch (NoSuchFieldException e) {
parameterName = parameter.getName();
}
select.append(parameterName);
}
select.append(") ");
// For nested classes, add parent parameter in parameterName
parameterName = (parentParameter == null) ? parameterName : parentParameter.concat(".").concat(parameterName);
// Test if the parameter is a nested Class that should be projected too.
if (parameter.getType().isAnnotationPresent(ProjectedClass.class)) {
Class<?> nestedType = parameter.getType();
return getParameterFromClass(nestedType, parameterName).toString();
} else {
return parameterName;
}
}

private boolean hasProjectedFieldName(AnnotatedElement annotatedElement) {
return annotatedElement.isAnnotationPresent(ProjectedFieldName.class);
}

return new CommonPanacheQueryImpl<>(this, select.toString() + selectQuery, "select count(*) " + selectQuery);
private String getNameFromProjectedFieldName(AnnotatedElement annotatedElement) {
final String name = annotatedElement.getAnnotation(ProjectedFieldName.class).value();
if (name.isEmpty()) {
throw new PanacheQueryException("The annotation ProjectedFieldName must have a non-empty value.");
}
return name;
}

public void filter(String filterName, Map<String, Object> parameters) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package io.quarkus.hibernate.reactive.panache.common;

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

/**
* Define a field's path for the SELECT statement when using a projection DTO.
* It supports the "dot" notation for fields in referenced entities:
* the name is composed of the property name for the relationship, followed by a dot ("."), followed by the name of the field.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface ProjectedClass {
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
* the name is composed of the property name for the relationship, followed by a dot ("."), followed by the name of the field.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
@Target({ ElementType.PARAMETER, ElementType.FIELD })
public @interface ProjectedFieldName {
String value();
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
package io.quarkus.hibernate.reactive.panache.common.runtime;

import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Parameter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.CompletionException;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import jakarta.persistence.LockModeType;

import org.hibernate.Filter;
import org.hibernate.reactive.mutiny.Mutiny;

import io.quarkus.hibernate.reactive.panache.common.ProjectedClass;
import io.quarkus.hibernate.reactive.panache.common.ProjectedFieldName;
import io.quarkus.panache.common.Page;
import io.quarkus.panache.common.Range;
Expand Down Expand Up @@ -106,38 +111,75 @@ public <T> CommonPanacheQueryImpl<T> project(Class<T> type) {
return new CommonPanacheQueryImpl<>(this, newQuery.toString(), "select count(*) " + from);
}

// build select clause with a constructor expression
StringBuilder selectClause = new StringBuilder("SELECT ");
selectClause.append(getParameterFromClass(type, null));
System.out.println(selectClause);
return new CommonPanacheQueryImpl<>(this, selectClause + selectQuery,
"select count(*) " + selectQuery);
}

private StringBuilder getParameterFromClass(Class<?> type, String parentParameter) {
// TODO: Should we check annotation @ProjectedClass on type ?
StringBuilder selectClause = new StringBuilder();
// We use the first constructor that we found and use the parameter names,
// so the projection class must have only one constructor,
// and the application must be built with parameter names.
// Maybe this should be improved some days ...
Constructor<?> constructor = type.getDeclaredConstructors()[0];
// TODO: Maybe this should be improved some days ...
Constructor<?> constructor = getConstructor(type); //type.getDeclaredConstructors()[0];
selectClause.append("new ").append(type.getName()).append(" (");
String parametersListStr = Stream.of(constructor.getParameters())
.map(parameter -> getParameterName(type, parentParameter, parameter))
.collect(Collectors.joining(","));
selectClause.append(parametersListStr);
selectClause.append(") ");
return selectClause;
}

// build select clause with a constructor expression
StringBuilder select = new StringBuilder("SELECT new ").append(type.getName()).append(" (");
int selectInitialLength = select.length();
for (Parameter parameter : constructor.getParameters()) {
String parameterName;
if (parameter.isAnnotationPresent(ProjectedFieldName.class)) {
final String name = parameter.getAnnotation(ProjectedFieldName.class).value();
if (name.isEmpty()) {
throw new PanacheQueryException("The annotation ProjectedFieldName must have a non-empty value.");
}
parameterName = name;
} else if (!parameter.isNamePresent()) {
throw new PanacheQueryException(
"Your application must be built with parameter names, this should be the default if" +
" using Quarkus artifacts. Check the maven or gradle compiler configuration to include '-parameters'.");
} else {
private Constructor<?> getConstructor(Class<?> type) {
return type.isPrimitive() || type.isAssignableFrom(String.class) ? type.getDeclaredConstructors()[1]
: type.getDeclaredConstructors()[0];
}

private String getParameterName(Class<?> parentType, String parentParameter, Parameter parameter) {
String parameterName;
// Check if constructor param is annotated with ProjectedFieldName
if (hasProjectedFieldName(parameter)) {
parameterName = getNameFromProjectedFieldName(parameter);
} else if (!parameter.isNamePresent()) {
throw new PanacheQueryException(
"Your application must be built with parameter names, this should be the default if" +
" using Quarkus project generation. Check the Maven or Gradle compiler configuration to include '-parameters'.");
} else {
// Check if class field with same parameter name exists and contains @ProjectFieldName annotation
try {
Field field = parentType.getDeclaredField(parameter.getName());
parameterName = hasProjectedFieldName(field) ? getNameFromProjectedFieldName(field) : parameter.getName();
} catch (NoSuchFieldException e) {
parameterName = parameter.getName();
}
if (select.length() > selectInitialLength) {
select.append(", ");
}
select.append(parameterName);
}
select.append(") ");
// For nested classes, add parent parameter in parameterName
parameterName = (parentParameter == null) ? parameterName : parentParameter.concat(".").concat(parameterName);
// Test if the parameter is a nested Class that should be projected too.
if (parameter.getType().isAnnotationPresent(ProjectedClass.class)) {
Class<?> nestedType = parameter.getType();
return getParameterFromClass(nestedType, parameterName).toString();
} else {
return parameterName;
}
}

private boolean hasProjectedFieldName(AnnotatedElement annotatedElement) {
return annotatedElement.isAnnotationPresent(ProjectedFieldName.class);
}

return new CommonPanacheQueryImpl<>(this, select.toString() + selectQuery, "select count(*) " + selectQuery);
private String getNameFromProjectedFieldName(AnnotatedElement annotatedElement) {
final String name = annotatedElement.getAnnotation(ProjectedFieldName.class).value();
if (name.isEmpty()) {
throw new PanacheQueryException("The annotation ProjectedFieldName must have a non-empty value.");
}
return name;
}

public void filter(String filterName, Map<String, Object> parameters) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Embedded;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
Expand Down Expand Up @@ -58,6 +59,9 @@ public class Person extends PanacheEntity {
@ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
public Address address;

@Embedded
public PersonDescription description;

@Enumerated(EnumType.STRING)
public Status status;

Expand Down
Loading

0 comments on commit 85764b1

Please sign in to comment.