diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/metamodel/mapping/internal/ReactiveEmbeddedIdentifierMappingImpl.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/metamodel/mapping/internal/ReactiveEmbeddedIdentifierMappingImpl.java new file mode 100644 index 000000000..3f5244fa9 --- /dev/null +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/metamodel/mapping/internal/ReactiveEmbeddedIdentifierMappingImpl.java @@ -0,0 +1,107 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.metamodel.mapping.internal; + +import java.util.function.BiConsumer; + +import org.hibernate.engine.FetchTiming; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.metamodel.internal.AbstractCompositeIdentifierMapping; +import org.hibernate.metamodel.mapping.EmbeddableMappingType; +import org.hibernate.metamodel.mapping.JdbcMapping; +import org.hibernate.metamodel.mapping.internal.EmbeddedIdentifierMappingImpl; +import org.hibernate.reactive.sql.results.graph.embeddable.internal.ReactiveEmbeddableFetchImpl; +import org.hibernate.spi.NavigablePath; +import org.hibernate.sql.ast.spi.SqlSelection; +import org.hibernate.sql.ast.tree.from.TableGroup; +import org.hibernate.sql.results.graph.DomainResultCreationState; +import org.hibernate.sql.results.graph.Fetch; +import org.hibernate.sql.results.graph.FetchParent; + +public class ReactiveEmbeddedIdentifierMappingImpl extends AbstractCompositeIdentifierMapping { + + private final EmbeddedIdentifierMappingImpl delegate; + + public ReactiveEmbeddedIdentifierMappingImpl(EmbeddedIdentifierMappingImpl delegate) { + super( delegate ); + this.delegate = delegate; + } + + @Override + public Fetch generateFetch( + FetchParent fetchParent, + NavigablePath fetchablePath, + FetchTiming fetchTiming, + boolean selected, + String resultVariable, + DomainResultCreationState creationState) { + return new ReactiveEmbeddableFetchImpl( + fetchablePath, + this, + fetchParent, + fetchTiming, + selected, + creationState + ); + } + + @Override + public EmbeddableMappingType getPartMappingType() { + return delegate.getPartMappingType(); + } + + @Override + public void applySqlSelections( + NavigablePath navigablePath, + TableGroup tableGroup, + DomainResultCreationState creationState) { + delegate.applySqlSelections( navigablePath, tableGroup, creationState ); + } + + @Override + public void applySqlSelections( + NavigablePath navigablePath, + TableGroup tableGroup, + DomainResultCreationState creationState, + BiConsumer selectionConsumer) { + delegate.applySqlSelections( navigablePath, tableGroup, creationState, selectionConsumer ); + } + + @Override + public EmbeddableMappingType getMappedIdEmbeddableTypeDescriptor() { + return delegate.getMappedIdEmbeddableTypeDescriptor(); + } + + @Override + public Nature getNature() { + return delegate.getNature(); + } + + @Override + public String getAttributeName() { + return delegate.getAttributeName(); + } + + @Override + public Object getIdentifier(Object entity) { + return delegate.getIdentifier( entity ); + } + + @Override + public void setIdentifier(Object entity, Object id, SharedSessionContractImplementor session) { + delegate.setIdentifier( entity, id, session ); + } + + @Override + public String getSqlAliasStem() { + return ""; + } + + @Override + public String getFetchableName() { + return delegate.getFetchableName(); + } +} diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveAbstractPersisterDelegate.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveAbstractPersisterDelegate.java index 4c1c67d9e..9a31d2bc8 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveAbstractPersisterDelegate.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveAbstractPersisterDelegate.java @@ -35,6 +35,7 @@ import org.hibernate.metamodel.mapping.EntityValuedModelPart; import org.hibernate.metamodel.mapping.ManagedMappingType; import org.hibernate.metamodel.mapping.SingularAttributeMapping; +import org.hibernate.metamodel.mapping.internal.EmbeddedIdentifierMappingImpl; import org.hibernate.metamodel.mapping.internal.GeneratedValuesProcessor; import org.hibernate.metamodel.mapping.internal.MappingModelCreationHelper; import org.hibernate.metamodel.mapping.internal.MappingModelCreationProcess; @@ -56,6 +57,7 @@ import org.hibernate.reactive.loader.ast.spi.ReactiveSingleUniqueKeyEntityLoader; import org.hibernate.reactive.logging.impl.Log; import org.hibernate.reactive.logging.impl.LoggerFactory; +import org.hibernate.reactive.metamodel.mapping.internal.ReactiveEmbeddedIdentifierMappingImpl; import org.hibernate.reactive.metamodel.mapping.internal.ReactivePluralAttributeMapping; import org.hibernate.reactive.metamodel.mapping.internal.ReactiveToOneAttributeMapping; import org.hibernate.reactive.sql.results.graph.embeddable.internal.ReactiveNonAggregatedIdentifierMappingFetch; @@ -324,6 +326,9 @@ public EntityIdentifierMapping convertEntityIdentifierMapping(EntityIdentifierMa if ( entityIdentifierMapping instanceof NonAggregatedIdentifierMappingImpl ) { return new ReactiveNonAggregatedIdentifierMappingImpl( (NonAggregatedIdentifierMappingImpl) entityIdentifierMapping ); } + if ( entityIdentifierMapping instanceof EmbeddedIdentifierMappingImpl ) { + return new ReactiveEmbeddedIdentifierMappingImpl( (EmbeddedIdentifierMappingImpl) entityIdentifierMapping ); + } return entityIdentifierMapping; } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/graph/embeddable/internal/ReactiveEmbeddableInitializerImpl.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/graph/embeddable/internal/ReactiveEmbeddableInitializerImpl.java index 3e760d9b0..69ae34a73 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/graph/embeddable/internal/ReactiveEmbeddableInitializerImpl.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/graph/embeddable/internal/ReactiveEmbeddableInitializerImpl.java @@ -84,6 +84,15 @@ public CompletionStage forEachReactiveSubInitializer( } } + @Override + public void resolveInstance(EmbeddableInitializerData data) { + // We need to clean up the instance, otherwise the .find with multiple id is not going to work correctly. + // It will only return the first element of the list. See EmbeddedIdTest#testFindMultipleIds. + // ORM doesn't have this issue because they don't have a find with multiple ids. + data.setInstance( null ); + super.resolveInstance( data ); + } + @Override public Object getResolvedInstance(EmbeddableInitializerData data) { return super.getResolvedInstance( data ); diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/EmbeddedIdTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/EmbeddedIdTest.java new file mode 100644 index 000000000..de3590fab --- /dev/null +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/EmbeddedIdTest.java @@ -0,0 +1,174 @@ +/* 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.Collection; +import java.util.Objects; +import java.util.Set; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import io.vertx.junit5.Timeout; +import io.vertx.junit5.VertxTestContext; +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.assertj.core.api.Assertions.assertThat; + +@Timeout(value = 10, timeUnit = MINUTES) +public class EmbeddedIdTest extends BaseReactiveTest { + + @Override + protected Collection> annotatedEntities() { + return Set.of( Delivery.class ); + } + + LocationId verbania = new LocationId( "Italy", "Verbania" ); + Delivery pizza = new Delivery( verbania, "Pizza Margherita" ); + + LocationId hallein = new LocationId( "Austria", "Hallein" ); + Delivery schnitzel = new Delivery( hallein, "Wiener Schnitzel" ); + + @BeforeEach + public void populateDb(VertxTestContext context) { + test( context, getMutinySessionFactory().withTransaction( s -> s.persistAll( pizza, schnitzel ) ) ); + } + + @Test + public void testFindSingleId(VertxTestContext context) { + test( context, getMutinySessionFactory().withTransaction( s -> s.find( Delivery.class, verbania ) ) + .invoke( result -> assertThat( result ).isEqualTo( pizza ) ) + ); + } + + @Test + public void testFindMultipleIds(VertxTestContext context) { + test( context, getMutinySessionFactory().withTransaction( s -> s.find( Delivery.class, verbania, hallein ) ) + .invoke( result -> assertThat( result ).containsExactlyInAnyOrder( pizza, schnitzel ) ) + ); + } + + @Entity(name = "Delivery") + @Table(name = "Delivery") + public static class Delivery { + + @EmbeddedId + private LocationId locationId; + + @Column(name = "field") + private String field; + + public Delivery() { + } + + public Delivery(LocationId locationId, String field) { + this.locationId = locationId; + this.field = field; + } + + public LocationId getLocationId() { + return locationId; + } + + public void setLocationId(LocationId locationId) { + this.locationId = locationId; + } + + public String getField() { + return field; + } + + public void setField(String field) { + this.field = field; + } + + @Override + public String toString() { + return locationId + ":" + field; + } + + @Override + public boolean equals(Object o) { + if ( this == o ) { + return true; + } + if ( o == null || getClass() != o.getClass() ) { + return false; + } + Delivery table = (Delivery) o; + return Objects.equals( locationId, table.locationId ) && + Objects.equals( field, table.field ); + } + + @Override + public int hashCode() { + return Objects.hash( locationId, field ); + } + } + + + @Embeddable + public static class LocationId { + + @Column(name = "sp_country") + private String country; + + @Column(name = "sp_city") + private String city; + + public LocationId(String country, String city) { + this.country = country; + this.city = city; + } + + public LocationId() { + } + + public String getCountry() { + return country; + } + + public String getCity() { + return city; + } + + public void setCountry(String country) { + this.country = country; + } + + public void setCity(String city) { + this.city = city; + } + + @Override + public String toString() { + return "[" + country + "-" + city + "]"; + } + + @Override + public boolean equals(Object o) { + if ( this == o ) { + return true; + } + if ( o == null || getClass() != o.getClass() ) { + return false; + } + LocationId tableId = (LocationId) o; + return Objects.equals( country, tableId.country ) && + Objects.equals( city, tableId.city ); + } + + @Override + public int hashCode() { + return Objects.hash( country, city ); + } + } +} diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/MutinySessionTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/MutinySessionTest.java index 604b08f6a..fb8c7e1f1 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/MutinySessionTest.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/MutinySessionTest.java @@ -68,6 +68,20 @@ private static String nameFromResult(List rowSet) { } } + @Test + public void reactiveFindMultipleIds(VertxTestContext context) { + final GuineaPig rump = new GuineaPig( 55, "Rumpelstiltskin" ); + final GuineaPig emma = new GuineaPig( 77, "Emma" ); + test( context, populateDB() + .chain( () -> getMutinySessionFactory().withTransaction( s -> s.persistAll( emma, rump ) ) ) + .chain( () -> getMutinySessionFactory().withTransaction( s -> s.find( GuineaPig.class, emma.getId(), rump.getId() ) ) + ) + .invoke( pigs -> { + org.assertj.core.api.Assertions.assertThat( pigs ).containsExactlyInAnyOrder( emma, rump ); + } ) + ); + } + @Test public void sessionClear(VertxTestContext context) { final GuineaPig guineaPig = new GuineaPig( 81, "Perry" ); diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/ReactiveSessionTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/ReactiveSessionTest.java index 959dc4ed7..73de9fd30 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/ReactiveSessionTest.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/ReactiveSessionTest.java @@ -79,6 +79,21 @@ public void reactiveFind(VertxTestContext context) { ); } + @Test + public void reactiveFindMultipleIds(VertxTestContext context) { + final GuineaPig rump = new GuineaPig( 55, "Rumpelstiltskin" ); + final GuineaPig emma = new GuineaPig( 77, "Emma" ); + test( context, populateDB() + .thenCompose( v -> getSessionFactory().withTransaction( s -> s.persist( emma, rump ) ) ) + .thenCompose( v -> getSessionFactory().withTransaction( s -> s + .find( GuineaPig.class, emma.getId(), rump.getId() ) ) + ) + .thenAccept( pigs -> { + org.assertj.core.api.Assertions.assertThat( pigs ).containsExactlyInAnyOrder( emma, rump ); + } ) + ); + } + @Test public void sessionClear(VertxTestContext context) { final GuineaPig guineaPig = new GuineaPig( 81, "Perry" );