From 37fa87153842d33b725a2ebd4ad01cfe8140769a Mon Sep 17 00:00:00 2001 From: radovanradic Date: Mon, 29 Jan 2024 09:30:32 +0100 Subject: [PATCH 1/7] DTO joins support issues --- .../tck/tests/AbstractRepositorySpec.groovy | 14 +++- .../tck/entities/BookDtoWithAuthorDto.java | 70 +++++++++++++++++++ .../data/tck/repositories/BookRepository.java | 3 + 3 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 data-tck/src/main/java/io/micronaut/data/tck/entities/BookDtoWithAuthorDto.java diff --git a/data-tck/src/main/groovy/io/micronaut/data/tck/tests/AbstractRepositorySpec.groovy b/data-tck/src/main/groovy/io/micronaut/data/tck/tests/AbstractRepositorySpec.groovy index 39cad5b4b92..b3361e8606d 100644 --- a/data-tck/src/main/groovy/io/micronaut/data/tck/tests/AbstractRepositorySpec.groovy +++ b/data-tck/src/main/groovy/io/micronaut/data/tck/tests/AbstractRepositorySpec.groovy @@ -48,9 +48,7 @@ import io.micronaut.data.tck.entities.Person import io.micronaut.data.tck.entities.Student import io.micronaut.data.tck.entities.TimezoneBasicTypes import io.micronaut.data.tck.jdbc.entities.Role -import io.micronaut.data.tck.jdbc.entities.User import io.micronaut.data.tck.jdbc.entities.UserRole -import io.micronaut.data.tck.jdbc.entities.UserRoleId import io.micronaut.data.tck.repositories.* import io.micronaut.transaction.SynchronousTransactionManager import io.micronaut.transaction.TransactionCallback @@ -1332,6 +1330,18 @@ abstract class AbstractRepositorySpec extends Specification { authors.forEach { assert it.books.size() == 0 } } + void "test DTO with nested DTO"() { + given: + saveSampleBooks() + + when: + def optBook = bookRepository.queryByTitleContains("Stand") + + then: + optBook.present + optBook.get().author + } + void "stream joined"() { if (!transactionManager.isPresent()) { return diff --git a/data-tck/src/main/java/io/micronaut/data/tck/entities/BookDtoWithAuthorDto.java b/data-tck/src/main/java/io/micronaut/data/tck/entities/BookDtoWithAuthorDto.java new file mode 100644 index 00000000000..f7449e706ba --- /dev/null +++ b/data-tck/src/main/java/io/micronaut/data/tck/entities/BookDtoWithAuthorDto.java @@ -0,0 +1,70 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.data.tck.entities; + +import io.micronaut.core.annotation.Introspected; + +import java.time.LocalDateTime; + +@Introspected +public class BookDtoWithAuthorDto { + + private String title; + private int totalPages; + private LocalDateTime lastUpdated; + + private AuthorDTO author; + + public BookDtoWithAuthorDto() { + } + + public BookDtoWithAuthorDto(String title, int totalPages) { + this.title = title; + this.totalPages = totalPages; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public int getTotalPages() { + return totalPages; + } + + public void setTotalPages(int totalPages) { + this.totalPages = totalPages; + } + + public LocalDateTime getLastUpdated() { + return lastUpdated; + } + + public void setLastUpdated(LocalDateTime lastUpdated) { + this.lastUpdated = lastUpdated; + } + + public AuthorDTO getAuthor() { + return author; + } + + public void setAuthor(AuthorDTO author) { + this.author = author; + } +} diff --git a/data-tck/src/main/java/io/micronaut/data/tck/repositories/BookRepository.java b/data-tck/src/main/java/io/micronaut/data/tck/repositories/BookRepository.java index 53b34e1b190..2f370a7ec3e 100644 --- a/data-tck/src/main/java/io/micronaut/data/tck/repositories/BookRepository.java +++ b/data-tck/src/main/java/io/micronaut/data/tck/repositories/BookRepository.java @@ -33,6 +33,7 @@ import io.micronaut.data.tck.entities.AuthorBooksDto; import io.micronaut.data.tck.entities.Book; import io.micronaut.data.tck.entities.BookDto; +import io.micronaut.data.tck.entities.BookDtoWithAuthorDto; import io.micronaut.data.tck.entities.Genre; import java.util.ArrayList; @@ -172,4 +173,6 @@ protected Book newBook(Author author, String title, int pages) { abstract List findByTitleInAndTotalPagesGreaterThan(List titles, int totalPages); abstract Long countByTitleInAndTotalPagesGreaterThan(List titles, int totalPages); + + abstract Optional queryByTitleContains(String title); } From b7c27cab197210e5def2971d3b4a43d0d25e4974 Mon Sep 17 00:00:00 2001 From: radovanradic Date: Mon, 29 Jan 2024 09:38:00 +0100 Subject: [PATCH 2/7] Check if entity and DTO field types are compatible --- .../finders/AbstractCriteriaMethodMatch.java | 12 +++++++++--- .../finders/criteria/DeleteCriteriaMethodMatch.java | 2 +- .../finders/criteria/QueryCriteriaMethodMatch.java | 2 +- .../finders/criteria/UpdateCriteriaMethodMatch.java | 2 +- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/AbstractCriteriaMethodMatch.java b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/AbstractCriteriaMethodMatch.java index 0613adcb023..5a4a29f3669 100644 --- a/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/AbstractCriteriaMethodMatch.java +++ b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/AbstractCriteriaMethodMatch.java @@ -792,11 +792,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 getDtoProjectionProperties(SourcePersistentEntity entity, + protected List getDtoProjectionProperties(MethodMatchContext matchContext, SourcePersistentEntity entity, ClassElement returnType) { return returnType.getBeanProperties().stream() .filter(dtoProperty -> { @@ -825,8 +826,13 @@ protected List getDtoProjectionProperties(SourcePersis // Convert anything to a string or an object return pp; } - if (!TypeUtils.areTypesCompatible(dtoPropertyType, pp.getType())) { - 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()); + boolean compatibleTypes = TypeUtils.areTypesCompatible(dtoPropertyType, pp.getType()); + if (!compatibleTypes) { + // Check if these are compatible non-simple field types (kind of nested DTOs) + List 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(); diff --git a/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/criteria/DeleteCriteriaMethodMatch.java b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/criteria/DeleteCriteriaMethodMatch.java index 5138c23551b..c36272bb427 100644 --- a/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/criteria/DeleteCriteriaMethodMatch.java +++ b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/criteria/DeleteCriteriaMethodMatch.java @@ -150,7 +150,7 @@ protected MethodMatchInfo build(MethodMatchContext matchContext) { ); if (result.isDto() && !result.isRuntimeDtoConversion()) { - List dtoProjectionProperties = getDtoProjectionProperties(matchContext.getRootEntity(), resultType); + List dtoProjectionProperties = getDtoProjectionProperties(matchContext, matchContext.getRootEntity(), resultType); if (!dtoProjectionProperties.isEmpty()) { List> selectionList = dtoProjectionProperties.stream() .map(p -> { diff --git a/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/criteria/QueryCriteriaMethodMatch.java b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/criteria/QueryCriteriaMethodMatch.java index 323b468751c..9662441c8ee 100644 --- a/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/criteria/QueryCriteriaMethodMatch.java +++ b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/criteria/QueryCriteriaMethodMatch.java @@ -160,7 +160,7 @@ protected MethodMatchInfo build(MethodMatchContext matchContext) { ); if (result.isDto() && !result.isRuntimeDtoConversion()) { - List dtoProjectionProperties = getDtoProjectionProperties(matchContext.getRootEntity(), resultType); + List dtoProjectionProperties = getDtoProjectionProperties(matchContext, matchContext.getRootEntity(), resultType); if (!dtoProjectionProperties.isEmpty()) { Root root = query.getRoots().iterator().next(); List> selectionList = dtoProjectionProperties.stream() diff --git a/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/criteria/UpdateCriteriaMethodMatch.java b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/criteria/UpdateCriteriaMethodMatch.java index af7a505f551..5a800124b83 100644 --- a/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/criteria/UpdateCriteriaMethodMatch.java +++ b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/criteria/UpdateCriteriaMethodMatch.java @@ -178,7 +178,7 @@ protected MethodMatchInfo build(MethodMatchContext matchContext) { ); if (result.isDto() && !result.isRuntimeDtoConversion()) { - List dtoProjectionProperties = getDtoProjectionProperties(matchContext.getRootEntity(), resultType); + List dtoProjectionProperties = getDtoProjectionProperties(matchContext, matchContext.getRootEntity(), resultType); if (!dtoProjectionProperties.isEmpty()) { List> selectionList = dtoProjectionProperties.stream() .map(p -> { From c526c081ac0bf3161fa36b1d551362e691d59023 Mon Sep 17 00:00:00 2001 From: radovanradic Date: Sun, 11 Feb 2024 12:04:30 +0100 Subject: [PATCH 3/7] Fix DTO many to one relation in jdbc --- .../data/model/PersistentEntity.java | 8 ++ .../runtime/RuntimePersistentEntity.java | 21 +++ .../model/SourcePersistentEntity.java | 7 +- .../mapper/sql/SqlResultEntityTypeMapper.java | 27 +++- .../sql/AbstractSqlRepositoryOperations.java | 15 ++- .../operations/internal/sql/TypeUtils.java | 120 ++++++++++++++++++ .../tck/tests/AbstractRepositorySpec.groovy | 8 ++ .../data/tck/repositories/BookRepository.java | 6 +- 8 files changed, 196 insertions(+), 16 deletions(-) create mode 100644 data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/TypeUtils.java diff --git a/data-model/src/main/java/io/micronaut/data/model/PersistentEntity.java b/data-model/src/main/java/io/micronaut/data/model/PersistentEntity.java index 7b0b3da75e5..0de8cc2e97c 100644 --- a/data-model/src/main/java/io/micronaut/data/model/PersistentEntity.java +++ b/data-model/src/main/java/io/micronaut/data/model/PersistentEntity.java @@ -405,6 +405,14 @@ default PersistentPropertyPath getPropertyPath(@NonNull String[] propertyPath) { @NonNull Optional 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 + */ + PersistentProperty getIdOrVersionPropertyByName(String name); + /** * 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. diff --git a/data-model/src/main/java/io/micronaut/data/model/runtime/RuntimePersistentEntity.java b/data-model/src/main/java/io/micronaut/data/model/runtime/RuntimePersistentEntity.java index c94785d0c5f..8a437870bd5 100644 --- a/data-model/src/main/java/io/micronaut/data/model/runtime/RuntimePersistentEntity.java +++ b/data-model/src/main/java/io/micronaut/data/model/runtime/RuntimePersistentEntity.java @@ -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; @@ -366,6 +367,26 @@ public PersistentEntity getParentEntity() { return null; } + @Override + public RuntimePersistentProperty getIdOrVersionPropertyByName(String name) { + if (ArrayUtils.isNotEmpty(identity)) { + RuntimePersistentProperty persistentProp = Arrays.stream(identity) + .filter(p -> p.getName().equals(name)) + .findFirst() + .orElse(null); + + if (persistentProp != null) { + return persistentProp; + } + } + + 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; } diff --git a/data-processor/src/main/java/io/micronaut/data/processor/model/SourcePersistentEntity.java b/data-processor/src/main/java/io/micronaut/data/processor/model/SourcePersistentEntity.java index 42d213a983f..179493a08f1 100644 --- a/data-processor/src/main/java/io/micronaut/data/processor/model/SourcePersistentEntity.java +++ b/data-processor/src/main/java/io/micronaut/data/processor/model/SourcePersistentEntity.java @@ -185,12 +185,7 @@ 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) diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/mapper/sql/SqlResultEntityTypeMapper.java b/data-runtime/src/main/java/io/micronaut/data/runtime/mapper/sql/SqlResultEntityTypeMapper.java index 21c6f72cad8..8d48d6c74e9 100644 --- a/data-runtime/src/main/java/io/micronaut/data/runtime/mapper/sql/SqlResultEntityTypeMapper.java +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/mapper/sql/SqlResultEntityTypeMapper.java @@ -52,6 +52,7 @@ import java.util.Objects; import java.util.Set; import java.util.StringJoiner; +import java.util.UUID; import java.util.function.BiFunction; /** @@ -71,6 +72,7 @@ public final class SqlResultEntityTypeMapper implements SqlTypeMapper jsonColumnReader; private final DataConversionService conversionService; private final BiFunction, Object, Object> eventListener; + private final boolean isDto; private boolean callNext = true; /** @@ -86,8 +88,9 @@ public SqlResultEntityTypeMapper( String prefix, @NonNull RuntimePersistentEntity entity, @NonNull ResultReader resultReader, - @Nullable SqlJsonColumnReader jsonColumnReader, DataConversionService conversionService) { - this(entity, resultReader, Collections.emptySet(), prefix, jsonColumnReader, conversionService, null); + @Nullable SqlJsonColumnReader jsonColumnReader, + DataConversionService conversionService) { + this(entity, resultReader, Collections.emptySet(), prefix, jsonColumnReader, conversionService, null, false); } /** @@ -104,7 +107,7 @@ public SqlResultEntityTypeMapper( @NonNull ResultReader resultReader, @Nullable Set joinPaths, @Nullable SqlJsonColumnReader jsonColumnReader, DataConversionService conversionService) { - this(entity, resultReader, joinPaths, null, jsonColumnReader, conversionService, null); + this(entity, resultReader, joinPaths, null, jsonColumnReader, conversionService, null, false); } /** @@ -116,14 +119,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 entity, @NonNull ResultReader resultReader, @Nullable Set joinPaths, @Nullable SqlJsonColumnReader jsonColumnReader, - @Nullable BiFunction, Object, Object> loadListener, DataConversionService conversionService) { - this(entity, resultReader, joinPaths, null, jsonColumnReader, conversionService, loadListener); + @Nullable BiFunction, Object, Object> loadListener, + DataConversionService conversionService, + boolean isDto) { + this(entity, resultReader, joinPaths, null, jsonColumnReader, conversionService, loadListener, isDto); } /** @@ -136,6 +142,7 @@ 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 entity, @@ -143,7 +150,9 @@ private SqlResultEntityTypeMapper( @Nullable Set joinPaths, String startingPrefix, @Nullable SqlJsonColumnReader jsonColumnReader, - DataConversionService conversionService, @Nullable BiFunction, Object, Object> eventListener) { + DataConversionService conversionService, + @Nullable BiFunction, Object, Object> eventListener, + boolean isDto) { this.conversionService = conversionService; ArgumentUtils.requireNonNull("entity", entity); ArgumentUtils.requireNonNull("resultReader", resultReader); @@ -160,6 +169,7 @@ private SqlResultEntityTypeMapper( this.joinPaths = Collections.emptyMap(); } this.startingPrefix = startingPrefix; + this.isDto = isDto; } @Override @@ -622,6 +632,11 @@ private K triggerPostLoad(RuntimePersistentEntity persistentEntity, K ent private Object readEntityId(RS rs, MappingContext ctx) { RuntimePersistentProperty identity = ctx.persistentEntity.getIdentity(); 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(); + } return null; } if (identity instanceof Embedded) { diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/AbstractSqlRepositoryOperations.java b/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/AbstractSqlRepositoryOperations.java index 91f886c00a6..94dbd871ac4 100644 --- a/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/AbstractSqlRepositoryOperations.java +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/AbstractSqlRepositoryOperations.java @@ -727,7 +727,8 @@ protected SqlTypeMapper createMapper(SqlStoredQuery prepared joinFetchPaths, sqlJsonColumnMapperProvider.getJsonColumnReader(preparedQuery, rsType), loadListener, - conversionService); + conversionService, + false); } else { RuntimePersistentEntity resultPersistentEntity = getEntity(resultType); Collection> beanProperties = resultPersistentEntity.getIntrospection().getBeanProperties(); @@ -738,7 +739,14 @@ protected SqlTypeMapper createMapper(SqlStoredQuery prepared return p; } RuntimePersistentProperty 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<>( @@ -753,7 +761,8 @@ protected SqlTypeMapper createMapper(SqlStoredQuery prepared joinFetchPaths, sqlJsonColumnMapperProvider.getJsonColumnReader(preparedQuery, rsType), null, - conversionService); + conversionService, + true); } } return new SqlTypeMapper<>() { diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/TypeUtils.java b/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/TypeUtils.java new file mode 100644 index 00000000000..e157219b7b9 --- /dev/null +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/TypeUtils.java @@ -0,0 +1,120 @@ +/* + * Copyright 2017-2024 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.data.runtime.operations.internal.sql; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.beans.BeanIntrospection; +import io.micronaut.core.beans.BeanProperty; +import io.micronaut.core.reflect.ClassUtils; +import io.micronaut.core.reflect.ReflectionUtils; +import io.micronaut.data.model.runtime.RuntimePersistentEntity; +import io.micronaut.data.model.runtime.RuntimePersistentProperty; + +/** + * The type utils for DTO and persistent entity type. + */ +@Internal +final class TypeUtils { + + private TypeUtils() { + } + + /** + * Return true if the left type is compatible or can be assigned to the right type. + * @param leftType The left type + * @param rightType The right type + * @return True if they are + */ + static boolean areTypesCompatible(Class leftType, Class rightType) { + String rightTypeName = rightType.getName(); + if (leftType.getName().equals(rightTypeName)) { + return true; + } else if (leftType.isInstance(rightTypeName)) { + return true; + } else { + if (isNumber(leftType) && isNumber(rightType)) { + return true; + } else { + return isBoolean(leftType) && isBoolean(rightType); + } + } + } + + /** + * Checks whether DTO type and given persistent entity type are compatible. + * It means DTO types will have corresponding compatible fields in the persistent entity. + * + * @param dtoType The DTO type + * @param entityType The persistent entity type + * @return true if types are compatible + */ + static boolean isDtoCompatibleWithEntity(@NonNull Class dtoType, @NonNull Class entityType) { + // If DTO type not introspected it is going to fail and throw error to the user + // that type must be @Introspected + BeanIntrospection dto = BeanIntrospection.getIntrospection(dtoType); + RuntimePersistentEntity entity = new RuntimePersistentEntity<>(entityType); + for (BeanProperty dtoProperty : dto.getBeanProperties()) { + String propertyName = dtoProperty.getName(); + // ignore Groovy meta class + if ("metaClass".equals(propertyName) || dtoProperty.getType().getName().equals("groovy.lang.MetaClass")) { + continue; + } + RuntimePersistentProperty pp = entity.getPropertyByName(propertyName); + + if (pp == null) { + pp = entity.getIdOrVersionPropertyByName(propertyName); + } + + if (pp == null) { + // Property is not present in entity + return false; + } + + Class dtoPropertyType = dtoProperty.getType(); + if (dtoPropertyType.getName().equals("java.lang.Object") || dtoPropertyType.getName().equals("java.lang.String")) { + // Convert anything to a string or an object + continue; + } + boolean compatibleTypes = areTypesCompatible(dtoPropertyType, pp.getType()); + // Check if these are compatible non-simple field types (kind of nested DTOs) + if (!compatibleTypes && !isDtoCompatibleWithEntity(dtoPropertyType, pp.getType())) { + // DTO Property is not compatible with equivalent property of type declared in entity + return false; + } + } + return true; + } + + private static boolean isNumber(@Nullable Class type) { + if (type == null) { + return false; + } + if (type.isPrimitive()) { + return ClassUtils.getPrimitiveType(type.getName()).map(aClass -> + Number.class.isAssignableFrom(ReflectionUtils.getWrapperType(aClass)) + ).orElse(false); + } else { + return type.isInstance(Number.class); + } + } + + private static boolean isBoolean(@Nullable Class type) { + return type != null && + (type.isInstance(Boolean.class) || (type.isPrimitive() && type.getName().equals("boolean"))); + } +} diff --git a/data-tck/src/main/groovy/io/micronaut/data/tck/tests/AbstractRepositorySpec.groovy b/data-tck/src/main/groovy/io/micronaut/data/tck/tests/AbstractRepositorySpec.groovy index b3361e8606d..9d8382bf841 100644 --- a/data-tck/src/main/groovy/io/micronaut/data/tck/tests/AbstractRepositorySpec.groovy +++ b/data-tck/src/main/groovy/io/micronaut/data/tck/tests/AbstractRepositorySpec.groovy @@ -1340,6 +1340,14 @@ abstract class AbstractRepositorySpec extends Specification { then: optBook.present optBook.get().author + + when: + optBook = bookRepository.findByTitleStartingWith("The Stand") + + then: + optBook.present + // author not joined, should be null in DTO + !optBook.get().author } void "stream joined"() { diff --git a/data-tck/src/main/java/io/micronaut/data/tck/repositories/BookRepository.java b/data-tck/src/main/java/io/micronaut/data/tck/repositories/BookRepository.java index 2f370a7ec3e..aa1e62c1e15 100644 --- a/data-tck/src/main/java/io/micronaut/data/tck/repositories/BookRepository.java +++ b/data-tck/src/main/java/io/micronaut/data/tck/repositories/BookRepository.java @@ -174,5 +174,9 @@ protected Book newBook(Author author, String title, int pages) { abstract Long countByTitleInAndTotalPagesGreaterThan(List titles, int totalPages); - abstract Optional queryByTitleContains(String title); + @Join(value = "author") + public abstract Optional queryByTitleContains(String title); + + // Returns DTO without joined author + public abstract Optional findByTitleStartingWith(String title); } From 78d0df409fd2215cc0bd5c99cd9df2e76dfb5447 Mon Sep 17 00:00:00 2001 From: radovanradic Date: Sun, 11 Feb 2024 14:26:17 +0100 Subject: [PATCH 4/7] Sonar and coverage issues --- .../runtime/RuntimePersistentEntitySpec.groovy | 10 +++++++++- .../finders/AbstractCriteriaMethodMatch.java | 15 +++++++++------ .../data/tck/entities/BookDtoWithAuthorDto.java | 15 --------------- 3 files changed, 18 insertions(+), 22 deletions(-) diff --git a/data-model/src/test/groovy/io/micronaut/data/model/runtime/RuntimePersistentEntitySpec.groovy b/data-model/src/test/groovy/io/micronaut/data/model/runtime/RuntimePersistentEntitySpec.groovy index 46079445bbb..8a0c7dc2874 100644 --- a/data-model/src/test/groovy/io/micronaut/data/model/runtime/RuntimePersistentEntitySpec.groovy +++ b/data-model/src/test/groovy/io/micronaut/data/model/runtime/RuntimePersistentEntitySpec.groovy @@ -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 { @@ -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 } } @@ -23,4 +28,7 @@ class Test { UUID id String name -} \ No newline at end of file + + @Version + Long version +} diff --git a/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/AbstractCriteriaMethodMatch.java b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/AbstractCriteriaMethodMatch.java index 5a4a29f3669..0f9f46ebab9 100644 --- a/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/AbstractCriteriaMethodMatch.java +++ b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/AbstractCriteriaMethodMatch.java @@ -827,13 +827,16 @@ protected List getDtoProjectionProperties(MethodMatchC return pp; } boolean compatibleTypes = TypeUtils.areTypesCompatible(dtoPropertyType, pp.getType()); - if (!compatibleTypes) { - // Check if these are compatible non-simple field types (kind of nested DTOs) - List 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()); - } + if (compatibleTypes) { + return pp; } + + // Check if these are compatible non-simple field types (kind of nested DTOs) + List 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(); } diff --git a/data-tck/src/main/java/io/micronaut/data/tck/entities/BookDtoWithAuthorDto.java b/data-tck/src/main/java/io/micronaut/data/tck/entities/BookDtoWithAuthorDto.java index f7449e706ba..92f8b0a16c2 100644 --- a/data-tck/src/main/java/io/micronaut/data/tck/entities/BookDtoWithAuthorDto.java +++ b/data-tck/src/main/java/io/micronaut/data/tck/entities/BookDtoWithAuthorDto.java @@ -1,18 +1,3 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ package io.micronaut.data.tck.entities; import io.micronaut.core.annotation.Introspected; From 3fee2bc52bfbf0b44ffcb96cfa6f0554655cfc4c Mon Sep 17 00:00:00 2001 From: radovanradic Date: Sun, 11 Feb 2024 15:19:02 +0100 Subject: [PATCH 5/7] Ignore Sonar error suggesting using instanceOf --- .../data/runtime/operations/internal/sql/TypeUtils.java | 1 + 1 file changed, 1 insertion(+) diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/TypeUtils.java b/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/TypeUtils.java index e157219b7b9..64a654d93aa 100644 --- a/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/TypeUtils.java +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/TypeUtils.java @@ -29,6 +29,7 @@ * The type utils for DTO and persistent entity type. */ @Internal +@SuppressWarnings({"java:S1872"}) final class TypeUtils { private TypeUtils() { From d3744c06010769b04e414868f85e348d3c6084bb Mon Sep 17 00:00:00 2001 From: radovanradic Date: Mon, 12 Feb 2024 15:45:17 +0100 Subject: [PATCH 6/7] Address CR comments --- .../data/model/runtime/RuntimePersistentEntity.java | 11 ++++------- .../data/processor/model/SourcePersistentEntity.java | 11 ++++------- .../data/processor/visitors/finders/TypeUtils.java | 2 -- 3 files changed, 8 insertions(+), 16 deletions(-) diff --git a/data-model/src/main/java/io/micronaut/data/model/runtime/RuntimePersistentEntity.java b/data-model/src/main/java/io/micronaut/data/model/runtime/RuntimePersistentEntity.java index 8a437870bd5..896847f92af 100644 --- a/data-model/src/main/java/io/micronaut/data/model/runtime/RuntimePersistentEntity.java +++ b/data-model/src/main/java/io/micronaut/data/model/runtime/RuntimePersistentEntity.java @@ -370,13 +370,10 @@ public PersistentEntity getParentEntity() { @Override public RuntimePersistentProperty getIdOrVersionPropertyByName(String name) { if (ArrayUtils.isNotEmpty(identity)) { - RuntimePersistentProperty persistentProp = Arrays.stream(identity) - .filter(p -> p.getName().equals(name)) - .findFirst() - .orElse(null); - - if (persistentProp != null) { - return persistentProp; + for (RuntimePersistentProperty identityProperty : identity) { + if (identityProperty.getName().equals(name)) { + return identityProperty; + } } } diff --git a/data-processor/src/main/java/io/micronaut/data/processor/model/SourcePersistentEntity.java b/data-processor/src/main/java/io/micronaut/data/processor/model/SourcePersistentEntity.java index 179493a08f1..39a6520ba94 100644 --- a/data-processor/src/main/java/io/micronaut/data/processor/model/SourcePersistentEntity.java +++ b/data-processor/src/main/java/io/micronaut/data/processor/model/SourcePersistentEntity.java @@ -188,13 +188,10 @@ public SourcePersistentProperty getIdentityByName(String name) { @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; + } } } diff --git a/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/TypeUtils.java b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/TypeUtils.java index 959a098ccbd..65e1c3d3f7b 100644 --- a/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/TypeUtils.java +++ b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/TypeUtils.java @@ -542,8 +542,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; From e7a0a4a0e83a0ce16a340999d5e7efcc4f2f0bba Mon Sep 17 00:00:00 2001 From: radovanradic Date: Mon, 12 Feb 2024 15:47:52 +0100 Subject: [PATCH 7/7] Added since --- .../src/main/java/io/micronaut/data/model/PersistentEntity.java | 1 + 1 file changed, 1 insertion(+) diff --git a/data-model/src/main/java/io/micronaut/data/model/PersistentEntity.java b/data-model/src/main/java/io/micronaut/data/model/PersistentEntity.java index 0de8cc2e97c..bfab8016410 100644 --- a/data-model/src/main/java/io/micronaut/data/model/PersistentEntity.java +++ b/data-model/src/main/java/io/micronaut/data/model/PersistentEntity.java @@ -410,6 +410,7 @@ default PersistentPropertyPath getPropertyPath(@NonNull String[] propertyPath) { * * @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);