Skip to content

Commit

Permalink
Working on fix for many to one relation loading
Browse files Browse the repository at this point in the history
  • Loading branch information
radovanradic committed Feb 27, 2025
1 parent 81ace1d commit b2e5b15
Show file tree
Hide file tree
Showing 24 changed files with 294 additions and 52 deletions.
1 change: 1 addition & 0 deletions .github/workflows/gradle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ on:
branches:
- master
- '[1-9]+.[0-9]+.x'
- 'fix-manytoone-join'
pull_request:
branches:
- master
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ import io.micronaut.data.model.Page
import io.micronaut.data.model.Pageable
import io.micronaut.data.model.query.builder.sql.Dialect
import io.micronaut.data.repository.CrudRepository
import io.micronaut.data.tck.entities.Document
import io.micronaut.data.tck.entities.DocumentType
import io.micronaut.data.tck.repositories.DocumentRepository
import io.micronaut.data.tck.repositories.DocumentTypeRepository
import io.micronaut.test.extensions.spock.annotation.MicronautTest
import spock.lang.AutoCleanup
import spock.lang.Shared
Expand Down Expand Up @@ -50,6 +54,14 @@ class MultiManyToOneJoinSpec extends Specification implements H2TestPropertyProv
@Inject
MyOtherRepository myOtherRepository

@Shared
@Inject
DocumentTypeRepository documentTypeRepository

@Shared
@Inject
DocumentRepository documentRepository

void 'test many-to-one hierarchy'() {
given:
RefA refA = new RefA(refB: new RefB(refC: new RefC(name: "TestXyz")))
Expand Down Expand Up @@ -143,6 +155,25 @@ class MultiManyToOneJoinSpec extends Specification implements H2TestPropertyProv
optFound.get().other
optFound.get().other.lid == myOther.lid
}

void "test many to one join nullable"() {
when:
def documentType = documentTypeRepository.save(new DocumentType(null, "PDF", false));
def document = documentRepository.save(new Document(null, "Opinion.pdf", documentType))
then:
def optionalDocument = documentRepository.findById(document.id())
optionalDocument.present
optionalDocument.get().type()
when:
documentTypeRepository.updateDeletedById(documentType.id(), true)
optionalDocument = documentRepository.findById(document.id())
then:
optionalDocument.present
!optionalDocument.get().type()
cleanup:
documentRepository.deleteAll()
documentTypeRepository.deleteAll()
}
}

