diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/ReactiveActionQueue.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/ReactiveActionQueue.java index 922af6e93..2336eb625 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/ReactiveActionQueue.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/ReactiveActionQueue.java @@ -34,7 +34,6 @@ import org.hibernate.action.spi.BeforeTransactionCompletionProcess; import org.hibernate.action.spi.Executable; import org.hibernate.cache.CacheException; -import org.hibernate.engine.internal.NonNullableTransientDependencies; import org.hibernate.engine.spi.ActionQueue; import org.hibernate.engine.spi.EntityEntry; import org.hibernate.engine.spi.ExecutableList; @@ -343,24 +342,25 @@ private CompletionStage addInsertAction(ReactiveEntityInsertAction insert) ret = ret.thenCompose( v -> executeInserts() ); } - NonNullableTransientDependencies nonNullableTransientDependencies = insert.findNonNullableTransientEntities(); - if ( nonNullableTransientDependencies == null ) { - LOG.tracev( "Adding insert with no non-nullable, transient entities: [{0}]", insert ); - ret = ret.thenCompose( v -> addResolvedEntityInsertAction( insert ) ); - } - else { - if ( LOG.isTraceEnabled() ) { - LOG.tracev( "Adding insert with non-nullable, transient entities; insert=[{0}], dependencies=[{1}]", - insert, - nonNullableTransientDependencies.toLoggableString( insert.getSession() ) - ); - } - if ( unresolvedInsertions == null ) { - unresolvedInsertions = new UnresolvedEntityInsertActions(); - } - unresolvedInsertions.addUnresolvedEntityInsertAction( (AbstractEntityInsertAction) insert, nonNullableTransientDependencies ); - } - return ret; + return ret + .thenCompose( v -> insert.reactiveFindNonNullableTransientEntities() ) + .thenCompose( nonNullables -> { + if ( nonNullables == null ) { + LOG.tracev( "Adding insert with no non-nullable, transient entities: [{0}]", insert ); + return addResolvedEntityInsertAction( insert ); + } + else { + if ( LOG.isTraceEnabled() ) { + LOG.tracev( "Adding insert with non-nullable, transient entities; insert=[{0}], dependencies=[{1}]", insert, nonNullables.toLoggableString( insert.getSession() ) ); + } + if ( unresolvedInsertions == null ) { + unresolvedInsertions = new UnresolvedEntityInsertActions(); + } + unresolvedInsertions + .addUnresolvedEntityInsertAction( (AbstractEntityInsertAction) insert, nonNullables ); + return voidFuture(); + } + } ); } private CompletionStage addResolvedEntityInsertAction(ReactiveEntityInsertAction insert) { diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/impl/ForeignKeys.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/impl/ForeignKeys.java index 99d953c62..a9c99e7be 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/impl/ForeignKeys.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/impl/ForeignKeys.java @@ -9,9 +9,11 @@ import org.hibernate.HibernateException; import org.hibernate.TransientObjectException; import org.hibernate.bytecode.enhance.spi.LazyPropertyInitializer; +import org.hibernate.engine.internal.NonNullableTransientDependencies; import org.hibernate.engine.spi.EntityEntry; import org.hibernate.engine.spi.SelfDirtinessTracker; import org.hibernate.engine.spi.SessionImplementor; +import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.internal.util.StringHelper; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.proxy.HibernateProxy; @@ -359,6 +361,92 @@ public static CompletionStage getEntityIdentifierIfNotUnsaved( } } + public static CompletionStage findNonNullableTransientEntities( + String entityName, + Object entity, + Object[] values, + boolean isEarlyInsert, + SharedSessionContractImplementor session) { + + final EntityPersister persister = session.getEntityPersister( entityName, entity ); + final Type[] types = persister.getPropertyTypes(); + final Nullifier nullifier = new Nullifier( entity, false, isEarlyInsert, (SessionImplementor) session, persister ); + final String[] propertyNames = persister.getPropertyNames(); + final boolean[] nullability = persister.getPropertyNullability(); + final NonNullableTransientDependencies nonNullableTransientEntities = new NonNullableTransientDependencies(); + + return loop( 0, types.length, + i -> collectNonNullableTransientEntities( + nullifier, + values[i], + propertyNames[i], + types[i], + nullability[i], + session, + nonNullableTransientEntities + ) + ).thenApply( r -> nonNullableTransientEntities.isEmpty() ? null : nonNullableTransientEntities ); + } + + private static CompletionStage collectNonNullableTransientEntities( + Nullifier nullifier, + Object value, + String propertyName, + Type type, + boolean isNullable, + SharedSessionContractImplementor session, + NonNullableTransientDependencies nonNullableTransientEntities) { + + if ( value == null ) { + return voidFuture(); + } + + if ( type.isEntityType() ) { + final EntityType entityType = (EntityType) type; + if ( !isNullable && !entityType.isOneToOne() ) { + return nullifier + .isNullifiable( entityType.getAssociatedEntityName(), value ) + .thenAccept( isNullifiable -> { + if ( isNullifiable ) { + nonNullableTransientEntities.add( propertyName, value ); + } + } ); + } + } + else if ( type.isAnyType() ) { + if ( !isNullable ) { + return nullifier + .isNullifiable( null, value ) + .thenAccept( isNullifiable -> { + if ( isNullifiable ) { + nonNullableTransientEntities.add( propertyName, value ); + } + } ); + }; + } + else if ( type.isComponentType() ) { + final CompositeType actype = (CompositeType) type; + final boolean[] subValueNullability = actype.getPropertyNullability(); + if ( subValueNullability != null ) { + final String[] subPropertyNames = actype.getPropertyNames(); + final Object[] subvalues = actype.getPropertyValues( value, session ); + final Type[] subtypes = actype.getSubtypes(); + return loop( 0, subtypes.length, + i -> collectNonNullableTransientEntities( + nullifier, + subvalues[i], + subPropertyNames[i], + subtypes[i], + subValueNullability[i], + session, + nonNullableTransientEntities + ) + ); + } + } + + return voidFuture(); + } /** * Disallow instantiation diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/impl/ReactiveEntityInsertAction.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/impl/ReactiveEntityInsertAction.java index a1cf3c431..f649a17f0 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/impl/ReactiveEntityInsertAction.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/impl/ReactiveEntityInsertAction.java @@ -87,4 +87,8 @@ default CompletionStage reactiveMakeEntityManaged() { isVersionIncrementDisabled() )); } + + default CompletionStage reactiveFindNonNullableTransientEntities() { + return ForeignKeys.findNonNullableTransientEntities( getPersister().getEntityName(), getInstance(), getState(), isEarlyInsert(), getSession() ); + } } diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/NonNullableManyToOneTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/NonNullableManyToOneTest.java new file mode 100644 index 000000000..36e5db03a --- /dev/null +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/NonNullableManyToOneTest.java @@ -0,0 +1,153 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive; + +import java.util.ArrayList; +import java.util.List; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.OneToMany; +import javax.persistence.Table; + +import org.hibernate.cfg.Configuration; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import io.vertx.ext.unit.TestContext; + +public class NonNullableManyToOneTest extends BaseReactiveTest { + + @Override + protected Configuration constructConfiguration() { + Configuration configuration = super.constructConfiguration(); + configuration.addAnnotatedClass( Dealer.class ); + configuration.addAnnotatedClass( Artist.class ); + configuration.addAnnotatedClass( Painting.class ); + return configuration; + } + + @Before + public void populateDB(TestContext context) { + Artist artist = new Artist( "Grand Master Painter" ); + artist.id = 1L; + Dealer dealer = new Dealer( "Dealer" ); + dealer.id = 1L; + Painting painting = new Painting( "Mona Lisa"); + painting.id = 2L; + artist.addPainting( painting ); + dealer.addPainting( painting ); + + test( context, getMutinySessionFactory() + .withTransaction( s -> s.persistAll( painting, artist, dealer ) ) ); + } + + @After + public void cleanDB(TestContext context) { + test( context, deleteEntities( "Painting", "Artist" ) ); + } + + @Test + public void testNonNullableSuccess(TestContext context) { + test( + context, + getMutinySessionFactory().withTransaction( session -> session + .createQuery( "from Artist", Artist.class ) + .getSingleResult().chain( a -> session.fetch( a.paintings ) ) + .invoke( paintings -> { + context.assertNotNull( paintings ); + context.assertEquals( 1, paintings.size() ); + context.assertEquals( "Mona Lisa", paintings.get( 0 ).name ); + } ) ) + .chain( () -> getMutinySessionFactory().withTransaction( s1 -> s1 + .createQuery( "from Dealer", Dealer.class ) + .getSingleResult().chain( d -> s1.fetch( d.paintings ) ) + .invoke( paintings -> { + context.assertNotNull( paintings ); + context.assertEquals( 1, paintings.size() ); + context.assertEquals( "Mona Lisa", paintings.get( 0 ).name ); + } ) + ) + ) + ); + } + + @Entity(name = "Painting") + @Table(name = "painting") + public static class Painting { + @Id + Long id; + String name; + + @JoinColumn(nullable = false) + @ManyToOne(optional = true) + Artist author; + + @JoinColumn(nullable = true) + @ManyToOne(optional = false) + Dealer dealer; + + public Painting() { + } + + public Painting(String name) { + this.name = name; + } + } + + @Entity(name = "Artist") + @Table(name = "artist") + public static class Artist { + + @Id + Long id; + String name; + + @OneToMany(mappedBy = "author") + List paintings = new ArrayList<>(); + + public Artist() { + } + + public Artist(String name) { + this.name = name; + } + + public void addPainting(Painting painting) { + this.paintings.add( painting ); + painting.author = this; + } + + } + + @Entity(name = "Dealer") + @Table(name = "dealer") + public static class Dealer { + + @Id + Long id; + String name; + + @OneToMany(mappedBy = "dealer") + List paintings = new ArrayList<>(); + + public Dealer() { + } + + public Dealer(String name) { + this.name = name; + } + + public void addPainting(Painting painting) { + this.paintings.add( painting ); + painting.dealer = this; + } + + } +}