Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: dto with association #583

Draft
wants to merge 4 commits into
base: 4.7.x
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
package io.micronaut.data.jdbc.h2

import io.micronaut.data.tck.entities.Car
import io.micronaut.data.tck.entities.Face
import io.micronaut.data.tck.entities.FaceDTO
import io.micronaut.data.tck.entities.Nose
import io.micronaut.data.tck.repositories.AuthorRepository
import io.micronaut.data.tck.repositories.BookDtoRepository
import io.micronaut.data.tck.repositories.BookRepository
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1330,6 +1330,9 @@ class PropertyProjection extends Projection {
private String propertyName;
private String alias;

@Nullable
private String entityAlias;

/**
* Default constructor.
* @param propertyName The property name
Expand All @@ -1338,6 +1341,29 @@ public PropertyProjection(String propertyName) {
this.propertyName = propertyName;
}

/**
* constructor.
* @param entityAlias Entity Alias
* @param propertyName The property name
* @param alias the alias
*/
public PropertyProjection(@Nullable String entityAlias,
String propertyName,
String alias) {
this.entityAlias = entityAlias;
this.propertyName = propertyName;
this.alias = alias;
}

/**
*
* @return The alias of the entity the property belongs to.
*/
@Nullable
public String getEntityAlias() {
return entityAlias;
}

/**
* @return The property name
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public abstract class AbstractSqlLikeQueryBuilder implements QueryBuilder {
protected static final char DOT = '.';
protected static final String NOT_CLAUSE = " NOT";
protected static final String AND = "AND";
protected static final String LOGICAL_AND = " "+ AND + " ";
protected static final String LOGICAL_AND = " " + AND + " ";
protected static final String UPDATE_CLAUSE = "UPDATE ";
protected static final String DELETE_CLAUSE = "DELETE ";
protected static final String OR = "OR";
Expand Down Expand Up @@ -727,7 +727,8 @@ private void buildSelect(QueryState queryState, StringBuilder queryString, List<
String propertyName = pp.getPropertyName();
PersistentProperty persistentProperty = entity.getPropertyByPath(propertyName)
.orElseThrow(() -> new IllegalArgumentException("Cannot project on non-existent property: " + propertyName));
appendPropertyProjection(queryString, logicalName, persistentProperty, propertyName);
String entityAlias = pp.getEntityAlias() != null ? pp.getEntityAlias() : logicalName;
appendPropertyProjection(queryString, entityAlias, persistentProperty, propertyName);
}
if (alias != null) {
queryString.append(AS_CLAUSE)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,17 @@ public static QueryModel.PropertyProjection property(String name) {
return new QueryModel.PropertyProjection(name);
}

/**
* A projection that obtains the value of a property of an entity.
* @param entityAlias Entity Alias
* @param name The name of the property
* @param alias alias
* @return The PropertyProjection instance
*/
public static QueryModel.PropertyProjection property(String entityAlias, String name, String alias) {
return new QueryModel.PropertyProjection(entityAlias, name, alias);
}

/**
* Computes the sum of a property.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,15 @@

import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;
import io.micronaut.core.annotation.AnnotationValue;
import io.micronaut.core.util.CollectionUtils;
import io.micronaut.core.util.StringUtils;
import io.micronaut.data.annotation.Join;
import io.micronaut.data.annotation.Query;
import io.micronaut.data.annotation.Where;
import io.micronaut.data.model.PersistentEntity;
import io.micronaut.data.model.PersistentProperty;
import io.micronaut.data.model.Sort;
import io.micronaut.data.model.query.QueryModel;
import io.micronaut.data.model.query.QueryParameter;
import io.micronaut.data.model.Sort;
import io.micronaut.data.processor.model.SourcePersistentEntity;
import io.micronaut.data.processor.model.SourcePersistentProperty;
import io.micronaut.data.processor.visitors.AnnotationMetadataHierarchy;
Expand All @@ -39,6 +37,7 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

Expand Down Expand Up @@ -139,13 +138,17 @@ public MethodMatchInfo buildMatchInfo(@NonNull MethodMatchContext matchContext)
}
}

List<AnnotationValue<Join>> joinSpecs = joinSpecsAtMatchContext(matchContext);

Set<AssociationJoin> joinSpecs = joinSpecsForMatchContext(matchContext);
ClassElement returnType = matchContext.getReturnType();
boolean dto = isReturnTypeDto(returnType, queryResultType);
if (dto) {
joinSpecs.addAll(joinSpecsByPath(joinPathsForDto(returnType, queryResultType)));
}
if (CollectionUtils.isNotEmpty(joinSpecs)) {
if (query == null) {
query = QueryModel.from(rootEntity);
}
if (applyJoinSpecs(matchContext, query, rootEntity, joinSpecs)) {
if (applyJoinSpecifications(matchContext, query, rootEntity, joinSpecs)) {
return null;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
import java.util.concurrent.Future;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
Expand All @@ -62,11 +63,13 @@
* @since 1.1.0
*/
public abstract class AbstractPatternBasedMethod implements MethodCandidate {
public static final String CAPITALIZED_ID = "Id";

private static final Pattern ORDER_BY_PATTERN = Pattern.compile("(.*)OrderBy([\\w\\d]+)");
private static final String DELETE = "delete";
private static final String UPDATE = "update";
private static final String VOID = "void";
private static final Join.Type DEFAULT_JOIN_TYPE = Join.Type.FETCH;
protected final Pattern pattern;

/**
Expand All @@ -83,6 +86,38 @@ public boolean isMethodMatch(@NonNull MethodElement methodElement, @NonNull Matc
return pattern.matcher(methodElement.getName()).find();
}

/**
*
* @param dto DTO Class element
* @param query Query Class Element
* @return A set of Paths
*/
@NonNull
protected static Set<String> joinPathsForDto(@NonNull ClassElement dto, @NonNull ClassElement query) {
Set<String> paths = new HashSet<>();
for (PropertyElement propertyElement : dto.getBeanProperties()) {
final String name = propertyElement.getName();
if (name.endsWith(CAPITALIZED_ID)) {
String associationName = name.substring(0, name.indexOf(CAPITALIZED_ID));
paths.add(associationName);
}
}
return paths;
}
/**
*
* @param returnType Return Type
* @param queryResultType Query Result Type
* @return Whether the return type is a DTO
*/
protected boolean isReturnTypeDto(@Nullable ClassElement returnType, @Nullable ClassElement queryResultType) {
return returnType != null &&
queryResultType != null &&
!TypeUtils.areTypesCompatible(returnType, queryResultType) &&
returnType.hasStereotype(Introspected.class) &&
queryResultType.hasStereotype(MappedEntity.class);
}

/**
* Matches order by definitions in the query sequence.
*
Expand Down Expand Up @@ -188,7 +223,7 @@ protected MethodMatchInfo buildInfo(
return null;
}
}

return new MethodMatchInfo(returnType, query, getInterceptorElement(matchContext, FindOneInterceptor.class), true);
} else {

Expand Down Expand Up @@ -479,7 +514,10 @@ private boolean isReactiveSingleResult(ClassElement returnType) {
return returnType.hasStereotype(SingleResult.class) || returnType.isAssignable("io.reactivex.Single") || returnType.isAssignable("reactor.core.publisher.Mono");
}

private boolean attemptProjection(@NonNull MethodMatchContext matchContext, @NonNull ClassElement queryResultType, @NonNull QueryModel query, ClassElement returnType) {
private boolean attemptProjection(@NonNull MethodMatchContext matchContext,
@NonNull ClassElement queryResultType,
@NonNull QueryModel query,
ClassElement returnType) {
List<PropertyElement> beanProperties = returnType.getBeanProperties();
SourcePersistentEntity entity = matchContext.getEntity(queryResultType);
for (PropertyElement beanProperty : beanProperties) {
Expand All @@ -489,6 +527,18 @@ private boolean attemptProjection(@NonNull MethodMatchContext matchContext, @Non
if (pp == null) {
pp = entity.getIdOrVersionPropertyByName(propertyName);
}
SourcePersistentEntity association = null;
if (pp == null && propertyName.endsWith(CAPITALIZED_ID)) {
String associationPropertyName = propertyName.substring(0, propertyName.indexOf(CAPITALIZED_ID));
Optional<SourcePersistentProperty> associationOptional = entity.getPersistentProperties()
.stream()
.filter(persistentProperty -> persistentProperty.getName().equals(associationPropertyName))
.findFirst();
if (associationOptional.isPresent()) {
association = matchContext.getEntity(associationOptional.get().getType());
pp = association.getIdentity();
}
}

if (pp == null) {
matchContext.fail("Property " + propertyName + " is not present in entity: " + entity.getName());
Expand All @@ -501,10 +551,16 @@ private boolean attemptProjection(@NonNull MethodMatchContext matchContext, @Non
}
// add an alias projection for each property
final QueryBuilder queryBuilder = matchContext.getQueryBuilder();
if (queryBuilder.shouldAliasProjections()) {
query.projections().add(Projections.property(propertyName).aliased());

if (association == null) {
if (queryBuilder.shouldAliasProjections()) {
query.projections().add(Projections.property(propertyName).aliased());
} else {
query.projections().add(Projections.property(propertyName));
}
} else {
query.projections().add(Projections.property(propertyName));
String entityAlias = entity.getAliasName() + association.getAliasName();
query.projections().add(Projections.property(entityAlias, pp.getName(), propertyName));
}
}
return false;
Expand Down Expand Up @@ -561,28 +617,78 @@ protected boolean applyJoinSpecs(
@NonNull QueryModel query,
@Nonnull SourcePersistentEntity rootEntity,
@NonNull List<AnnotationValue<Join>> joinSpecs) {
for (AnnotationValue<Join> joinSpec : joinSpecs) {
String path = joinSpec.stringValue().orElse(null);
Join.Type type = joinSpec.enumValue("type", Join.Type.class).orElse(Join.Type.FETCH);
String alias = joinSpec.stringValue("alias").orElse(null);
if (path != null) {
PersistentProperty prop = rootEntity.getPropertyByPath(path).orElse(null);
if (!(prop instanceof Association)) {
matchContext.fail("Invalid join spec [" + path + "]. Property is not an association!");
return true;
} else {
boolean hasExisting = query.getCriteria().getCriteria().stream().anyMatch(c -> {
if (c instanceof AssociationQuery) {
AssociationQuery aq = (AssociationQuery) c;
return aq.getAssociation().equals(prop);
}
return false;
});
if (!hasExisting) {
query.add(new AssociationQuery(path, (Association) prop));
return applyJoinSpecifications(matchContext, query, rootEntity, joinSpecsByAnnotationValue(joinSpecs));
}

/**
*
* @param paths Association Paths
* @return set of {@link AssociationJoin} for the supplied association paths.
*/
protected Set<AssociationJoin> joinSpecsByPath(@NonNull Set<String> paths) {
return paths.stream().map(path -> new AssociationJoin(path, null, DEFAULT_JOIN_TYPE)).collect(Collectors.toSet());
}

/**
*
* @param joinSpecs {@link Join} Annotation values.
* @return set of {@link AssociationJoin} for the supplied {@link Join} Annotation values.
*/
protected Set<AssociationJoin> joinSpecsByAnnotationValue(@NonNull List<AnnotationValue<Join>> joinSpecs) {
return joinSpecs.stream()
.filter(joinSpec -> joinSpec.stringValue().orElse(null) != null)
.map(joinSpec -> {
String path = joinSpec.stringValue().orElse(null);
Join.Type type = joinSpec.enumValue("type", Join.Type.class).orElse(DEFAULT_JOIN_TYPE);
String alias = joinSpec.stringValue("alias").orElse(null);
return new AssociationJoin(path, alias, type);
}).collect(Collectors.toSet());
}

/**
*
* @param matchContext Match Context
* @return set of {@link AssociationJoin} for the {@link Join} annotations.
*/
protected Set<AssociationJoin> joinSpecsForMatchContext(@NonNull MethodMatchContext matchContext) {
final MethodMatchInfo.OperationType operationType = getOperationType();
if (operationType != MethodMatchInfo.OperationType.QUERY) {
return joinSpecsByAnnotationValue(matchContext.getAnnotationMetadata().getDeclaredAnnotationValuesByType(Join.class));
}
return joinSpecsByAnnotationValue(matchContext.getAnnotationMetadata().getAnnotationValuesByType(Join.class));
}

/**
* Apply the configured join specifications to the given query.
*
* @param matchContext The match context
* @param query The query
* @param rootEntity the root entity
* @param joinSpecs The join specs
* @return True if an error occurred applying the specs
*/
protected boolean applyJoinSpecifications(
@NonNull MethodMatchContext matchContext,
@NonNull QueryModel query,
@Nonnull SourcePersistentEntity rootEntity,
@NonNull Set<AssociationJoin> joinSpecs) {
for (AssociationJoin spec : joinSpecs) {
PersistentProperty prop = rootEntity.getPropertyByPath(spec.getPath()).orElse(null);
if (!(prop instanceof Association)) {
matchContext.fail("Invalid join spec [" + spec.getPath() + "]. Property is not an association!");
return true;
} else {
boolean hasExisting = query.getCriteria().getCriteria().stream().anyMatch(c -> {
if (c instanceof AssociationQuery) {
AssociationQuery aq = (AssociationQuery) c;
return aq.getAssociation().equals(prop);
}
query.join(path, (Association) prop, type, alias);
return false;
});
if (!hasExisting) {
query.add(new AssociationQuery(spec.getPath(), (Association) prop));
}
query.join(spec.getPath(), (Association) prop, spec.getJoinType(), spec.getAlias());
}
}
return false;
Expand Down
Loading