@JdbcRepository(dialect = Dialect.H2)
Expand Down Expand Up @@ -291,9 +322,12 @@ class User {
@JdbcRepository(dialect = Dialect.H2)
interface UserGroupMembershipRepository extends CrudRepository<UserGroupMembership, Long> {

@Join(value = "userGroup.area", type = Join.Type.FETCH)
@Join(value = "user", type = Join.Type.FETCH)
List<UserGroupMembership> findAllByUserLogin(String login)

@Join(value = "userGroup.area", type = Join.Type.FETCH)
@Join(value = "user", type = Join.Type.FETCH)
List<UserGroupMembership> findAllByUserLoginAndUserGroup_AreaId(String login, Long uid)
}

Expand Down Expand Up @@ -327,6 +361,10 @@ class MyOther {
}
@JdbcRepository(dialect = H2)
interface MyEntityRepository extends CrudRepository<MyEntity, Long> {

@Override
@Join(value = "other", type = Join.Type.LEFT_FETCH)
Optional<MyEntity> findById(Long aLong);
}
@JdbcRepository(dialect = H2)
interface MyOtherRepository extends CrudRepository<MyOther, String> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ class OneToManyHierarchicalSpec extends Specification implements H2TestPropertyP
loadedTestEntity.children.size() == 1
optTestHierarchyEntity.present
def loadedTestHierarchyEntity = optTestHierarchyEntity.get()
loadedTestHierarchyEntity.parent
!loadedTestHierarchyEntity.parent
!loadedTestHierarchyEntity.child
when:
def testEntities = testEntityRepository.findAll(Specifications.getChildrenByParentCodeSpecification("code2"))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.micronaut.data.jdbc.h2.remap;

import io.micronaut.core.annotation.Nullable;
import io.micronaut.data.annotation.Id;
import io.micronaut.data.annotation.MappedEntity;
import io.micronaut.data.annotation.Relation;
Expand All @@ -19,6 +20,7 @@ record Course(
String name,

@Relation(value = MANY_TO_MANY, mappedBy = "courses", cascade = ALL)
@Nullable
List<Student> students
) {
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.micronaut.data.jdbc.h2.remap;

import io.micronaut.core.annotation.Nullable;
import io.micronaut.data.annotation.Id;
import io.micronaut.data.annotation.MappedEntity;
import io.micronaut.data.annotation.MappedProperty;
Expand All @@ -21,6 +22,7 @@ record Student(
String name,

@Relation(value = MANY_TO_MANY, cascade = ALL)
@Nullable
List<Course> courses
) {
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
package io.micronaut.data.jdbc.h2.remap;

import io.micronaut.data.annotation.Join;
import io.micronaut.data.jdbc.annotation.JdbcRepository;
import io.micronaut.data.model.query.builder.sql.Dialect;
import io.micronaut.data.repository.CrudRepository;

import java.util.Optional;

@JdbcRepository(dialect = Dialect.H2)
interface StudentRepository extends CrudRepository<Student, StudentId> {

@Join(value = "courses", type = Join.Type.LEFT_FETCH)
Optional<Student> findById(StudentId id);
}
Original file line number Diff line number Diff line change
Expand Up @@ -449,7 +449,8 @@ class PostgresRepositorySpec extends AbstractRepositorySpec implements PostgresT
when:
def b = bookRepository.modifyReturning(petCemetery.author.id)
then:
b.author.id == petCemetery.author.id
// Not loaded because it is many to one
!b.author
b.postLoad == 1
when:
def allBooks = bookRepository.findAll()
Expand Down Expand Up @@ -486,7 +487,8 @@ class PostgresRepositorySpec extends AbstractRepositorySpec implements PostgresT
when:
def b = bookRepository.customUpdateReturningBook(petCemetery.author.id)
then:
b.author.id == petCemetery.author.id
// null because many to one mapping is not loaded in updateReturning
!b.author
b.postLoad == 1
when:
def allBooks = bookRepository.findAll()
Expand Down
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.DocumentRepository;

@JdbcRepository(dialect = Dialect.H2)
public interface H2DocumentRepository extends DocumentRepository {
}
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.DocumentTypeRepository;

@JdbcRepository(dialect = Dialect.H2)
public interface H2DocumentTypeRepository extends DocumentTypeRepository {
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import io.micronaut.core.annotation.Nullable;
import io.micronaut.data.annotation.Expandable;
import io.micronaut.data.annotation.Id;
import io.micronaut.data.annotation.Join;
import io.micronaut.data.annotation.Query;
import io.micronaut.data.annotation.TypeDef;
import io.micronaut.data.annotation.sql.Procedure;
Expand Down Expand Up @@ -129,4 +130,8 @@ public abstract Book customInsertReturningBook(Long authorId,
DELETE FROM "book" WHERE "id" = :id RETURNING *
""")
public abstract Book customDeleteOne(Long id);

@Override
@Join(value = "author", type = Join.Type.LEFT_FETCH)
public abstract List<Book> findAll();
}
12 changes: 12 additions & 0 deletions data-model/src/main/java/io/micronaut/data/model/Association.java
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,18 @@ default boolean isForeignKey() {
return kind == Relation.Kind.ONE_TO_MANY || kind == Relation.Kind.MANY_TO_MANY || (kind == Relation.Kind.ONE_TO_ONE && getAnnotationMetadata().stringValue(Relation.class, "mappedBy").isPresent());
}

/**
* Determines whether this association is single-ended, meaning it only has one end point.
* An association is considered single-ended if its kind is either ONE_TO_ONE or MANY_TO_ONE,
* but not EMBEDDED.
*
* @return True if the association is single-ended, false otherwise.
*/
default boolean isSingleEnded() {
Relation.Kind kind = getKind();
return kind.isSingleEnded() && !kind.equals(Relation.Kind.EMBEDDED);
}

/**
* Whether this association cascades the given types.
* @param types The types
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import io.micronaut.core.annotation.AnnotationMetadata;
import io.micronaut.core.annotation.AnnotationValue;
import io.micronaut.core.annotation.Internal;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.naming.NameUtils;
import io.micronaut.data.annotation.sql.JoinColumn;
import io.micronaut.data.annotation.sql.JoinColumns;
Expand All @@ -29,6 +30,7 @@
import java.util.Optional;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Predicate;

/**
* Persistent entity utils.
Expand Down Expand Up @@ -81,14 +83,18 @@ public static void traversePersistentProperties(PersistentProperty property, BiC
* @param consumer The function to invoke on every property
*/
public static void traversePersistentProperties(PersistentEntity persistentEntity, BiConsumer<List<Association>, PersistentProperty> consumer) {
traversePersistentProperties(persistentEntity, null, consumer);
}

public static void traversePersistentProperties(PersistentEntity persistentEntity, @Nullable Predicate<Association> skipAssociationPredicate, BiConsumer<List<Association>, PersistentProperty> consumer) {
for (PersistentProperty identityProperty : persistentEntity.getIdentityProperties()) {
traversePersistentProperties(Collections.emptyList(), identityProperty, consumer);
traversePersistentProperties(Collections.emptyList(), identityProperty, skipAssociationPredicate, consumer);
}
if (persistentEntity.getVersion() != null) {
traversePersistentProperties(Collections.emptyList(), persistentEntity.getVersion(), consumer);
traversePersistentProperties(Collections.emptyList(), persistentEntity.getVersion(), skipAssociationPredicate, consumer);
}
for (PersistentProperty property : persistentEntity.getPersistentProperties()) {
traversePersistentProperties(Collections.emptyList(), property, consumer);
traversePersistentProperties(Collections.emptyList(), property, skipAssociationPredicate, consumer);
}
}

Expand Down Expand Up @@ -141,7 +147,14 @@ public static int countPersistentProperties(List<Association> associations,
public static void traversePersistentProperties(List<Association> associations,
PersistentProperty property,
BiConsumer<List<Association>, PersistentProperty> consumerProperty) {
traversePersistentProperties(associations, property, true, consumerProperty);
traversePersistentProperties(associations, property, null, consumerProperty);
}

public static void traversePersistentProperties(List<Association> associations,
PersistentProperty property,
@Nullable Predicate<Association> skipAssociationPredicate,
BiConsumer<List<Association>, PersistentProperty> consumerProperty) {
traversePersistentProperties(associations, property, true, skipAssociationPredicate, consumerProperty);
}

public static void traversePersistentProperties(PersistentPropertyPath propertyPath,
Expand All @@ -165,6 +178,14 @@ public static void traversePersistentProperties(List<Association> associations,
PersistentProperty property,
boolean traverseEmbedded,
BiConsumer<List<Association>, PersistentProperty> consumerProperty) {
traversePersistentProperties(associations, property, traverseEmbedded, null, consumerProperty);
}

public static void traversePersistentProperties(List<Association> associations,
PersistentProperty property,
boolean traverseEmbedded,
@Nullable Predicate<Association> skipAssociationPredicate,
BiConsumer<List<Association>, PersistentProperty> consumerProperty) {
if (property instanceof Embedded embedded) {
if (traverseEmbedded) {
PersistentEntity embeddedEntity = embedded.getAssociatedEntity();
Expand All @@ -181,6 +202,9 @@ public static void traversePersistentProperties(List<Association> associations,
if (association.isForeignKey()) {
return;
}
if (skipAssociationPredicate != null && skipAssociationPredicate.test(association)) {
return;
}
List<Association> newAssociations = new ArrayList<>(associations);
newAssociations.add((Association) property);
PersistentEntity associatedEntity = association.getAssociatedEntity();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1666,7 +1666,7 @@ protected void selectAllColumnsFromJoinPaths(Collection<JoinPath> allPaths,

query.append(COMMA);

boolean includeIdentity = association.isForeignKey();
boolean includeIdentity = association.isForeignKey() || association.isSingleEnded();
// in the case of a foreign key association the ID is not in the table,
// so we need to retrieve it
PersistentEntityUtils.traversePersistentProperties(associatedEntity, includeIdentity, true, (propertyAssociations, prop) -> {
Expand Down Expand Up @@ -1710,7 +1710,7 @@ public void selectAllColumns(AnnotationMetadata annotationMetadata, PersistentEn
boolean escape = shouldEscape(entity);
NamingStrategy namingStrategy = getNamingStrategy(entity);
int length = query.length();
PersistentEntityUtils.traversePersistentProperties(entity, (associations, property)
PersistentEntityUtils.traversePersistentProperties(entity, Association::isSingleEnded, (associations, property)
-> appendProperty(query, associations, property, namingStrategy, alias, escape));
int newLength = query.length();
if (newLength == length) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ public class RuntimeAssociation<T> extends RuntimePersistentProperty<T> implemen
private final Relation.Kind kind;
private final String aliasName;
private final boolean isForeignKey;
private final boolean isSingleEnded;

/**
* Default constructor.
Expand All @@ -48,13 +49,19 @@ public class RuntimeAssociation<T> extends RuntimePersistentProperty<T> implemen
this.kind = Association.super.getKind();
this.aliasName = Association.super.getAliasName();
this.isForeignKey = Association.super.isForeignKey();
this.isSingleEnded = Association.super.isSingleEnded();
}

@Override
public boolean isForeignKey() {
return isForeignKey;
}

@Override
public boolean isSingleEnded() {
return isSingleEnded;
}

@Override
public String getAliasName() {
return aliasName;
Expand Down
Loading

0 comments on commit b2e5b15

Please sign in to comment.