Skip to content

Commit

Permalink
Correct OneToMany relationship for immutable data classes (#1072)
Browse files Browse the repository at this point in the history
Fixes #1054
  • Loading branch information
dstepanov authored Jul 4, 2021
1 parent d8b5277 commit 7ea4c1f
Show file tree
Hide file tree
Showing 7 changed files with 252 additions and 24 deletions.
4 changes: 2 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ subprojects { Project subproject ->
if (JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_15)) {
jvmArgs "--enable-preview"
}
timeout = Duration.ofMinutes(20)
timeout = Duration.ofHours(1)
}

if (subproject.name != 'data-bom') {
Expand All @@ -80,7 +80,7 @@ subprojects { Project subproject ->
if (JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_15)) {
jvmArgs "--enable-preview"
}
timeout = Duration.ofMinutes(20)
timeout = Duration.ofHours(1)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package io.micronaut.data.jdbc.h2.one2many

import io.micronaut.context.ApplicationContext
import io.micronaut.data.annotation.*
import io.micronaut.data.jdbc.annotation.JdbcRepository
import io.micronaut.data.jdbc.h2.H2DBProperties
import io.micronaut.data.jdbc.h2.H2TestPropertyProvider
import io.micronaut.data.model.query.builder.sql.Dialect
import io.micronaut.data.repository.CrudRepository
import io.micronaut.test.extensions.spock.annotation.MicronautTest
import spock.lang.AutoCleanup
import spock.lang.Shared
import spock.lang.Specification

import javax.inject.Inject

@MicronautTest
@H2DBProperties
class OneToManyChildrenSpec extends Specification implements H2TestPropertyProvider {
@AutoCleanup
@Shared
ApplicationContext applicationContext = ApplicationContext.run(getProperties())

@Shared
@Inject
ParentRepository parentRepository = applicationContext.getBean(ParentRepository)

void 'test one-to-many hierarchy'() {
given:
def children = []
Parent parent = new Parent(name: "parent", children: children)
children.add new Child(name: "A", parent: parent)
children.add new Child(name: "B", parent: parent)
children.add new Child(name: "C", parent: parent)
when:
parentRepository.save(parent)
then:
parent.id
parent.children.size() == 3
parent.children.forEach {
verifyAll(it) {
it.id
it.parent
it.name
}
}
when:
parent = parentRepository.findById(parent.id).get()
then:
parent.id
parent.children.size() == 3
parent.children.forEach {
verifyAll(it) {
it.id
it.parent
it.name
}
}
}

}

@JdbcRepository(dialect = Dialect.H2)
interface ParentRepository extends CrudRepository<Parent, Long> {

@Join(value = "children", type = Join.Type.FETCH)
@Override
Optional<Parent> findById(Long id);
}

@MappedEntity("x_product")
class Parent {
String name
@Relation(value = Relation.Kind.ONE_TO_MANY, mappedBy = "parent", cascade = Relation.Cascade.ALL)
List<Child> children
@Id
@GeneratedValue
Long id
}

@MappedEntity("x_child")
class Child {
String name
@Relation(value = Relation.Kind.MANY_TO_ONE)
Parent parent
@Id
@GeneratedValue
Long id
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
* using column naming conventions mapped by the entity.
*
* @param <RS> The result set type
* @param <R> The result type
* @param <R> The result type
*/
@Internal
public final class SqlResultEntityTypeMapper<RS, R> implements SqlTypeMapper<RS, R> {
Expand All @@ -81,10 +81,11 @@ public final class SqlResultEntityTypeMapper<RS, R> implements SqlTypeMapper<RS,

/**
* Default constructor.
* @param prefix The prefix to startup from.
* @param entity The entity
*
* @param prefix The prefix to startup from.
* @param entity The entity
* @param resultReader The result reader
* @param jsonCodec The JSON codec
* @param jsonCodec The JSON codec
*/
public SqlResultEntityTypeMapper(
String prefix,
Expand All @@ -96,10 +97,11 @@ public SqlResultEntityTypeMapper(

/**
* Constructor used to customize the join paths.
* @param entity The entity
*
* @param entity The entity
* @param resultReader The result reader
* @param joinPaths The join paths
* @param jsonCodec The JSON codec
* @param joinPaths The join paths
* @param jsonCodec The JSON codec
*/
public SqlResultEntityTypeMapper(
@NonNull RuntimePersistentEntity<R> entity,
Expand All @@ -111,10 +113,11 @@ public SqlResultEntityTypeMapper(

/**
* Constructor used to customize the join paths.
* @param entity The entity
*
* @param entity The entity
* @param resultReader The result reader
* @param joinPaths The join paths
* @param jsonCodec The JSON codec
* @param joinPaths The join paths
* @param jsonCodec The JSON codec
* @param loadListener The event listener
*/
public SqlResultEntityTypeMapper(
Expand All @@ -128,9 +131,10 @@ public SqlResultEntityTypeMapper(

/**
* Constructor used to customize the join paths.
* @param entity The entity
*
* @param entity The entity
* @param resultReader The result reader
* @param joinPaths The join paths
* @param joinPaths The join paths
*/
private SqlResultEntityTypeMapper(
@NonNull RuntimePersistentEntity<R> entity,
Expand Down Expand Up @@ -159,14 +163,16 @@ private SqlResultEntityTypeMapper(
/**
* @return The entity to be materialized
*/
public @NonNull RuntimePersistentEntity<R> getEntity() {
public @NonNull
RuntimePersistentEntity<R> getEntity() {
return entity;
}

/**
* @return The result reader instance.
*/
public @NonNull ResultReader<RS, String> getResultReader() {
public @NonNull
ResultReader<RS, String> getResultReader() {
return resultReader;
}

Expand Down Expand Up @@ -250,7 +256,7 @@ public R getResult() {
return null;
}
if (!joinPaths.isEmpty()) {
entityInstance = (R) setChildrenAndTriggerPostLoad(entityInstance, ctx);
entityInstance = (R) setChildrenAndTriggerPostLoad(entityInstance, ctx, null);
} else {
return triggerPostLoad(entity, entityInstance);
}
Expand Down Expand Up @@ -290,7 +296,7 @@ public List<R> getResult() {
List<R> values = new ArrayList<>(processed.size());
for (Map.Entry<Object, MappingContext<R>> e : processed.entrySet()) {
MappingContext<R> ctx = e.getValue();
R entityInstance = (R) setChildrenAndTriggerPostLoad(ctx.entity, ctx);
R entityInstance = (R) setChildrenAndTriggerPostLoad(ctx.entity, ctx, null);
values.add(entityInstance);
}
return values;
Expand Down Expand Up @@ -321,11 +327,11 @@ private void readChildren(RS rs, Object instance, Object parent, MappingContext<
}
}

private Object setChildrenAndTriggerPostLoad(Object instance, MappingContext<?> ctx) {
private Object setChildrenAndTriggerPostLoad(Object instance, MappingContext<?> ctx, Object parent) {
if (ctx.manyAssociations != null) {
List<Object> values = new ArrayList<>(ctx.manyAssociations.size());
for (MappingContext associationCtx : ctx.manyAssociations.values()) {
values.add(setChildrenAndTriggerPostLoad(associationCtx.entity, associationCtx));
values.add(setChildrenAndTriggerPostLoad(associationCtx.entity, associationCtx, parent));
}
return values;
} else if (ctx.associations != null) {
Expand All @@ -335,18 +341,28 @@ private Object setChildrenAndTriggerPostLoad(Object instance, MappingContext<?>
BeanProperty beanProperty = runtimeAssociation.getProperty();
if (runtimeAssociation.getKind().isSingleEnded() && (associationCtx.manyAssociations == null || associationCtx.manyAssociations.isEmpty())) {
Object value = beanProperty.get(instance);
Object newValue = setChildrenAndTriggerPostLoad(value, associationCtx);
Object newValue = setChildrenAndTriggerPostLoad(value, associationCtx, instance);
if (newValue != value) {
instance = setProperty(beanProperty, instance, newValue);
}
} else {
Object newValue = setChildrenAndTriggerPostLoad(null, associationCtx);
Object newValue = setChildrenAndTriggerPostLoad(null, associationCtx, instance);
newValue = resultReader.convertRequired(newValue == null ? new ArrayList<>() : newValue, beanProperty.getType());
instance = setProperty(beanProperty, instance, newValue);
}
}
}
if (instance != null && (ctx.association == null || ctx.jp != null)) {
if (parent != null && ctx.association != null && ctx.association.isBidirectional()) {
RuntimeAssociation inverseAssociation = (RuntimeAssociation) ctx.association.getInverseSide().get();
if (inverseAssociation.getKind().isSingleEnded()) {
BeanProperty inverseProperty = inverseAssociation.getProperty();
Object inverseInstance = inverseProperty.get(instance);
if (inverseInstance != parent) {
instance = setProperty(inverseProperty, instance, parent);
}
}
}
triggerPostLoad(ctx.persistentEntity, instance);
}
return instance;
Expand Down Expand Up @@ -565,7 +581,8 @@ private <K> K triggerPostLoad(RuntimePersistentEntity<?> persistentEntity, K ent
return finalEntity;
}

private @Nullable <K> Object readEntityId(RS rs, MappingContext<K> ctx) {
private @Nullable
<K> Object readEntityId(RS rs, MappingContext<K> ctx) {
RuntimePersistentProperty<K> identity = ctx.persistentEntity.getIdentity();
if (identity == null) {
return null;
Expand Down Expand Up @@ -833,7 +850,7 @@ private static List<Association> associated(List<Association> associations, Asso
* The pushing mapper helper interface.
*
* @param <RS> The row type
* @param <R> The result type
* @param <R> The result type
*/
public interface PushingMapper<RS, R> {

Expand Down
46 changes: 46 additions & 0 deletions doc-examples/jdbc-example-kotlin/src/main/kotlin/example/Child.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package example

import io.micronaut.data.annotation.GeneratedValue
import io.micronaut.data.annotation.Id
import io.micronaut.data.annotation.MappedEntity
import io.micronaut.data.annotation.Relation

@MappedEntity
data class Child(

val name: String,

@Relation(Relation.Kind.MANY_TO_ONE)
val parent: Parent? = null,

@field:Id @GeneratedValue val id: Int? = null



) {

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Child) return false

if (name != other.name) return false
if (parent?.id != other.parent?.id) return false
if (id != other.id) return false

return true
}

override fun hashCode(): Int {
var result = name.hashCode()
result = 31 * result + parent?.id.hashCode()
result = 31 * result + (id ?: 0)
return result
}

override fun toString(): String {
return "Child(name='$name', parent=${parent?.id}, id=$id)"
}


}

19 changes: 19 additions & 0 deletions doc-examples/jdbc-example-kotlin/src/main/kotlin/example/Parent.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package example

import io.micronaut.data.annotation.GeneratedValue
import io.micronaut.data.annotation.Id
import io.micronaut.data.annotation.MappedEntity
import io.micronaut.data.annotation.Relation

@MappedEntity
data class Parent(

val name: String,

@Relation(value = Relation.Kind.ONE_TO_MANY, mappedBy = "parent", cascade = [Relation.Cascade.ALL])
val children: List<Child>,

@field:Id @GeneratedValue
val id: Int? = null

)
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package example

import example.Parent
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)
abstract class ParentRepository : CrudRepository<Parent, Int> {

@Join(value = "children", type = Join.Type.FETCH)
abstract override fun findById(id: Int): Optional<Parent>

}
Loading

0 comments on commit 7ea4c1f

Please sign in to comment.