Skip to content

Commit b2e5b15

Browse files
committed
Working on fix for many to one relation loading
1 parent 81ace1d commit b2e5b15

File tree

24 files changed

+294
-52
lines changed

24 files changed

+294
-52
lines changed

.github/workflows/gradle.yml

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ on:
99
branches:
1010
- master
1111
- '[1-9]+.[0-9]+.x'
12+
- 'fix-manytoone-join'
1213
pull_request:
1314
branches:
1415
- master

data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/many2one/MultiManyToOneJoinSpec.groovy

+38
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ import io.micronaut.data.model.Page
1212
import io.micronaut.data.model.Pageable
1313
import io.micronaut.data.model.query.builder.sql.Dialect
1414
import io.micronaut.data.repository.CrudRepository
15+
import io.micronaut.data.tck.entities.Document
16+
import io.micronaut.data.tck.entities.DocumentType
17+
import io.micronaut.data.tck.repositories.DocumentRepository
18+
import io.micronaut.data.tck.repositories.DocumentTypeRepository
1519
import io.micronaut.test.extensions.spock.annotation.MicronautTest
1620
import spock.lang.AutoCleanup
1721
import spock.lang.Shared
@@ -50,6 +54,14 @@ class MultiManyToOneJoinSpec extends Specification implements H2TestPropertyProv
5054
@Inject
5155
MyOtherRepository myOtherRepository
5256

57+
@Shared
58+
@Inject
59+
DocumentTypeRepository documentTypeRepository
60+
61+
@Shared
62+
@Inject
63+
DocumentRepository documentRepository
64+
5365
void 'test many-to-one hierarchy'() {
5466
given:
5567
RefA refA = new RefA(refB: new RefB(refC: new RefC(name: "TestXyz")))
@@ -143,6 +155,25 @@ class MultiManyToOneJoinSpec extends Specification implements H2TestPropertyProv
143155
optFound.get().other
144156
optFound.get().other.lid == myOther.lid
145157
}
158+
159+
void "test many to one join nullable"() {
160+
when:
161+
def documentType = documentTypeRepository.save(new DocumentType(null, "PDF", false));
162+
def document = documentRepository.save(new Document(null, "Opinion.pdf", documentType))
163+
then:
164+
def optionalDocument = documentRepository.findById(document.id())
165+
optionalDocument.present
166+
optionalDocument.get().type()
167+
when:
168+
documentTypeRepository.updateDeletedById(documentType.id(), true)
169+
optionalDocument = documentRepository.findById(document.id())
170+
then:
171+
optionalDocument.present
172+
!optionalDocument.get().type()
173+
cleanup:
174+
documentRepository.deleteAll()
175+
documentTypeRepository.deleteAll()
176+
}
146177
}
147178

