From f006fbf029ae3a20ad10d1460c15153cd4679ff4 Mon Sep 17 00:00:00 2001 From: Andrea Boriero <andrea@hibernate.org> Date: Thu, 6 Mar 2025 16:39:44 +0100 Subject: [PATCH 1/2] [#1867] Error when inserting in batch with joined table inheritance --- .../ReactiveMutationExecutorStandard.java | 90 +++++++++++++++++-- 1 file changed, 83 insertions(+), 7 deletions(-) diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/jdbc/mutation/internal/ReactiveMutationExecutorStandard.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/jdbc/mutation/internal/ReactiveMutationExecutorStandard.java index 4f1d62325..0a09ff3a1 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/jdbc/mutation/internal/ReactiveMutationExecutorStandard.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/jdbc/mutation/internal/ReactiveMutationExecutorStandard.java @@ -5,10 +5,6 @@ */ package org.hibernate.reactive.engine.jdbc.mutation.internal; -import java.lang.invoke.MethodHandles; -import java.sql.SQLException; -import java.util.concurrent.CompletionStage; - import org.hibernate.engine.jdbc.batch.spi.Batch; import org.hibernate.engine.jdbc.mutation.JdbcValueBindings; import org.hibernate.engine.jdbc.mutation.OperationResultChecker; @@ -25,6 +21,7 @@ import org.hibernate.persister.entity.mutation.EntityTableMapping; import org.hibernate.reactive.adaptor.impl.PrepareStatementDetailsAdaptor; import org.hibernate.reactive.adaptor.impl.PreparedStatementAdaptor; +import org.hibernate.reactive.engine.jdbc.ResultsCheckerUtil; import org.hibernate.reactive.engine.jdbc.env.internal.ReactiveMutationExecutor; import org.hibernate.reactive.generator.values.ReactiveGeneratedValuesMutationDelegate; import org.hibernate.reactive.logging.impl.Log; @@ -37,9 +34,16 @@ import org.hibernate.sql.model.TableMapping; import org.hibernate.sql.model.ValuesAnalysis; +import java.lang.invoke.MethodHandles; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletionStage; + import static org.hibernate.engine.jdbc.mutation.internal.ModelMutationHelper.checkResults; import static org.hibernate.reactive.logging.impl.LoggerFactory.make; import static org.hibernate.reactive.util.impl.CompletionStages.failedFuture; +import static org.hibernate.reactive.util.impl.CompletionStages.loop; import static org.hibernate.reactive.util.impl.CompletionStages.nullFuture; import static org.hibernate.reactive.util.impl.CompletionStages.voidFuture; import static org.hibernate.sql.model.ModelMutationLogging.MODEL_MUTATION_LOGGER; @@ -73,10 +77,64 @@ private ReactiveConnection connection(SharedSessionContractImplementor session) @Override public CompletionStage<Void> performReactiveBatchedOperations( ValuesAnalysis valuesAnalysis, - TableInclusionChecker inclusionChecker, OperationResultChecker resultChecker, + TableInclusionChecker inclusionChecker, + OperationResultChecker resultChecker, SharedSessionContractImplementor session) { - return ReactiveMutationExecutor.super - .performReactiveBatchedOperations( valuesAnalysis, inclusionChecker, resultChecker, session); + final PreparedStatementGroup batchedMutationOperationGroup = getBatchedPreparedStatementGroup(); + if ( batchedMutationOperationGroup != null ) { + final List<PreparedStatementDetails> preparedStatementDetailsList = new ArrayList<>( + batchedMutationOperationGroup.getNumberOfStatements() ); + batchedMutationOperationGroup.forEachStatement( (tableName, statementDetails) -> preparedStatementDetailsList + .add( statementDetails ) ); + return loop( preparedStatementDetailsList, statementDetails -> { + if ( statementDetails == null ) { + return voidFuture(); + } + final JdbcValueBindings valueBindings = getJdbcValueBindings(); + final TableMapping tableDetails = statementDetails.getMutatingTableDetails(); + if ( inclusionChecker != null && !inclusionChecker.include( tableDetails ) ) { + if ( MODEL_MUTATION_LOGGER.isTraceEnabled() ) { + MODEL_MUTATION_LOGGER.tracef( + "Skipping execution of secondary insert : %s", + tableDetails.getTableName() + ); + } + return voidFuture(); + } + + // If we get here the statement is needed - make sure it is resolved + final Object[] paramValues = PreparedStatementAdaptor.bind( statement -> { + PreparedStatementDetails details = new PrepareStatementDetailsAdaptor( + statementDetails, + statement, + session.getJdbcServices() + ); + valueBindings.beforeStatement( details ); + } ); + + final ReactiveConnection reactiveConnection = ( (ReactiveConnectionSupplier) session ).getReactiveConnection(); + final String sql = statementDetails.getSqlString(); + return reactiveConnection.update( + sql, + paramValues, + true, + (rowCount, batchPosition, query) -> ResultsCheckerUtil.checkResults( + session, + statementDetails, + resultChecker, + rowCount, + batchPosition + ) + ).whenComplete( (o, throwable) -> { //TODO: is this part really needed? + if ( statementDetails.getStatement() != null ) { + statementDetails.releaseStatement( session ); + } + valueBindings.afterStatement( tableDetails ); + } ); + } + ); + } + return voidFuture(); } @Override @@ -159,6 +217,23 @@ public CompletionStage<GeneratedValues> performReactiveNonBatchedOperations( } } + @Override + public CompletionStage<Void> performReactiveSelfExecutingOperations( + ValuesAnalysis valuesAnalysis, + TableInclusionChecker inclusionChecker, + SharedSessionContractImplementor session) { + if ( getSelfExecutingMutations() == null || getSelfExecutingMutations().isEmpty() ) { + return voidFuture(); + } + + return loop( getSelfExecutingMutations(), operation -> { + if ( inclusionChecker.include( operation.getTableDetails() ) ) { + operation.performMutation( getJdbcValueBindings(), valuesAnalysis, session ); + } + return voidFuture(); + }); + } + private class OperationsForEach { private final Object id; @@ -210,6 +285,7 @@ public CompletionStage<Void> buildLoop() { return loop; } } + @Override public CompletionStage<Void> performReactiveNonBatchedMutation( PreparedStatementDetails statementDetails, From 9598c9c60d6fc620af203ca85e9dd4bdf8d2ed10 Mon Sep 17 00:00:00 2001 From: Davide D'Alto <davide@hibernate.org> Date: Tue, 27 Feb 2024 12:59:51 +0100 Subject: [PATCH 2/2] [#1867] Test case --- .../reactive/JoinedInheritanceBatchTest.java | 130 ++++++++ .../ManyToManyWithCompositeIdTest.java | 293 ++++++++++++++++++ 2 files changed, 423 insertions(+) create mode 100644 hibernate-reactive-core/src/test/java/org/hibernate/reactive/JoinedInheritanceBatchTest.java create mode 100644 hibernate-reactive-core/src/test/java/org/hibernate/reactive/ManyToManyWithCompositeIdTest.java diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/JoinedInheritanceBatchTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/JoinedInheritanceBatchTest.java new file mode 100644 index 000000000..0c49d883e --- /dev/null +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/JoinedInheritanceBatchTest.java @@ -0,0 +1,130 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive; + + +import org.junit.jupiter.api.Test; + +import io.vertx.junit5.Timeout; +import io.vertx.junit5.VertxTestContext; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Inheritance; +import jakarta.persistence.InheritanceType; +import jakarta.persistence.SequenceGenerator; +import jakarta.persistence.Table; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.CompletionStage; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hibernate.reactive.util.impl.CompletionStages.voidFuture; + +@Timeout(value = 10, timeUnit = MINUTES) +public class JoinedInheritanceBatchTest extends BaseReactiveTest { + + @Override + protected Collection<Class<?>> annotatedEntities() { + return List.of( ClientA.class, Client.class ); + } + + @Override + protected CompletionStage<Void> cleanDb() { + return voidFuture(); + } + + @Test + public void test(VertxTestContext context) { + final ClientA client1 = new ClientA("Client 1", "email@c1", "123456"); + + test( context, getMutinySessionFactory().withTransaction( session -> { + session.setBatchSize( 5 ); + return session.persist( client1 ); + } ) + .chain( () -> getMutinySessionFactory().withTransaction( session -> session + .createQuery( "select c from Client c", Client.class ) + .getResultList() + .invoke( persistedClients -> assertThat( persistedClients ) + .as( "Clients has not bee persisted" ) + .isNotEmpty() ) ) ) + ); + } + + @Entity(name = "Client") + @Table(name = "`Client`") + @Inheritance(strategy = InheritanceType.JOINED) + public static class Client { + + @Id + @SequenceGenerator(name = "seq", sequenceName = "id_seq", allocationSize = 1) + @GeneratedValue(generator = "seq", strategy = GenerationType.SEQUENCE) + private Long id; + + private String name; + + private String email; + + private String phone; + + public Client() { + } + + public Client(String name, String email, String phone) { + this.name = name; + this.email = email; + this.phone = phone; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPhone() { + return phone; + } + + public void setPhone(String phone) { + this.phone = phone; + } + + } + + @Entity + @Table(name = "`ClientA`") + public static class ClientA extends Client { + + public ClientA() { + } + + public ClientA(String name, String email, String phone) { + super( name, email, phone ); + } + } + +} diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/ManyToManyWithCompositeIdTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/ManyToManyWithCompositeIdTest.java new file mode 100644 index 000000000..8c72c38d3 --- /dev/null +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/ManyToManyWithCompositeIdTest.java @@ -0,0 +1,293 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive; + +import io.vertx.junit5.Timeout; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import io.vertx.junit5.VertxTestContext; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.IdClass; +import jakarta.persistence.Inheritance; +import jakarta.persistence.InheritanceType; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.SequenceGenerator; +import jakarta.persistence.Table; +import java.util.concurrent.CompletionStage; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.hibernate.reactive.util.impl.CompletionStages.voidFuture; + +@Timeout(value = 10, timeUnit = MINUTES) +public class ManyToManyWithCompositeIdTest extends BaseReactiveTest { + + @Override + protected Collection<Class<?>> annotatedEntities() { + return List.of( CarsClients.class, ClientA.class, Client.class, Car.class ); + } + + @Override + protected CompletionStage<Void> cleanDb() { + return voidFuture(); + } + + @Test + public void test(VertxTestContext context) { + List<Client> clients = new ArrayList<>(); + for ( int i = 0; i < 5; i++ ) { + ClientA client = new ClientA(); + client.setName( "name" + i ); + client.setEmail( "email" + i ); + client.setPhone( "phone" + i ); + clients.add( client ); + } + + List<Car> cars = new ArrayList<>(); + for ( int i = 0; i < 2; i++ ) { + Car car = new Car(); + car.setBrand( "brand" + i ); + car.setModel( "model" + i ); + cars.add( car ); + } + + test( context, getMutinySessionFactory() + .withSession( session -> { + session.setBatchSize( 5 ); + return session.persistAll( cars.toArray() ) + .chain( () -> session + .persistAll( clients.toArray() ) + .chain( session::flush ) ) + .chain( () -> { + List<CarsClients> carsClientsList = new ArrayList<>(); + for ( Client client : clients ) { + for ( Car car : cars ) { + CarsClients carsClients = new CarsClients( "location" ); + carsClientsList.add( carsClients ); + car.addClient( carsClients ); + client.addCar( carsClients ); + } + } + return session + .persistAll( carsClientsList.toArray() ) + .chain( session::flush ); + } ); + } ) + ); + } + + @Entity(name = "Car") + @Table(name = "Car_Table") + public static class Car { + + @Id + @SequenceGenerator(name = "seq_car", sequenceName = "id_seq_car", allocationSize = 1) + @GeneratedValue(generator = "seq_car", strategy = GenerationType.SEQUENCE) + private Long id; + + public String brand; + + + private String model; + + @OneToMany(mappedBy = "car") + private Set<CarsClients> clients = new HashSet<>(); + + public Car() { + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getBrand() { + return brand; + } + + public void setBrand(String brand) { + this.brand = brand; + } + + public String getModel() { + return model; + } + + public void setModel(String model) { + this.model = model; + } + + public Set<CarsClients> getClients() { + return clients; + } + + public void setClients(Set<CarsClients> clients) { + this.clients = clients; + } + + public void addClient(CarsClients carsClients) { + carsClients.setCar( this ); + clients.add( carsClients ); + } + } + + @Entity + @Table(name = "Client_Table") + @Inheritance(strategy = InheritanceType.JOINED) + public static class Client { + + @Id + @SequenceGenerator(name = "seq", sequenceName = "id_seq", allocationSize = 1) + @GeneratedValue(generator = "seq", strategy = GenerationType.SEQUENCE) + private Long id; + + private String name; + + private String email; + + private String phone; + + @OneToMany(mappedBy = "client") + private Set<CarsClients> cars = new HashSet<>(); + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPhone() { + return phone; + } + + public void setPhone(String phone) { + this.phone = phone; + } + + public Set<CarsClients> getCars() { + return cars; + } + + public void setCars(Set<CarsClients> cars) { + this.cars = cars; + } + + public void addCar(CarsClients carsClients) { + carsClients.setClient( this ); + cars.add( carsClients ); + } + } + + @Entity + @Table(name = "ClientA_Table") + public static class ClientA extends Client { + + public ClientA() { + } + } + + @Entity + @IdClass(CarsClientsId.class) + @Table(name = "cars_clients_table") + public static class CarsClients { + + @Id + @ManyToOne + private Car car; + + @Id + @ManyToOne + private Client client; + + private String location; + + public CarsClients() { + } + + public CarsClients(String location) { + this.location = location; + } + + public Car getCar() { + return car; + } + + public void setCar(Car car) { + this.car = car; + } + + public Client getClient() { + return client; + } + + public void setClient(Client client) { + this.client = client; + } + + public String getLocation() { + return location; + } + + public void setLocation(String location) { + this.location = location; + } + } + + public static class CarsClientsId { + private Car car; + + private Client client; + + public CarsClientsId() { + } + + public Car getCar() { + return car; + } + + public void setCar(Car car) { + this.car = car; + } + + public Client getClient() { + return client; + } + + public void setClient(Client client) { + this.client = client; + } + } +}