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

DTO joins support issues #2761

Draft
wants to merge 10 commits into
base: 4.10.x
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,15 @@ default PersistentPropertyPath getPropertyPath(@NonNull String[] propertyPath) {
@NonNull
Optional<NamingStrategy> findNamingStrategy();

/**
* Obtains a PersistentProperty representing id or version property by name.
*
* @param name The name of the id or version property
* @return The PersistentProperty used as id or version or null if it doesn't exist
* @since 4.7.0
*/
PersistentProperty getIdOrVersionPropertyByName(String name);
radovanradic marked this conversation as resolved.
Show resolved Hide resolved

/**
* Creates a new persistent entity representation of the given type. The type
* must be annotated with {@link io.micronaut.core.annotation.Introspected}. This method will create a new instance on demand and does not cache.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import io.micronaut.core.beans.BeanProperty;
import io.micronaut.core.type.Argument;
import io.micronaut.core.util.ArgumentUtils;
import io.micronaut.core.util.ArrayUtils;
import io.micronaut.data.annotation.Id;
import io.micronaut.data.annotation.Relation;
import io.micronaut.data.annotation.Transient;
Expand Down Expand Up @@ -379,6 +380,23 @@ public PersistentEntity getParentEntity() {
return null;
}

@Override
public RuntimePersistentProperty<T> getIdOrVersionPropertyByName(String name) {
if (ArrayUtils.isNotEmpty(identity)) {
for (RuntimePersistentProperty<T> identityProperty : identity) {
if (identityProperty.getName().equals(name)) {
return identityProperty;
}
}
}

if (version != null && version.getName().equals(name)) {
return version;
}

return null;
}

private boolean isEmbedded(BeanProperty bp) {
return bp.enumValue(Relation.class, Relation.Kind.class).orElse(null) == Relation.Kind.EMBEDDED;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package io.micronaut.data.model.runtime
import io.micronaut.data.annotation.AutoPopulated
import io.micronaut.data.annotation.Id
import io.micronaut.data.annotation.MappedEntity
import io.micronaut.data.annotation.Version
import spock.lang.Specification

class RuntimePersistentEntitySpec extends Specification {
Expand All @@ -12,6 +13,10 @@ class RuntimePersistentEntitySpec extends Specification {
def rtpe = new RuntimePersistentEntity(Test)
expect:
rtpe.getPersistentPropertyNames().contains('id')
rtpe.getIdOrVersionPropertyByName('id') != null
rtpe.getIdOrVersionPropertyByName('name') == null
rtpe.getIdOrVersionPropertyByName('version') != null
rtpe.getIdOrVersionPropertyByName('name') == null
}

}
Expand All @@ -23,4 +28,7 @@ class Test {
UUID id

String name
}

@Version
Long version
}
Original file line number Diff line number Diff line change
Expand Up @@ -190,21 +190,13 @@ public SourcePersistentProperty getIdentityByName(String name) {
return (SourcePersistentProperty) super.getIdentityByName(name);
}

/**
* Obtains a PersistentProperty representing id or version property by name.
*
* @param name The name of the id or version property
* @return The PersistentProperty used as id or version or null if it doesn't exist
*/
@Override
public SourcePersistentProperty getIdOrVersionPropertyByName(String name) {
if (ArrayUtils.isNotEmpty(ids)) {
SourcePersistentProperty persistentProp = Arrays.stream(ids)
.filter(p -> p.getName().equals(name))
.findFirst()
.orElse(null);

if (persistentProp != null) {
return persistentProp;
for (SourcePersistentProperty id : ids) {
if (id.getName().equals(name)) {
return id;
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -718,11 +718,12 @@ private boolean isDtoType(ClassElement repositoryElement, ClassElement classElem
/**
* Find DTO properties.
*
* @param matchContext The method match context
* @param entity The entity
* @param returnType The result
* @return DTO properties
*/
protected List<SourcePersistentProperty> getDtoProjectionProperties(SourcePersistentEntity entity,
protected List<SourcePersistentProperty> getDtoProjectionProperties(MethodMatchContext matchContext, SourcePersistentEntity entity,
ClassElement returnType) {
return returnType.getBeanProperties().stream()
.filter(dtoProperty -> {
Expand Down Expand Up @@ -751,9 +752,17 @@ protected List<SourcePersistentProperty> getDtoProjectionProperties(SourcePersis
// Convert anything to a string or an object
return pp;
}
if (!TypeUtils.areTypesCompatible(dtoPropertyType, pp.getType())) {
boolean compatibleTypes = TypeUtils.areTypesCompatible(dtoPropertyType, pp.getType());
if (compatibleTypes) {
return pp;
}

// Check if these are compatible non-simple field types (kind of nested DTOs)
List<SourcePersistentProperty> props = getDtoProjectionProperties(matchContext, new SourcePersistentEntity(pp.getType(), matchContext::getEntity), dtoPropertyType);
if (props.isEmpty()) {
throw new MatchFailedException("Property [" + propertyName + "] of type [" + dtoPropertyType.getName() + "] is not compatible with equivalent property of type [" + pp.getType().getName() + "] declared in entity: " + entity.getName());
}

return pp;
}).toList();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -575,8 +575,6 @@ public static boolean areTypesCompatible(ClassElement leftType, ClassElement rig
String rightTypeName = rightType.getName();
if (leftType.getName().equals(rightTypeName)) {
return true;
} else if (leftType.isAssignable(rightTypeName)) {
return true;
} else {
if (isNumber(leftType) && isNumber(rightType)) {
return true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ protected MethodMatchInfo build(MethodMatchContext matchContext) {
);

if (result.isDto() && !result.isRuntimeDtoConversion()) {
List<SourcePersistentProperty> dtoProjectionProperties = getDtoProjectionProperties(matchContext.getRootEntity(), resultType);
List<SourcePersistentProperty> dtoProjectionProperties = getDtoProjectionProperties(matchContext, matchContext.getRootEntity(), resultType);
if (!dtoProjectionProperties.isEmpty()) {
List<Selection<?>> selectionList = dtoProjectionProperties.stream()
.map(p -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ protected MethodMatchInfo build(MethodMatchContext matchContext) {
);

if (result.isDto() && !result.isRuntimeDtoConversion()) {
List<SourcePersistentProperty> dtoProjectionProperties = getDtoProjectionProperties(matchContext.getRootEntity(), resultType);
List<SourcePersistentProperty> dtoProjectionProperties = getDtoProjectionProperties(matchContext, matchContext.getRootEntity(), resultType);
if (!dtoProjectionProperties.isEmpty()) {
Root<?> root = query.getRoots().iterator().next();
List<Selection<?>> selectionList = dtoProjectionProperties.stream()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ protected MethodMatchInfo build(MethodMatchContext matchContext) {
);

if (result.isDto() && !result.isRuntimeDtoConversion()) {
List<SourcePersistentProperty> dtoProjectionProperties = getDtoProjectionProperties(matchContext.getRootEntity(), resultType);
List<SourcePersistentProperty> dtoProjectionProperties = getDtoProjectionProperties(matchContext, matchContext.getRootEntity(), resultType);
if (!dtoProjectionProperties.isEmpty()) {
List<Selection<?>> selectionList = dtoProjectionProperties.stream()
.map(p -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
import java.util.Objects;
import java.util.Set;
import java.util.StringJoiner;
import java.util.UUID;
import java.util.function.BiFunction;

/**
Expand All @@ -74,6 +75,7 @@ public final class SqlResultEntityTypeMapper<RS, R> implements SqlTypeMapper<RS,
private final SqlJsonColumnReader<RS> jsonColumnReader;
private final DataConversionService conversionService;
private final BiFunction<RuntimePersistentEntity<Object>, Object, Object> eventListener;
private final boolean isDto;
private boolean callNext = true;

/**
Expand All @@ -89,8 +91,9 @@ public SqlResultEntityTypeMapper(
String prefix,
@NonNull RuntimePersistentEntity<R> entity,
@NonNull ResultReader<RS, String> resultReader,
@Nullable SqlJsonColumnReader<RS> jsonColumnReader, DataConversionService conversionService) {
this(entity, resultReader, Collections.emptySet(), prefix, jsonColumnReader, conversionService, null);
@Nullable SqlJsonColumnReader<RS> jsonColumnReader,
DataConversionService conversionService) {
this(entity, resultReader, Collections.emptySet(), prefix, jsonColumnReader, conversionService, null, false);
}

/**
Expand All @@ -107,7 +110,7 @@ public SqlResultEntityTypeMapper(
@NonNull ResultReader<RS, String> resultReader,
@Nullable Set<JoinPath> joinPaths,
@Nullable SqlJsonColumnReader<RS> jsonColumnReader, DataConversionService conversionService) {
this(entity, resultReader, joinPaths, null, jsonColumnReader, conversionService, null);
this(entity, resultReader, joinPaths, null, jsonColumnReader, conversionService, null, false);
}

/**
Expand All @@ -119,14 +122,17 @@ public SqlResultEntityTypeMapper(
* @param jsonColumnReader The json column reader
* @param loadListener The event listener
* @param conversionService The conversion service
* @param isDto Whether reading/mapping DTO projection
*/
public SqlResultEntityTypeMapper(
@NonNull RuntimePersistentEntity<R> entity,
@NonNull ResultReader<RS, String> resultReader,
@Nullable Set<JoinPath> joinPaths,
@Nullable SqlJsonColumnReader<RS> jsonColumnReader,
@Nullable BiFunction<RuntimePersistentEntity<Object>, Object, Object> loadListener, DataConversionService conversionService) {
this(entity, resultReader, joinPaths, null, jsonColumnReader, conversionService, loadListener);
@Nullable BiFunction<RuntimePersistentEntity<Object>, Object, Object> loadListener,
DataConversionService conversionService,
boolean isDto) {
this(entity, resultReader, joinPaths, null, jsonColumnReader, conversionService, loadListener, isDto);
}

/**
Expand All @@ -139,14 +145,17 @@ public SqlResultEntityTypeMapper(
* @param jsonColumnReader The json column reader
* @param eventListener The event listener used for trigger post load if configured
* @param conversionService The conversion service
* @param isDto Whether reading/mapping DTO projection
*/
private SqlResultEntityTypeMapper(
@NonNull RuntimePersistentEntity<R> entity,
@NonNull ResultReader<RS, String> resultReader,
@Nullable Set<JoinPath> joinPaths,
String startingPrefix,
@Nullable SqlJsonColumnReader<RS> jsonColumnReader,
DataConversionService conversionService, @Nullable BiFunction<RuntimePersistentEntity<Object>, Object, Object> eventListener) {
DataConversionService conversionService,
@Nullable BiFunction<RuntimePersistentEntity<Object>, Object, Object> eventListener,
boolean isDto) {
this.conversionService = conversionService;
ArgumentUtils.requireNonNull("entity", entity);
ArgumentUtils.requireNonNull("resultReader", resultReader);
Expand All @@ -168,6 +177,7 @@ private SqlResultEntityTypeMapper(
this.hasJoins = false;
}
this.startingPrefix = startingPrefix;
this.isDto = isDto;
}

@Override
Expand Down Expand Up @@ -721,7 +731,13 @@ private <K> K triggerPostLoad(RuntimePersistentEntity<?> persistentEntity, K ent
@Nullable
private <K> Object readEntityId(RS rs, MappingContext<K> ctx) {
RuntimePersistentProperty<K> identity = ctx.persistentEntity.getIdentity();
if (identity != null) {
if (identity == null) {
// DTO might not have ID mapped, and in this case to maintain relation
// we set random UUID as id to be able to read and make relation
if (isDto) {
return UUID.randomUUID();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure this will work?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, there is a test that covers that case.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the other hand, don't know if could break some user case, all tests we have are passing.

}
} else {
if (identity instanceof Embedded embedded) {
return readEntity(rs, ctx.embedded(embedded), null, null);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -692,7 +692,8 @@ protected <E, R> SqlTypeMapper<RS, R> createMapper(SqlStoredQuery<E, R> prepared
preparedQuery.getJoinPaths(),
sqlJsonColumnMapperProvider.getJsonColumnReader(preparedQuery, rsType),
loadListener,
conversionService);
conversionService,
false);
}
if (preparedQuery.isDtoProjection()) {
RuntimePersistentEntity<R> resultPersistentEntity = getEntity(preparedQuery.getResultType());
Expand All @@ -704,7 +705,14 @@ protected <E, R> SqlTypeMapper<RS, R> createMapper(SqlStoredQuery<E, R> prepared
return p;
}
RuntimePersistentProperty<E> entityProperty = persistentEntity.getPropertyByName(p.getName());
if (entityProperty == null || !ReflectionUtils.getWrapperType(entityProperty.getType()).equals(ReflectionUtils.getWrapperType(p.getType()))) {
if (entityProperty == null) {
return p;
}
Class<?> dtoPropertyType = ReflectionUtils.getWrapperType(p.getType());
Class<?> entityPropertyType = ReflectionUtils.getWrapperType(entityProperty.getType());
if (!dtoPropertyType.equals(entityPropertyType)
&& !TypeUtils.areTypesCompatible(dtoPropertyType, entityPropertyType)
&& !TypeUtils.isDtoCompatibleWithEntity(dtoPropertyType, entityPropertyType)) {
return p;
}
return new BeanPropertyWithAnnotationMetadata<>(
Expand All @@ -719,7 +727,8 @@ protected <E, R> SqlTypeMapper<RS, R> createMapper(SqlStoredQuery<E, R> prepared
preparedQuery.getJoinPaths(),
sqlJsonColumnMapperProvider.getJsonColumnReader(preparedQuery, rsType),
null,
conversionService);
conversionService,
false);
}
return new SqlTypeMapper<>() {
@Override
Expand Down
Loading