Skip to content

Commit

Permalink
Fix mapping with embedded field with generic type (#3224)
Browse files Browse the repository at this point in the history
  • Loading branch information
radovanradic authored Nov 13, 2024
1 parent 43f44e7 commit 8fa4b37
Show file tree
Hide file tree
Showing 14 changed files with 213 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<BookState>("1984", BookState.BORROWED))
bookEntityRepository.save(bookEntity)
def result = bookEntityRepository.findAllByResourceState(BookState.BORROWED)
then:
result
cleanup:
bookEntityRepository.deleteAll()
}
}
Original file line number Diff line number Diff line change
@@ -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 {
}
Original file line number Diff line number Diff line change
@@ -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 {
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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<CustomKeyType, CustomValueType>} 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<? extends ClassElement> 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();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2053,4 +2053,28 @@ interface TestRepository extends GenericRepository<Book, Long> {
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<HouseEntity, Long> {
List<HouseEntity> findAllByResourceState(HouseState state);
}
@JdbcRepository(dialect = Dialect.POSTGRES)
interface OtherRepository extends GenericRepository<BookEntity, Long> {
List<BookEntity> findAllByResourceState(BookState state);
}
""")
then:
noExceptionThrown()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package io.micronaut.data.tck.entities.embedded;

interface BaseEntity<I, S extends Enum<S>> {

I id();

ResourceEntity<S> resource();
}
Original file line number Diff line number Diff line change
@@ -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<BookState> resource
) implements BaseEntity<Long, BookState> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package io.micronaut.data.tck.entities.embedded;

public enum BookState {
BORROWED,
READ,
RETURNED
}
Original file line number Diff line number Diff line change
@@ -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<HouseState> resource
) implements BaseEntity<Long, HouseState> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package io.micronaut.data.tck.entities.embedded;

public enum HouseState {
BUILDING,
FINISHED
}

Original file line number Diff line number Diff line change
@@ -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<S extends Enum<S>>(

String displayName,

@TypeDef(type = DataType.STRING)
@Convert(converter = StateConverter.class)
S state
) {
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package io.micronaut.data.tck.entities.embedded;

import jakarta.persistence.AttributeConverter;
import jakarta.persistence.Converter;

@Converter
class StateConverter implements AttributeConverter<Enum, String> {

@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);
}
}
Original file line number Diff line number Diff line change
@@ -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<BookEntity, Long> {

List<BookEntity> findAllByResourceState(BookState state);
}
Original file line number Diff line number Diff line change
@@ -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<HouseEntity, Long> {

List<HouseEntity> findAllByResourceState(HouseState state);
}

0 comments on commit 8fa4b37

Please sign in to comment.