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 db21b1aa015..a5b2e491498 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.AuthorRepository import io.micronaut.data.tck.repositories.BasicTypesRepository import io.micronaut.data.tck.repositories.BookDtoRepository @@ -113,6 +116,9 @@ class H2RepositorySpec extends AbstractRepositorySpec implements H2TestPropertyP @Shared H2EntityWithIdClass2Repository entityWithIdClass2Repo = context.getBean(H2EntityWithIdClass2Repository) + @Shared + H2BookEntityRepository bookEntityRepository = context.getBean(H2BookEntityRepository) + @Override EntityWithIdClassRepository getEntityWithIdClassRepository() { return entityWithIdClassRepo @@ -293,4 +299,12 @@ 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 + } } 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 00000000000..b9b957fc957 --- /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 00000000000..cfd40f556f6 --- /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 026d093ad91..ef69b69652f 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 @@ -184,7 +184,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 { @@ -755,4 +756,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 fac6eee9d65..728d67bae78 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 @@ -1992,4 +1992,28 @@ interface TestRepository extends GenericRepository { expect: getQuery(findByAuthorInListMethod) == "SELECT book_.`id`,book_.`author_id`,book_.`genre_id`,book_.`title`,book_.`total_pages`,book_.`publisher_id`,book_.`last_updated` FROM `book` book_ WHERE (book_.`author_id` IN (?))" } + + 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 00000000000..9e30b407619 --- /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; + +public 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 00000000000..6575f9c0a4e --- /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 00000000000..3080952d47f --- /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 00000000000..78f85113a36 --- /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 00000000000..ef103e34be1 --- /dev/null +++ b/data-tck/src/main/java/io/micronaut/data/tck/entities/embedded/HouseState.java @@ -0,0 +1,6 @@ +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 00000000000..59c4498f8c1 --- /dev/null +++ b/data-tck/src/main/java/io/micronaut/data/tck/entities/embedded/ResourceEntity.java @@ -0,0 +1,17 @@ +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 00000000000..e07f33ba18d --- /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 +public class StateConverter implements AttributeConverter { + + @Override + public String convertToDatabaseColumn(java.lang.Enum anEnum) { + if (anEnum == null) { + return null; + } + return anEnum.name(); + } + + @Override + public java.lang.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 00000000000..a56c702b80d --- /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 00000000000..ba47d877cfc --- /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); +}