diff --git a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/H2RepositorySpec.groovy b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/H2RepositorySpec.groovy index b5b2e8d2f9..321920cf96 100644 --- a/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/H2RepositorySpec.groovy +++ b/data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/H2RepositorySpec.groovy @@ -16,6 +16,9 @@ package io.micronaut.data.jdbc.h2 import groovy.transform.Memoized +import io.micronaut.data.tck.entities.embedded.BookEntity +import io.micronaut.data.tck.entities.embedded.BookState +import io.micronaut.data.tck.entities.embedded.ResourceEntity import io.micronaut.data.tck.repositories.* import io.micronaut.data.tck.tests.AbstractRepositorySpec import spock.lang.Shared @@ -27,8 +30,6 @@ import static io.micronaut.data.tck.repositories.PersonRepository.Specifications class H2RepositorySpec extends AbstractRepositorySpec implements H2TestPropertyProvider { - - @Shared H2PersonRepository pr = context.getBean(H2PersonRepository) @@ -95,6 +96,9 @@ class H2RepositorySpec extends AbstractRepositorySpec implements H2TestPropertyP @Shared H2EntityWithIdClass2Repository entityWithIdClass2Repo = context.getBean(H2EntityWithIdClass2Repository) + @Shared + H2BookEntityRepository bookEntityRepository = context.getBean(H2BookEntityRepository) + @Override EntityWithIdClassRepository getEntityWithIdClassRepository() { return entityWithIdClassRepo @@ -308,4 +312,14 @@ class H2RepositorySpec extends AbstractRepositorySpec implements H2TestPropertyP cleanupData() } + void "find by embedded entity field"() { + when: + def bookEntity = new BookEntity(1L, new ResourceEntity("1984", BookState.BORROWED)) + bookEntityRepository.save(bookEntity) + def result = bookEntityRepository.findAllByResourceState(BookState.BORROWED) + then: + result + cleanup: + bookEntityRepository.deleteAll() + } } diff --git a/data-jdbc/src/test/java/io/micronaut/data/jdbc/h2/H2BookEntityRepository.java b/data-jdbc/src/test/java/io/micronaut/data/jdbc/h2/H2BookEntityRepository.java new file mode 100644 index 0000000000..b9b957fc95 --- /dev/null +++ b/data-jdbc/src/test/java/io/micronaut/data/jdbc/h2/H2BookEntityRepository.java @@ -0,0 +1,9 @@ +package io.micronaut.data.jdbc.h2; + +import io.micronaut.data.jdbc.annotation.JdbcRepository; +import io.micronaut.data.model.query.builder.sql.Dialect; +import io.micronaut.data.tck.repositories.embedded.BookEntityRepository; + +@JdbcRepository(dialect = Dialect.H2) +public interface H2BookEntityRepository extends BookEntityRepository { +} diff --git a/data-jdbc/src/test/java/io/micronaut/data/jdbc/h2/H2HouseEntityRepository.java b/data-jdbc/src/test/java/io/micronaut/data/jdbc/h2/H2HouseEntityRepository.java new file mode 100644 index 0000000000..cfd40f556f --- /dev/null +++ b/data-jdbc/src/test/java/io/micronaut/data/jdbc/h2/H2HouseEntityRepository.java @@ -0,0 +1,9 @@ +package io.micronaut.data.jdbc.h2; + +import io.micronaut.data.jdbc.annotation.JdbcRepository; +import io.micronaut.data.model.query.builder.sql.Dialect; +import io.micronaut.data.tck.repositories.embedded.HouseEntityRepository; + +@JdbcRepository(dialect = Dialect.H2) +public interface H2HouseEntityRepository extends HouseEntityRepository { +} diff --git a/data-processor/src/main/java/io/micronaut/data/processor/visitors/RepositoryTypeElementVisitor.java b/data-processor/src/main/java/io/micronaut/data/processor/visitors/RepositoryTypeElementVisitor.java index e227f52cc6..f434917c90 100644 --- a/data-processor/src/main/java/io/micronaut/data/processor/visitors/RepositoryTypeElementVisitor.java +++ b/data-processor/src/main/java/io/micronaut/data/processor/visitors/RepositoryTypeElementVisitor.java @@ -186,7 +186,8 @@ public void visitClass(ClassElement element, VisitorContext context) { @Override public SourcePersistentEntity apply(ClassElement classElement) { - return entityMap.computeIfAbsent(classElement.getName(), s -> { + String classNameKey = getClassNameKey(classElement); + return entityMap.computeIfAbsent(classNameKey, s -> { if (classElement.hasAnnotation("io.micronaut.data.annotation.Embeddable")) { embeddedMappedEntityVisitor.visitClass(classElement, context); } else { @@ -768,4 +769,32 @@ private void annotateQueryResultIfApplicable(MethodElement element, MethodMatchI } } } + + /** + * Generates key for the entityMap using {@link ClassElement}. + * If class element has generic types then will use all bound generic types in the key like + * for example {@code Entity} and for non-generic class element + * will just return class name. + * This is needed when there are for example multiple embedded fields with the same type + * but different generic type argument. + * + * @param classElement The class element + * @return The key for entityMap created from the class element + */ + private String getClassNameKey(ClassElement classElement) { + List boundGenericTypes = classElement.getBoundGenericTypes(); + if (CollectionUtils.isNotEmpty(boundGenericTypes)) { + StringBuilder keyBuff = new StringBuilder(classElement.getName()); + keyBuff.append("<"); + for (ClassElement boundGenericType : boundGenericTypes) { + keyBuff.append(boundGenericType.getName()); + keyBuff.append(","); + } + keyBuff.deleteCharAt(keyBuff.length() - 1); + keyBuff.append(">"); + return keyBuff.toString(); + } else { + return classElement.getName(); + } + } } diff --git a/data-processor/src/test/groovy/io/micronaut/data/processor/sql/BuildQuerySpec.groovy b/data-processor/src/test/groovy/io/micronaut/data/processor/sql/BuildQuerySpec.groovy index ddbbb1cc88..fc5652f4c0 100644 --- a/data-processor/src/test/groovy/io/micronaut/data/processor/sql/BuildQuerySpec.groovy +++ b/data-processor/src/test/groovy/io/micronaut/data/processor/sql/BuildQuerySpec.groovy @@ -2053,4 +2053,28 @@ interface TestRepository extends GenericRepository { countQueryAnnotation.stringValue().get() == """SELECT COUNT(DISTINCT(book_."id")) FROM "book" book_ LEFT JOIN "book_student" book_students_book_student_ ON book_."id"=book_students_book_student_."book_id" LEFT JOIN "student" book_students_ ON book_students_book_student_."student_id"=book_students_."id" WHERE (book_students_."name" IN (?))""" countQueryAnnotation.getAnnotations("parameters").size() == 1 } + + void "test repository with reused embedded entity"() { + when: + buildRepository('test.TestRepository', """ +import io.micronaut.data.jdbc.annotation.JdbcRepository; +import io.micronaut.data.repository.GenericRepository; +import io.micronaut.data.model.query.builder.sql.Dialect; +import io.micronaut.data.tck.entities.embedded.BookEntity; +import io.micronaut.data.tck.entities.embedded.BookState; +import io.micronaut.data.tck.entities.embedded.HouseEntity; +import io.micronaut.data.tck.entities.embedded.HouseState; +import java.util.List; +@JdbcRepository(dialect = Dialect.POSTGRES) +interface TestRepository extends GenericRepository { + List findAllByResourceState(HouseState state); +} +@JdbcRepository(dialect = Dialect.POSTGRES) +interface OtherRepository extends GenericRepository { + List findAllByResourceState(BookState state); +} +""") + then: + noExceptionThrown() + } } diff --git a/data-tck/src/main/java/io/micronaut/data/tck/entities/embedded/BaseEntity.java b/data-tck/src/main/java/io/micronaut/data/tck/entities/embedded/BaseEntity.java new file mode 100644 index 0000000000..f25ae02d34 --- /dev/null +++ b/data-tck/src/main/java/io/micronaut/data/tck/entities/embedded/BaseEntity.java @@ -0,0 +1,8 @@ +package io.micronaut.data.tck.entities.embedded; + +interface BaseEntity> { + + I id(); + + ResourceEntity resource(); +} diff --git a/data-tck/src/main/java/io/micronaut/data/tck/entities/embedded/BookEntity.java b/data-tck/src/main/java/io/micronaut/data/tck/entities/embedded/BookEntity.java new file mode 100644 index 0000000000..6575f9c0a4 --- /dev/null +++ b/data-tck/src/main/java/io/micronaut/data/tck/entities/embedded/BookEntity.java @@ -0,0 +1,12 @@ +package io.micronaut.data.tck.entities.embedded; + +import io.micronaut.data.annotation.Id; +import io.micronaut.data.annotation.MappedEntity; +import jakarta.persistence.Embedded; + +@MappedEntity +public record BookEntity( + @Id Long id, + @Embedded ResourceEntity resource +) implements BaseEntity { +} diff --git a/data-tck/src/main/java/io/micronaut/data/tck/entities/embedded/BookState.java b/data-tck/src/main/java/io/micronaut/data/tck/entities/embedded/BookState.java new file mode 100644 index 0000000000..3080952d47 --- /dev/null +++ b/data-tck/src/main/java/io/micronaut/data/tck/entities/embedded/BookState.java @@ -0,0 +1,7 @@ +package io.micronaut.data.tck.entities.embedded; + +public enum BookState { + BORROWED, + READ, + RETURNED +} diff --git a/data-tck/src/main/java/io/micronaut/data/tck/entities/embedded/HouseEntity.java b/data-tck/src/main/java/io/micronaut/data/tck/entities/embedded/HouseEntity.java new file mode 100644 index 0000000000..78f85113a3 --- /dev/null +++ b/data-tck/src/main/java/io/micronaut/data/tck/entities/embedded/HouseEntity.java @@ -0,0 +1,12 @@ +package io.micronaut.data.tck.entities.embedded; + +import io.micronaut.data.annotation.Id; +import io.micronaut.data.annotation.MappedEntity; +import jakarta.persistence.Embedded; + +@MappedEntity +public record HouseEntity( + @Id Long id, + @Embedded ResourceEntity resource +) implements BaseEntity { +} diff --git a/data-tck/src/main/java/io/micronaut/data/tck/entities/embedded/HouseState.java b/data-tck/src/main/java/io/micronaut/data/tck/entities/embedded/HouseState.java new file mode 100644 index 0000000000..e424656a04 --- /dev/null +++ b/data-tck/src/main/java/io/micronaut/data/tck/entities/embedded/HouseState.java @@ -0,0 +1,7 @@ +package io.micronaut.data.tck.entities.embedded; + +public enum HouseState { + BUILDING, + FINISHED +} + diff --git a/data-tck/src/main/java/io/micronaut/data/tck/entities/embedded/ResourceEntity.java b/data-tck/src/main/java/io/micronaut/data/tck/entities/embedded/ResourceEntity.java new file mode 100644 index 0000000000..a783fda8f4 --- /dev/null +++ b/data-tck/src/main/java/io/micronaut/data/tck/entities/embedded/ResourceEntity.java @@ -0,0 +1,18 @@ +package io.micronaut.data.tck.entities.embedded; + +import io.micronaut.data.annotation.Embeddable; +import io.micronaut.data.annotation.TypeDef; +import io.micronaut.data.model.DataType; +import jakarta.persistence.Convert; + +@Embeddable +public record ResourceEntity>( + + String displayName, + + @TypeDef(type = DataType.STRING) + @Convert(converter = StateConverter.class) + S state +) { +} + diff --git a/data-tck/src/main/java/io/micronaut/data/tck/entities/embedded/StateConverter.java b/data-tck/src/main/java/io/micronaut/data/tck/entities/embedded/StateConverter.java new file mode 100644 index 0000000000..7ceab89b19 --- /dev/null +++ b/data-tck/src/main/java/io/micronaut/data/tck/entities/embedded/StateConverter.java @@ -0,0 +1,37 @@ +package io.micronaut.data.tck.entities.embedded; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +@Converter +class StateConverter implements AttributeConverter { + + @Override + public String convertToDatabaseColumn(Enum anEnum) { + if (anEnum == null) { + return null; + } + return anEnum.name(); + } + + @Override + public Enum convertToEntityAttribute(String string) { + if (string == null) { + return null; + } + // Because enum generics in ResourceEntity then implement this + // simple converter just to be able to run tests + if (string.equals("BORROWED")) { + return BookState.BORROWED; + } else if (string.equals("READ")) { + return BookState.READ; + } else if (string.equals("RETURNED")) { + return BookState.RETURNED; + } else if (string.equals("BUILDING")) { + return HouseState.BUILDING; + } else if (string.equals("FINISHED")) { + return HouseState.FINISHED; + } + throw new IllegalStateException("Unexpected enum value: " + string); + } +} diff --git a/data-tck/src/main/java/io/micronaut/data/tck/repositories/embedded/BookEntityRepository.java b/data-tck/src/main/java/io/micronaut/data/tck/repositories/embedded/BookEntityRepository.java new file mode 100644 index 0000000000..a56c702b80 --- /dev/null +++ b/data-tck/src/main/java/io/micronaut/data/tck/repositories/embedded/BookEntityRepository.java @@ -0,0 +1,12 @@ +package io.micronaut.data.tck.repositories.embedded; + +import io.micronaut.data.repository.CrudRepository; +import io.micronaut.data.tck.entities.embedded.BookEntity; +import io.micronaut.data.tck.entities.embedded.BookState; + +import java.util.List; + +public interface BookEntityRepository extends CrudRepository { + + List findAllByResourceState(BookState state); +} diff --git a/data-tck/src/main/java/io/micronaut/data/tck/repositories/embedded/HouseEntityRepository.java b/data-tck/src/main/java/io/micronaut/data/tck/repositories/embedded/HouseEntityRepository.java new file mode 100644 index 0000000000..ba47d877cf --- /dev/null +++ b/data-tck/src/main/java/io/micronaut/data/tck/repositories/embedded/HouseEntityRepository.java @@ -0,0 +1,12 @@ +package io.micronaut.data.tck.repositories.embedded; + +import io.micronaut.data.repository.CrudRepository; +import io.micronaut.data.tck.entities.embedded.HouseEntity; +import io.micronaut.data.tck.entities.embedded.HouseState; + +import java.util.List; + +public interface HouseEntityRepository extends CrudRepository { + + List findAllByResourceState(HouseState state); +}