148179
@JdbcRepository(dialect = Dialect.H2)
@@ -291,9 +322,12 @@ class User {
291322
@JdbcRepository(dialect = Dialect.H2)
292323
interface UserGroupMembershipRepository extends CrudRepository<UserGroupMembership, Long> {
293324

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

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

@@ -327,6 +361,10 @@ class MyOther {
327361
}
328362
@JdbcRepository(dialect = H2)
329363
interface MyEntityRepository extends CrudRepository<MyEntity, Long> {
364+
365+
@Override
366+
@Join(value = "other", type = Join.Type.LEFT_FETCH)
367+
Optional<MyEntity> findById(Long aLong);
330368
}
331369
@JdbcRepository(dialect = H2)
332370
interface MyOtherRepository extends CrudRepository<MyOther, String> {

data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/one2many/OneToManyHierarchicalSpec.groovy

+1-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ class OneToManyHierarchicalSpec extends Specification implements H2TestPropertyP
5353
loadedTestEntity.children.size() == 1
5454
optTestHierarchyEntity.present
5555
def loadedTestHierarchyEntity = optTestHierarchyEntity.get()
56-
loadedTestHierarchyEntity.parent
56+
!loadedTestHierarchyEntity.parent
5757
!loadedTestHierarchyEntity.child
5858
when:
5959
def testEntities = testEntityRepository.findAll(Specifications.getChildrenByParentCodeSpecification("code2"))

data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/remap/Course.java

+2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.micronaut.data.jdbc.h2.remap;
22

3+
import io.micronaut.core.annotation.Nullable;
34
import io.micronaut.data.annotation.Id;
45
import io.micronaut.data.annotation.MappedEntity;
56
import io.micronaut.data.annotation.Relation;
@@ -19,6 +20,7 @@ record Course(
1920
String name,
2021

2122
@Relation(value = MANY_TO_MANY, mappedBy = "courses", cascade = ALL)
23+
@Nullable
2224
List<Student> students
2325
) {
2426
}

data-jdbc/src/test/groovy/io/micronaut/data/jdbc/h2/remap/Student.java

+2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.micronaut.data.jdbc.h2.remap;
22

3+
import io.micronaut.core.annotation.Nullable;
34
import io.micronaut.data.annotation.Id;
45
import io.micronaut.data.annotation.MappedEntity;
56
import io.micronaut.data.annotation.MappedProperty;
@@ -21,6 +22,7 @@ record Student(
2122
String name,
2223

2324
@Relation(value = MANY_TO_MANY, cascade = ALL)
25+
@Nullable
2426
List<Course> courses
2527
) {
2628
}
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
package io.micronaut.data.jdbc.h2.remap;
22

3+
import io.micronaut.data.annotation.Join;
34
import io.micronaut.data.jdbc.annotation.JdbcRepository;
45
import io.micronaut.data.model.query.builder.sql.Dialect;
56
import io.micronaut.data.repository.CrudRepository;
67

8+
import java.util.Optional;
9+
710
@JdbcRepository(dialect = Dialect.H2)
811
interface StudentRepository extends CrudRepository<Student, StudentId> {
12+
13+
@Join(value = "courses", type = Join.Type.LEFT_FETCH)
14+
Optional<Student> findById(StudentId id);
915
}

data-jdbc/src/test/groovy/io/micronaut/data/jdbc/postgres/PostgresRepositorySpec.groovy

+4-2
Original file line numberDiff line numberDiff line change
@@ -449,7 +449,8 @@ class PostgresRepositorySpec extends AbstractRepositorySpec implements PostgresT
449449
when:
450450
def b = bookRepository.modifyReturning(petCemetery.author.id)
451451
then:
452-
b.author.id == petCemetery.author.id
452+
// Not loaded because it is many to one
453+
!b.author
453454
b.postLoad == 1
454455
when:
455456
def allBooks = bookRepository.findAll()
@@ -486,7 +487,8 @@ class PostgresRepositorySpec extends AbstractRepositorySpec implements PostgresT
486487
when:
487488
def b = bookRepository.customUpdateReturningBook(petCemetery.author.id)
488489
then:
489-
b.author.id == petCemetery.author.id
490+
// null because many to one mapping is not loaded in updateReturning
491+
!b.author
490492
b.postLoad == 1
491493
when:
492494
def allBooks = bookRepository.findAll()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package io.micronaut.data.jdbc.h2;
2+
3+
import io.micronaut.data.jdbc.annotation.JdbcRepository;
4+
import io.micronaut.data.model.query.builder.sql.Dialect;
5+
import io.micronaut.data.tck.repositories.DocumentRepository;
6+
7+
@JdbcRepository(dialect = Dialect.H2)
8+
public interface H2DocumentRepository extends DocumentRepository {
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package io.micronaut.data.jdbc.h2;
2+
3+
import io.micronaut.data.jdbc.annotation.JdbcRepository;
4+
import io.micronaut.data.model.query.builder.sql.Dialect;
5+
import io.micronaut.data.tck.repositories.DocumentTypeRepository;
6+
7+
@JdbcRepository(dialect = Dialect.H2)
8+
public interface H2DocumentTypeRepository extends DocumentTypeRepository {
9+
}

data-jdbc/src/test/java/io/micronaut/data/jdbc/postgres/PostgresBookRepository.java

+5
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import io.micronaut.core.annotation.Nullable;
1919
import io.micronaut.data.annotation.Expandable;
2020
import io.micronaut.data.annotation.Id;
21+
import io.micronaut.data.annotation.Join;
2122
import io.micronaut.data.annotation.Query;
2223
import io.micronaut.data.annotation.TypeDef;
2324
import io.micronaut.data.annotation.sql.Procedure;
@@ -129,4 +130,8 @@ public abstract Book customInsertReturningBook(Long authorId,
129130
DELETE FROM "book" WHERE "id" = :id RETURNING *
130131
""")
131132
public abstract Book customDeleteOne(Long id);
133+
134+
@Override
135+
@Join(value = "author", type = Join.Type.LEFT_FETCH)
136+
public abstract List<Book> findAll();
132137
}

data-model/src/main/java/io/micronaut/data/model/Association.java

+12
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,18 @@ default boolean isForeignKey() {
123123
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());
124124
}
125125

126+
/**
127+
* Determines whether this association is single-ended, meaning it only has one end point.
128+
* An association is considered single-ended if its kind is either ONE_TO_ONE or MANY_TO_ONE,
129+
* but not EMBEDDED.
130+
*
131+
* @return True if the association is single-ended, false otherwise.
132+
*/
133+
default boolean isSingleEnded() {
134+
Relation.Kind kind = getKind();
135+
return kind.isSingleEnded() && !kind.equals(Relation.Kind.EMBEDDED);
136+
}
137+
126138
/**
127139
* Whether this association cascades the given types.
128140
* @param types The types

data-model/src/main/java/io/micronaut/data/model/PersistentEntityUtils.java

+28-4
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import io.micronaut.core.annotation.AnnotationMetadata;
1919
import io.micronaut.core.annotation.AnnotationValue;
2020
import io.micronaut.core.annotation.Internal;
21+
import io.micronaut.core.annotation.Nullable;
2122
import io.micronaut.core.naming.NameUtils;
2223
import io.micronaut.data.annotation.sql.JoinColumn;
2324
import io.micronaut.data.annotation.sql.JoinColumns;
@@ -29,6 +30,7 @@
2930
import java.util.Optional;
3031
import java.util.function.BiConsumer;
3132
import java.util.function.Consumer;
33+
import java.util.function.Predicate;
3234

3335
/**
3436
* Persistent entity utils.
@@ -81,14 +83,18 @@ public static void traversePersistentProperties(PersistentProperty property, BiC
8183
* @param consumer The function to invoke on every property
8284
*/
8385
public static void traversePersistentProperties(PersistentEntity persistentEntity, BiConsumer<List<Association>, PersistentProperty> consumer) {
86+
traversePersistentProperties(persistentEntity, null, consumer);
87+
}
88+
89+
public static void traversePersistentProperties(PersistentEntity persistentEntity, @Nullable Predicate<Association> skipAssociationPredicate, BiConsumer<List<Association>, PersistentProperty> consumer) {
8490
for (PersistentProperty identityProperty : persistentEntity.getIdentityProperties()) {
85-
traversePersistentProperties(Collections.emptyList(), identityProperty, consumer);
91+
traversePersistentProperties(Collections.emptyList(), identityProperty, skipAssociationPredicate, consumer);
8692
}
8793
if (persistentEntity.getVersion() != null) {
88-
traversePersistentProperties(Collections.emptyList(), persistentEntity.getVersion(), consumer);
94+
traversePersistentProperties(Collections.emptyList(), persistentEntity.getVersion(), skipAssociationPredicate, consumer);
8995
}
9096
for (PersistentProperty property : persistentEntity.getPersistentProperties()) {
91-
traversePersistentProperties(Collections.emptyList(), property, consumer);
97+
traversePersistentProperties(Collections.emptyList(), property, skipAssociationPredicate, consumer);
9298
}
9399
}
94100

@@ -141,7 +147,14 @@ public static int countPersistentProperties(List<Association> associations,
141147
public static void traversePersistentProperties(List<Association> associations,
142148
PersistentProperty property,
143149
BiConsumer<List<Association>, PersistentProperty> consumerProperty) {
144-
traversePersistentProperties(associations, property, true, consumerProperty);
150+
traversePersistentProperties(associations, property, null, consumerProperty);
151+
}
152+
153+
public static void traversePersistentProperties(List<Association> associations,
154+
PersistentProperty property,
155+
@Nullable Predicate<Association> skipAssociationPredicate,
156+
BiConsumer<List<Association>, PersistentProperty> consumerProperty) {
157+
traversePersistentProperties(associations, property, true, skipAssociationPredicate, consumerProperty);
145158
}
146159

147160
public static void traversePersistentProperties(PersistentPropertyPath propertyPath,
@@ -165,6 +178,14 @@ public static void traversePersistentProperties(List<Association> associations,
165178
PersistentProperty property,
166179
boolean traverseEmbedded,
167180
BiConsumer<List<Association>, PersistentProperty> consumerProperty) {
181+
traversePersistentProperties(associations, property, traverseEmbedded, null, consumerProperty);
182+
}
183+
184+
public static void traversePersistentProperties(List<Association> associations,
185+
PersistentProperty property,
186+
boolean traverseEmbedded,
187+
@Nullable Predicate<Association> skipAssociationPredicate,
188+
BiConsumer<List<Association>, PersistentProperty> consumerProperty) {
168189
if (property instanceof Embedded embedded) {
169190
if (traverseEmbedded) {
170191
PersistentEntity embeddedEntity = embedded.getAssociatedEntity();
@@ -181,6 +202,9 @@ public static void traversePersistentProperties(List<Association> associations,
181202
if (association.isForeignKey()) {
182203
return;
183204
}
205+
if (skipAssociationPredicate != null && skipAssociationPredicate.test(association)) {
206+
return;
207+
}
184208
List<Association> newAssociations = new ArrayList<>(associations);
185209
newAssociations.add((Association) property);
186210
PersistentEntity associatedEntity = association.getAssociatedEntity();

data-model/src/main/java/io/micronaut/data/model/query/builder/sql/SqlQueryBuilder2.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -1666,7 +1666,7 @@ protected void selectAllColumnsFromJoinPaths(Collection<JoinPath> allPaths,
16661666

16671667
query.append(COMMA);
16681668

1669-
boolean includeIdentity = association.isForeignKey();
1669+
boolean includeIdentity = association.isForeignKey() || association.isSingleEnded();
16701670
// in the case of a foreign key association the ID is not in the table,
16711671
// so we need to retrieve it
16721672
PersistentEntityUtils.traversePersistentProperties(associatedEntity, includeIdentity, true, (propertyAssociations, prop) -> {
@@ -1710,7 +1710,7 @@ public void selectAllColumns(AnnotationMetadata annotationMetadata, PersistentEn
17101710
boolean escape = shouldEscape(entity);
17111711
NamingStrategy namingStrategy = getNamingStrategy(entity);
17121712
int length = query.length();
1713-
PersistentEntityUtils.traversePersistentProperties(entity, (associations, property)
1713+
PersistentEntityUtils.traversePersistentProperties(entity, Association::isSingleEnded, (associations, property)
17141714
-> appendProperty(query, associations, property, namingStrategy, alias, escape));
17151715
int newLength = query.length();
17161716
if (newLength == length) {

data-model/src/main/java/io/micronaut/data/model/runtime/RuntimeAssociation.java

+7
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ public class RuntimeAssociation<T> extends RuntimePersistentProperty<T> implemen
3636
private final Relation.Kind kind;
3737
private final String aliasName;
3838
private final boolean isForeignKey;
39+
private final boolean isSingleEnded;
3940

4041
/**
4142
* Default constructor.
@@ -48,13 +49,19 @@ public class RuntimeAssociation<T> extends RuntimePersistentProperty<T> implemen
4849
this.kind = Association.super.getKind();
4950
this.aliasName = Association.super.getAliasName();
5051
this.isForeignKey = Association.super.isForeignKey();
52+
this.isSingleEnded = Association.super.isSingleEnded();
5153
}
5254

5355
@Override
5456
public boolean isForeignKey() {
5557
return isForeignKey;
5658
}
5759

60+
@Override
61+
public boolean isSingleEnded() {
62+
return isSingleEnded;
63+
}
64+
5865
@Override
5966
public String getAliasName() {
6067
return aliasName;

0 commit comments

Comments
 (0)