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 b4d811c66f..657ad61058 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 @@ -427,6 +427,15 @@ 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 + * @since 4.7.0 + */ + 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 dbef7a3fe2..3d58528588 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; @@ -379,6 +380,23 @@ public PersistentEntity getParentEntity() { return null; } + @Override + public RuntimePersistentProperty getIdOrVersionPropertyByName(String name) { + if (ArrayUtils.isNotEmpty(identity)) { + for (RuntimePersistentProperty 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; } 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 46079445bb..8a0c7dc287 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/model/SourcePersistentEntity.java b/data-processor/src/main/java/io/micronaut/data/processor/model/SourcePersistentEntity.java index e2fe828bdc..acda82b99e 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 @@ -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; + } } } 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 607bea8356..02946e8723 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 @@ -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 getDtoProjectionProperties(SourcePersistentEntity entity, + protected List getDtoProjectionProperties(MethodMatchContext matchContext, SourcePersistentEntity entity, ClassElement returnType) { return returnType.getBeanProperties().stream() .filter(dtoProperty -> { @@ -751,9 +752,17 @@ protected List 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 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/TypeUtils.java b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/TypeUtils.java index 9bfccf822a..7ba4aec838 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 @@ -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; 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 dd830f358a..3b2ab72976 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 @@ -221,7 +221,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 54b7bdd250..2dd04a13f7 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 @@ -185,7 +185,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 fda95437a8..fe6bc04c0d 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 @@ -265,7 +265,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-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 b7c0a23ed1..6549b7e4ef 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 @@ -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; /** @@ -74,6 +75,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; /** @@ -89,8 +91,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); } /** @@ -107,7 +110,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); } /** @@ -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 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); } /** @@ -139,6 +145,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, @@ -146,7 +153,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); @@ -168,6 +177,7 @@ private SqlResultEntityTypeMapper( this.hasJoins = false; } this.startingPrefix = startingPrefix; + this.isDto = isDto; } @Override @@ -721,7 +731,13 @@ private K triggerPostLoad(RuntimePersistentEntity persistentEntity, K ent @Nullable private Object readEntityId(RS rs, MappingContext ctx) { RuntimePersistentProperty 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(); + } + } else { if (identity instanceof Embedded embedded) { return readEntity(rs, ctx.embedded(embedded), null, null); } 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 d9aedff2c4..beead11476 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 @@ -692,7 +692,8 @@ protected SqlTypeMapper createMapper(SqlStoredQuery prepared preparedQuery.getJoinPaths(), sqlJsonColumnMapperProvider.getJsonColumnReader(preparedQuery, rsType), loadListener, - conversionService); + conversionService, + false); } if (preparedQuery.isDtoProjection()) { RuntimePersistentEntity resultPersistentEntity = getEntity(preparedQuery.getResultType()); @@ -704,7 +705,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<>( @@ -719,7 +727,8 @@ protected SqlTypeMapper createMapper(SqlStoredQuery prepared preparedQuery.getJoinPaths(), sqlJsonColumnMapperProvider.getJsonColumnReader(preparedQuery, rsType), null, - conversionService); + conversionService, + false); } return new SqlTypeMapper<>() { @Override 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 0000000000..64a654d93a --- /dev/null +++ b/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/TypeUtils.java @@ -0,0 +1,121 @@ +/* + * 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 +@SuppressWarnings({"java:S1872"}) +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 d91526bb3a..fe8c2a8258 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 @@ -1426,6 +1426,26 @@ 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 + + when: + optBook = bookRepository.findByTitleStartingWith("The Stand") + + then: + optBook.present + // author not joined, should be null in DTO + !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 0000000000..92f8b0a16c --- /dev/null +++ b/data-tck/src/main/java/io/micronaut/data/tck/entities/BookDtoWithAuthorDto.java @@ -0,0 +1,55 @@ +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 4c210c441e..1b90e1fe40 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; @@ -183,4 +184,10 @@ protected Book newBook(Author author, String title, int pages) { public abstract List findByAuthorIds(List authorIds); public abstract List findByAuthorInList(List authors); + + @Join(value = "author") + public abstract Optional queryByTitleContains(String title); + + // Returns DTO without joined author + public abstract Optional findByTitleStartingWith(String title); }