From f56ddc257895bcdc47e0e0b6432d05bed7e14c0c Mon Sep 17 00:00:00 2001 From: Andrea Boriero Date: Thu, 20 Feb 2025 17:21:01 +0100 Subject: [PATCH 1/2] [#2108] StatelessSession insertAll in batch does not do batching --- .../org/hibernate/reactive/mutiny/Mutiny.java | 12 +++++--- .../impl/MutinyStatelessSessionImpl.java | 10 +++---- .../impl/ReactiveStatelessSessionImpl.java | 30 ++++++++++++------- .../org/hibernate/reactive/stage/Stage.java | 12 +++++--- .../stage/impl/StageStatelessSessionImpl.java | 8 ++--- 5 files changed, 45 insertions(+), 27 deletions(-) diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/mutiny/Mutiny.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/mutiny/Mutiny.java index a8faefbbd..a648a7bd7 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/mutiny/Mutiny.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/mutiny/Mutiny.java @@ -1779,7 +1779,8 @@ default Uni get(Class entityClass, Object id, LockModeType lockModeTyp Uni insert(Object entity); /** - * Insert multiple rows. + * Insert multiple rows, using the number of the + * given entities as the batch size. * * @param entities new transient instances * @@ -1817,7 +1818,8 @@ default Uni get(Class entityClass, Object id, LockModeType lockModeTyp Uni delete(Object entity); /** - * Delete multiple rows. + * Delete multiple rows, using the number of the + * given entities as the batch size. * * @param entities detached entity instances * @@ -1855,7 +1857,8 @@ default Uni get(Class entityClass, Object id, LockModeType lockModeTyp Uni update(Object entity); /** - * Update multiple rows. + * Update multiple rows, using the number of the + * given entities as the batch size. * * @param entities detached entity instances * @@ -1915,7 +1918,8 @@ default Uni get(Class entityClass, Object id, LockModeType lockModeTyp Uni refresh(Object entity); /** - * Refresh the entity instance state from the database. + * Refresh the entity instance state from the database, using the number of the + * given entities as the batch size. * * @param entities The entities to be refreshed. * diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/mutiny/impl/MutinyStatelessSessionImpl.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/mutiny/impl/MutinyStatelessSessionImpl.java index 473196712..a80d79750 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/mutiny/impl/MutinyStatelessSessionImpl.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/mutiny/impl/MutinyStatelessSessionImpl.java @@ -138,7 +138,7 @@ public Uni insert(Object entity) { @Override public Uni insertAll(Object... entities) { - return uni( () -> delegate.reactiveInsertAll( entities ) ); + return uni( () -> delegate.reactiveInsertAll( entities.length, entities ) ); } @Override @@ -158,12 +158,12 @@ public Uni delete(Object entity) { @Override public Uni deleteAll(Object... entities) { - return uni( () -> delegate.reactiveDeleteAll( entities ) ); + return uni( () -> delegate.reactiveDeleteAll( entities.length, entities ) ); } @Override public Uni deleteAll(int batchSize, Object... entities) { - return uni( () -> delegate.reactiveDeleteAll( entities ) ); + return uni( () -> delegate.reactiveDeleteAll( batchSize, entities ) ); } @Override @@ -178,7 +178,7 @@ public Uni update(Object entity) { @Override public Uni updateAll(Object... entities) { - return uni( () -> delegate.reactiveUpdateAll( entities ) ); + return uni( () -> delegate.reactiveUpdateAll( entities.length, entities ) ); } @Override @@ -208,7 +208,7 @@ public Uni upsert(String entityName, Object entity) { @Override public Uni refreshAll(Object... entities) { - return uni( () -> delegate.reactiveRefreshAll( entities ) ); + return uni( () -> delegate.reactiveRefreshAll( entities.length, entities ) ); } @Override diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/session/impl/ReactiveStatelessSessionImpl.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/session/impl/ReactiveStatelessSessionImpl.java index 8e8e338a6..53b928903 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/session/impl/ReactiveStatelessSessionImpl.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/session/impl/ReactiveStatelessSessionImpl.java @@ -128,7 +128,7 @@ public class ReactiveStatelessSessionImpl extends StatelessSessionImpl implement private final ReactiveConnection reactiveConnection; - private final ReactiveStatelessSession batchingHelperSession; + private final ReactiveStatelessSessionImpl batchingHelperSession; private final PersistenceContext persistenceContext; @@ -150,10 +150,9 @@ private ReactiveStatelessSessionImpl( PersistenceContext persistenceContext) { super( factory, options ); this.persistenceContext = persistenceContext; - Integer batchSize = getConfiguredJdbcBatchSize(); - reactiveConnection = batchSize == null || batchSize < 2 - ? connection - : new BatchingConnection( connection, batchSize ); + // Setting batch size to 0 because `StatelessSession` does not consider + // the value of `hibernate.jdbc.batch_size` + reactiveConnection = new BatchingConnection( connection, 0 ); batchingHelperSession = this; influencers = new LoadQueryInfluencers( factory ); } @@ -551,9 +550,12 @@ public CompletionStage reactiveInsertAll(Object... entities) { @Override public CompletionStage reactiveInsertAll(int batchSize, Object... entities) { + final Integer jdbcBatchSize = batchingHelperSession.getJdbcBatchSize(); + batchingHelperSession.setJdbcBatchSize( batchSize ); final ReactiveConnection connection = batchingConnection( batchSize ); return loop( entities, batchingHelperSession::reactiveInsert ) - .thenCompose( v -> connection.executeBatch() ); + .thenCompose( v -> connection.executeBatch() ) + .whenComplete( (v, throwable) -> batchingHelperSession.setJdbcBatchSize( jdbcBatchSize ) ); } @Override @@ -564,9 +566,12 @@ public CompletionStage reactiveUpdateAll(Object... entities) { @Override public CompletionStage reactiveUpdateAll(int batchSize, Object... entities) { + final Integer jdbcBatchSize = batchingHelperSession.getJdbcBatchSize(); + batchingHelperSession.setJdbcBatchSize( batchSize ); final ReactiveConnection connection = batchingConnection( batchSize ); return loop( entities, batchingHelperSession::reactiveUpdate ) - .thenCompose( v -> connection.executeBatch() ); + .thenCompose( v -> connection.executeBatch() ) + .whenComplete( (v, throwable) -> batchingHelperSession.setJdbcBatchSize( jdbcBatchSize ) ); } @Override @@ -577,9 +582,11 @@ public CompletionStage reactiveDeleteAll(Object... entities) { @Override public CompletionStage reactiveDeleteAll(int batchSize, Object... entities) { + final Integer jdbcBatchSize = batchingHelperSession.getJdbcBatchSize(); + batchingHelperSession.setJdbcBatchSize( batchSize ); final ReactiveConnection connection = batchingConnection( batchSize ); - return loop( entities, batchingHelperSession::reactiveDelete ) - .thenCompose( v -> connection.executeBatch() ); + return loop( entities, batchingHelperSession::reactiveDelete ).thenCompose( v -> connection.executeBatch() ) + .whenComplete( (v, throwable) -> batchingHelperSession.setJdbcBatchSize( jdbcBatchSize ) ); } @@ -591,9 +598,12 @@ public CompletionStage reactiveRefreshAll(Object... entities) { @Override public CompletionStage reactiveRefreshAll(int batchSize, Object... entities) { + final Integer jdbcBatchSize = batchingHelperSession.getJdbcBatchSize(); + batchingHelperSession.setJdbcBatchSize( batchSize ); final ReactiveConnection connection = batchingConnection( batchSize ); return loop( entities, batchingHelperSession::reactiveRefresh ) - .thenCompose( v -> connection.executeBatch() ); + .thenCompose( v -> connection.executeBatch() ) + .whenComplete( (v, throwable) -> batchingHelperSession.setJdbcBatchSize( jdbcBatchSize ) ); } private ReactiveConnection batchingConnection(int batchSize) { diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/stage/Stage.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/stage/Stage.java index 711a91ab9..074f63e58 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/stage/Stage.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/stage/Stage.java @@ -1836,7 +1836,8 @@ default CompletionStage get(Class entityClass, Object id, LockModeType CompletionStage insert(Object entity); /** - * Insert multiple rows. + * Insert multiple rows, using the number of the + * given entities as the batch size. * * @param entities new transient instances * @@ -1874,7 +1875,8 @@ default CompletionStage get(Class entityClass, Object id, LockModeType CompletionStage delete(Object entity); /** - * Delete multiple rows. + * Delete multiple rows, using the number of the + * given entities as the batch size. * * @param entities detached entity instances * @@ -1912,7 +1914,8 @@ default CompletionStage get(Class entityClass, Object id, LockModeType CompletionStage update(Object entity); /** - * Update multiple rows. + * Update multiple rows, using the number of the + * given entities as the batch size. * * @param entities a detached entity instance * @@ -1950,7 +1953,8 @@ default CompletionStage get(Class entityClass, Object id, LockModeType CompletionStage refresh(Object entity); /** - * Refresh the entity instance state from the database. + * Refresh the entity instance state from the database, using the number of the + * given entities as the batch size. * * @param entities The entities to be refreshed. * diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/stage/impl/StageStatelessSessionImpl.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/stage/impl/StageStatelessSessionImpl.java index 7724a5cd1..c97be1a94 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/stage/impl/StageStatelessSessionImpl.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/stage/impl/StageStatelessSessionImpl.java @@ -67,7 +67,7 @@ public CompletionStage insert(Object entity) { @Override public CompletionStage insert(Object... entities) { - return delegate.reactiveInsertAll( entities ); + return delegate.reactiveInsertAll( entities.length, entities ); } @Override @@ -87,7 +87,7 @@ public CompletionStage delete(Object entity) { @Override public CompletionStage delete(Object... entities) { - return delegate.reactiveDeleteAll( entities ); + return delegate.reactiveDeleteAll( entities.length, entities ); } @Override @@ -107,7 +107,7 @@ public CompletionStage update(Object entity) { @Override public CompletionStage update(Object... entities) { - return delegate.reactiveUpdateAll( entities ); + return delegate.reactiveUpdateAll( entities.length, entities ); } @Override @@ -127,7 +127,7 @@ public CompletionStage refresh(Object entity) { @Override public CompletionStage refresh(Object... entities) { - return delegate.reactiveRefreshAll( entities ); + return delegate.reactiveRefreshAll( entities.length, entities ); } @Override From b464efea02f71966d47d0c17c4a906e1d1a6c206 Mon Sep 17 00:00:00 2001 From: Andrea Boriero Date: Wed, 12 Feb 2025 10:57:21 +0100 Subject: [PATCH 2/2] [#2108] Add test for StatelessSession insertAll in batch does not do batching --- ...ReactiveStatelessDefaultBatchSizeTest.java | 622 ++++++++++++++++++ .../reactive/BatchingConnectionTest.java | 69 ++ 2 files changed, 691 insertions(+) create mode 100644 hibernate-reactive-core/src/test/java/org/hibernate/ReactiveStatelessDefaultBatchSizeTest.java diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/ReactiveStatelessDefaultBatchSizeTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/ReactiveStatelessDefaultBatchSizeTest.java new file mode 100644 index 000000000..c4d74261e --- /dev/null +++ b/hibernate-reactive-core/src/test/java/org/hibernate/ReactiveStatelessDefaultBatchSizeTest.java @@ -0,0 +1,622 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate; + +import org.hibernate.boot.registry.StandardServiceRegistryBuilder; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.cfg.Configuration; +import org.hibernate.reactive.BaseReactiveTest; +import org.hibernate.reactive.stage.Stage; +import org.hibernate.reactive.testing.SqlStatementTracker; + +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.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * The test aims to check that methods accepting the batch size as parameter e.g. {@link Stage.StatelessSession#insert(int, Object...)} + * work when {@link AvailableSettings.STATEMENT_BATCH_SIZE} hasn't been set. + */ +@Timeout(value = 10, timeUnit = MINUTES) +public class ReactiveStatelessDefaultBatchSizeTest extends BaseReactiveTest { + private static SqlStatementTracker sqlTracker; + + private static final String PIG_ONE_NAME = "One"; + private static final String PIG_TWO_NAME = "Two"; + private static final String PIG_THREE_NAME = "Three"; + private static final String PIG_FOUR_NAME = "Four"; + private static final String PIG_FIVE_NAME = "Five"; + private static final String PIG_SIX_NAME = "Six"; + + private static final GuineaPig PIG_ONE = new GuineaPig( 11, PIG_ONE_NAME ); + private static final GuineaPig PIG_TWO = new GuineaPig( 22, PIG_TWO_NAME ); + private static final GuineaPig PIG_THREE = new GuineaPig( 33, PIG_THREE_NAME ); + private static final GuineaPig PIG_FOUR = new GuineaPig( 44, PIG_FOUR_NAME ); + private static final GuineaPig PIG_FIVE = new GuineaPig( 55, PIG_FIVE_NAME ); + private static final GuineaPig PIG_SIX = new GuineaPig( 66, PIG_SIX_NAME ); + + private static final GuineaPig[] PIGS = { PIG_ONE, PIG_TWO, PIG_THREE, PIG_FOUR, PIG_FIVE, PIG_SIX, }; + + @Override + protected Set> annotatedEntities() { + return Set.of( GuineaPig.class ); + } + + @Override + protected Configuration constructConfiguration() { + Configuration configuration = super.constructConfiguration(); + + // Construct a tracker that collects query statements via the SqlStatementLogger framework. + // Pass in configuration properties to hand off any actual logging properties + sqlTracker = new SqlStatementTracker( + ReactiveStatelessDefaultBatchSizeTest::filter, + configuration.getProperties() + ); + return configuration; + } + + @BeforeEach + public void clearTracker() { + sqlTracker.clear(); + } + + @Override + protected void addServices(StandardServiceRegistryBuilder builder) { + sqlTracker.registerService( builder ); + } + + private static boolean filter(String s) { + String[] accepted = { "insert ", "update ", "delete " }; + for ( String valid : accepted ) { + if ( s.toLowerCase().startsWith( valid ) ) { + return true; + } + } + return false; + } + + @Test + public void testMutinyBatchingInsert(VertxTestContext context) { + test( context, getMutinySessionFactory().withStatelessTransaction( s -> s.insertAll( 10, PIGS ) ) + .invoke( () -> { + // We expect only one insert query + assertThat( sqlTracker.getLoggedQueries() ).hasSize( 1 ); + // Parameters are different for different dbs, so we cannot do an exact match + assertThat( sqlTracker.getLoggedQueries().get( 0 ) ) + .matches( "insert into pig \\(name,id\\) values (.*)" ); + } ) + ); + } + + @Test + public void testMutinyBatchingInsertMultiple(VertxTestContext context) { + test( context, getMutinySessionFactory().withStatelessTransaction( s -> s.insertMultiple( List.of( PIGS ) ) ) + .invoke( () -> { + // We expect only one insert query + assertThat( sqlTracker.getLoggedQueries() ).hasSize( 1 ); + // Parameters are different for different dbs, so we cannot do an exact match + assertThat( sqlTracker.getLoggedQueries().get( 0 ) ) + .matches( "insert into pig \\(name,id\\) values (.*)" ); + } ) + .invoke( v -> getMutinySessionFactory().withStatelessTransaction( s -> s + .createQuery( "select p from GuineaPig p", GuineaPig.class ) + .getResultList() + .invoke( pigs -> assertThat( pigs ).hasSize( PIGS.length ) ) + ) + ) + ); + } + + @Test + public void testMutinyBatchingInsertAllNoBatchSizeParameter(VertxTestContext context) { + test( context, getMutinySessionFactory().withStatelessTransaction( s -> s.insertAll( PIGS ) ) + .invoke( () -> { + // We expect only one insert query + assertThat( sqlTracker.getLoggedQueries() ).hasSize( 1 ); + // Parameters are different for different dbs, so we cannot do an exact match + assertThat( sqlTracker.getLoggedQueries().get( 0 ) ) + .matches( "insert into pig \\(name,id\\) values (.*)" ); + } ) + .invoke( v -> getMutinySessionFactory().withStatelessTransaction( s -> s + .createQuery( "select p from GuineaPig p", GuineaPig.class ) + .getResultList() + .invoke( pigs -> assertThat( pigs ).hasSize( PIGS.length ) ) + ) + ) + ); + } + + @Test + public void testStageBatchingInsert(VertxTestContext context) { + test( context, getSessionFactory().withStatelessTransaction( s -> s.insert( 10, PIGS ) ) + .thenAccept( v -> { + // We expect only one insert query + assertThat( sqlTracker.getLoggedQueries() ).hasSize( 1 ); + // Parameters are different for different dbs, so we cannot do an exact match + assertThat( sqlTracker.getLoggedQueries().get( 0 ) ) + .matches( "insert into pig \\(name,id\\) values (.*)" ); + } ) + .thenAccept( v -> getSessionFactory().withStatelessTransaction( s -> s + .createQuery( "select p from GuineaPig p", GuineaPig.class ) + .getResultList() + .thenAccept( pigs -> assertThat( pigs ).hasSize( PIGS.length ) ) + ) + ) + ); + } + + @Test + public void testStageBatchingInsertMultiple(VertxTestContext context) { + test( context, getSessionFactory().withStatelessTransaction( s -> s.insertMultiple( List.of(PIGS) ) ) + .thenAccept( v -> { + // We expect only one insert query + assertThat( sqlTracker.getLoggedQueries() ).hasSize( 1 ); + // Parameters are different for different dbs, so we cannot do an exact match + assertThat( sqlTracker.getLoggedQueries().get( 0 ) ) + .matches( "insert into pig \\(name,id\\) values (.*)" ); + } ) + .thenAccept( v -> getSessionFactory().withStatelessTransaction( s -> s + .createQuery( "select p from GuineaPig p", GuineaPig.class ) + .getResultList() + .thenAccept( pigs -> assertThat( pigs ).hasSize( PIGS.length ) ) + ) + ) + ); + } + + @Test + public void testStageBatchingInsertNoBatchSizeParameter(VertxTestContext context) { + test( context, getSessionFactory().withStatelessTransaction( s -> s.insert( PIGS ) ) + .thenAccept( v -> { + // We expect only one insert query + assertThat( sqlTracker.getLoggedQueries() ).hasSize( 1 ); + // Parameters are different for different dbs, so we cannot do an exact match + assertThat( sqlTracker.getLoggedQueries().get( 0 ) ) + .matches( "insert into pig \\(name,id\\) values (.*)" ); + } ) + .thenAccept( v -> getSessionFactory().withStatelessTransaction( s -> s + .createQuery( "select p from GuineaPig p", GuineaPig.class ) + .getResultList() + .thenAccept( pigs -> assertThat( pigs ).hasSize( PIGS.length ) ) + ) + ) + ); + } + + @Test + public void testMutinyBatchingDelete(VertxTestContext context) { + test( context, getMutinySessionFactory().withStatelessTransaction( s -> s.insertAll( 10, PIGS ) ) + .invoke( sqlTracker::clear ) + .chain( v -> getMutinySessionFactory().withStatelessTransaction(s -> s + .createQuery( "select p from GuineaPig p", GuineaPig.class ).getResultList() + ) + .invoke( pigs -> sqlTracker.clear() ) + .chain( pigs -> getMutinySessionFactory().withStatelessTransaction( + s -> + s.deleteAll( 10, pigs.subList( 0, 2 ).toArray() ) + ) + ) + .invoke( () -> { + // We expect only one delete query + assertThat( sqlTracker.getLoggedQueries() ).hasSize( 1 ); + // Parameters are different for different dbs, so we cannot do an exact match + assertThat( sqlTracker.getLoggedQueries().get( 0 ) ).matches( "delete from pig where id=.*" ); + } ) + .chain( () -> getMutinySessionFactory().withStatelessTransaction( s -> s + .createQuery( "select p from GuineaPig p", GuineaPig.class ) + .getResultList() + ) + .invoke( guineaPigs -> assertThat( guineaPigs.size() ).isEqualTo( 4 ) ) + ) ) + ); + } + + @Test + public void testMutinyBatchingDeleteMultiple(VertxTestContext context) { + test( context, getMutinySessionFactory().withStatelessTransaction( s -> s.insertAll( 10, PIGS ) ) + .invoke( sqlTracker::clear ) + .chain( v -> getMutinySessionFactory().withStatelessTransaction(s -> s + .createQuery( "select p from GuineaPig p", GuineaPig.class ).getResultList() + ) + .invoke( pigs -> sqlTracker.clear() ) + .chain( pigs -> getMutinySessionFactory().withStatelessTransaction( + s -> s.deleteMultiple( pigs.subList( 0, 2 ) ) ) + ) + .invoke( () -> { + // We expect only one delete query + assertThat( sqlTracker.getLoggedQueries() ).hasSize( 1 ); + // Parameters are different for different dbs, so we cannot do an exact match + assertThat( sqlTracker.getLoggedQueries().get( 0 ) ).matches( "delete from pig where id=.*" ); + } ) + .chain( () -> getMutinySessionFactory().withStatelessTransaction( s -> s + .createQuery( "select p from GuineaPig p", GuineaPig.class ) + .getResultList() + ) + .invoke( guineaPigs -> assertThat( guineaPigs.size() ).isEqualTo( 4 ) ) + ) ) + ); + } + + @Test + public void testMutinyBatchingDeleteAllNoBatchSizeParameter(VertxTestContext context) { + test( context, getMutinySessionFactory().withStatelessTransaction( s -> s.insertAll( PIGS ) ) + .invoke( sqlTracker::clear ) + .chain( v -> getMutinySessionFactory().withStatelessTransaction(s -> s + .createQuery( "select p from GuineaPig p", GuineaPig.class ).getResultList() + ) + .invoke( pigs -> sqlTracker.clear() ) + .chain( pigs -> getMutinySessionFactory().withStatelessTransaction( + s -> s.deleteAll( pigs.subList( 0, 2 ).toArray() ) ) + ) + .invoke( () -> { + // We expect only one delete query + assertThat( sqlTracker.getLoggedQueries() ).hasSize( 1 ); + // Parameters are different for different dbs, so we cannot do an exact match + assertThat( sqlTracker.getLoggedQueries().get( 0 ) ).matches( "delete from pig where id=.*" ); + } ) + .chain( () -> getMutinySessionFactory().withStatelessTransaction( s -> s + .createQuery( "select p from GuineaPig p", GuineaPig.class ) + .getResultList() + ) + .invoke( guineaPigs -> assertThat( guineaPigs.size() ).isEqualTo( 4 ) ) + ) ) + ); + } + + @Test + public void testStageBatchingDelete(VertxTestContext context) { + test( context, getSessionFactory().withStatelessTransaction( s -> s.insert( 10, PIGS ) ) + .thenAccept( v -> sqlTracker.clear() ) + .thenCompose( v -> getSessionFactory().withStatelessTransaction( s -> s + .createQuery( "select p from GuineaPig p", GuineaPig.class ).getResultList() + .thenCompose( pigs -> { + sqlTracker.clear(); + return s.delete( 10, pigs.subList( 0, 2 ).toArray() ); + } + ) ) + .thenAccept( vo -> { + // We expect only one delete query + assertThat( sqlTracker.getLoggedQueries() ).hasSize( 1 ); + // Parameters are different for different dbs, so we cannot do an exact match + assertThat( sqlTracker.getLoggedQueries().get( 0 ) ) + .matches( "delete from pig where id=.*" ); + } ) + .thenCompose( vo -> getSessionFactory().withStatelessTransaction( s -> s + .createQuery( "select p from GuineaPig p", GuineaPig.class ) + .getResultList() + ) + .thenAccept( guineaPigs -> assertThat( guineaPigs.size() ).isEqualTo( 4 ) ) + ) ) + ); + } + + @Test + public void testStageBatchingDeleteMultiple(VertxTestContext context) { + test( context, getSessionFactory().withStatelessTransaction( s -> s.insert( 10, PIGS ) ) + .thenAccept( v -> sqlTracker.clear() ) + .thenCompose( v -> getSessionFactory().withStatelessTransaction( s -> s + .createQuery( "select p from GuineaPig p", GuineaPig.class ).getResultList() + .thenCompose( pigs -> { + sqlTracker.clear(); + return s.deleteMultiple( pigs.subList( 0, 2 ) ); + } + ) ) + .thenAccept( vo -> { + // We expect only one delete query + assertThat( sqlTracker.getLoggedQueries() ).hasSize( 1 ); + // Parameters are different for different dbs, so we cannot do an exact match + assertThat( sqlTracker.getLoggedQueries().get( 0 ) ) + .matches( "delete from pig where id=.*" ); + } ) + .thenCompose( vo -> getSessionFactory().withStatelessTransaction( s -> s + .createQuery( "select p from GuineaPig p", GuineaPig.class ) + .getResultList() ) + .thenAccept( guineaPigs -> assertThat( guineaPigs.size() ).isEqualTo( 4 ) ) + ) ) + ); + } + + @Test + public void testStageBatchingDeleteNoBatchSizeParameter(VertxTestContext context) { + test( context, getSessionFactory().withStatelessTransaction( s -> s.insert( 10, PIGS ) ) + .thenAccept( v -> sqlTracker.clear() ) + .thenCompose( v -> getSessionFactory().withStatelessTransaction( s -> s + .createQuery( "select p from GuineaPig p", GuineaPig.class ).getResultList() + .thenCompose( pigs -> { + sqlTracker.clear(); + return s.delete( pigs.subList( 0, 2 ).toArray() ); + } + ) ) + .thenAccept( vo -> { + // We expect only one delete query + assertThat( sqlTracker.getLoggedQueries() ).hasSize( 1 ); + // Parameters are different for different dbs, so we cannot do an exact match + assertThat( sqlTracker.getLoggedQueries().get( 0 ) ) + .matches( "delete from pig where id=.*" ); + } ) + .thenCompose( vo -> getSessionFactory().withStatelessTransaction( s -> s + .createQuery( "select p from GuineaPig p", GuineaPig.class ) + .getResultList() ) + .thenAccept( guineaPigs -> assertThat( guineaPigs.size() ).isEqualTo( 4 ) ) + ) ) + ); + } + + @Test + public void testMutinyBatchingUpdate(VertxTestContext context) { + final String pigOneUpdatedName = "One updated"; + final String pigTwoUpdatedName = "Two updated"; + test( context, getMutinySessionFactory().withStatelessTransaction( s -> s .insertAll( 10, PIGS )) + .invoke( sqlTracker::clear ) + .chain( v -> getMutinySessionFactory().withStatelessTransaction( s -> s + .createQuery( "select p from GuineaPig p order by p.id", GuineaPig.class ) + .getResultList() + .invoke( pigs -> sqlTracker.clear() ) + .chain( pigs -> { + GuineaPig guineaPigOne = pigs.get( 0 ); + guineaPigOne.setName( pigOneUpdatedName ); + GuineaPig guineaPigTwo = pigs.get( 1 ); + guineaPigTwo.setName( pigTwoUpdatedName ); + return s.updateAll( 10, new GuineaPig[] { guineaPigOne, guineaPigTwo } ); + } ) + ) ) + .invoke( () -> { + // We expect only one update query + assertThat( sqlTracker.getLoggedQueries() ).hasSize( 1 ); + // Parameters are different for different dbs, so we cannot do an exact match + assertThat( sqlTracker.getLoggedQueries().get( 0 ) ).matches( "update pig set name=.* where id=.*" ); + } ) + .chain( () -> getMutinySessionFactory().withStatelessTransaction( s -> s + .createQuery( "select p from GuineaPig p order by id", GuineaPig.class ) + .getResultList() + .invoke( guineaPigs -> { + checkPigsAreCorrectlyUpdated( guineaPigs, pigOneUpdatedName, pigTwoUpdatedName ); + } ) ) ) + ); + } + + @Test + public void testMutinyBatchingUpdateMultiple(VertxTestContext context) { + final String pigOneUpdatedName = "One updated"; + final String pigTwoUpdatedName = "Two updated"; + test( context, getMutinySessionFactory().withStatelessTransaction( s -> s .insertAll( 10, PIGS )) + .invoke( sqlTracker::clear ) + .chain( v -> getMutinySessionFactory().withStatelessTransaction( s -> s + .createQuery( "select p from GuineaPig p order by p.id", GuineaPig.class ) + .getResultList() + .invoke( pigs -> sqlTracker.clear() ) + .chain( pigs -> { + GuineaPig guineaPigOne = pigs.get( 0 ); + guineaPigOne.setName( pigOneUpdatedName ); + GuineaPig guineaPigTwo = pigs.get( 1 ); + guineaPigTwo.setName( pigTwoUpdatedName ); + return s.updateMultiple( List.of( guineaPigOne, guineaPigTwo ) ); + } ) + ) ) + .invoke( () -> { + // We expect only one update query + assertThat( sqlTracker.getLoggedQueries() ).hasSize( 1 ); + // Parameters are different for different dbs, so we cannot do an exact match + assertThat( sqlTracker.getLoggedQueries().get( 0 ) ).matches( "update pig set name=.* where id=.*" ); + } ) + .chain( () -> getMutinySessionFactory().withStatelessTransaction( s -> s + .createQuery( "select p from GuineaPig p order by id", GuineaPig.class ) + .getResultList() + .invoke( guineaPigs -> { + checkPigsAreCorrectlyUpdated( guineaPigs, pigOneUpdatedName, pigTwoUpdatedName ); + } ) ) ) + ); + } + + @Test + public void testMutinyBatchingUpdateAllNoBatchSizeParameter(VertxTestContext context) { + final String pigOneUpdatedName = "One updated"; + final String pigTwoUpdatedName = "Two updated"; + test( context, getMutinySessionFactory().withStatelessTransaction( s -> s .insertAll( 10, PIGS )) + .invoke( sqlTracker::clear ) + .chain( v -> getMutinySessionFactory().withStatelessTransaction( s -> s + .createQuery( "select p from GuineaPig p order by p.id", GuineaPig.class ) + .getResultList() + .invoke( pigs -> sqlTracker.clear() ) + .chain( pigs -> { + GuineaPig guineaPigOne = pigs.get( 0 ); + guineaPigOne.setName( pigOneUpdatedName ); + GuineaPig guineaPigTwo = pigs.get( 1 ); + guineaPigTwo.setName( pigTwoUpdatedName ); + return s.updateAll( guineaPigOne, guineaPigTwo ); + } ) + ) ) + .invoke( () -> { + // We expect only one update query + assertThat( sqlTracker.getLoggedQueries() ).hasSize( 1 ); + // Parameters are different for different dbs, so we cannot do an exact match + assertThat( sqlTracker.getLoggedQueries().get( 0 ) ).matches( "update pig set name=.* where id=.*" ); + } ) + .chain( () -> getMutinySessionFactory().withStatelessTransaction( s -> s + .createQuery( "select p from GuineaPig p order by id", GuineaPig.class ) + .getResultList() + .invoke( guineaPigs -> { + checkPigsAreCorrectlyUpdated( guineaPigs, pigOneUpdatedName, pigTwoUpdatedName ); + } ) ) ) + ); + } + + @Test + public void testStageBatchingUpdate(VertxTestContext context) { + final String pigOneUpdatedName = "One updated"; + final String pigTwoUpdatedName = "Two updated"; + test(context, getSessionFactory().withStatelessTransaction( s -> s.insert( 10, PIGS ) ) + .thenAccept( v -> sqlTracker.clear() ) + .thenCompose( v -> getSessionFactory().withStatelessTransaction(s -> s + .createQuery( "select p from GuineaPig p order by p.id", GuineaPig.class ) + .getResultList() + .thenApply( pigs -> { + sqlTracker.clear(); + GuineaPig guineaPigOne = pigs.get( 0 ); + guineaPigOne.setName( pigOneUpdatedName ); + GuineaPig guineaPigTwo = pigs.get( 1 ); + guineaPigTwo.setName( pigTwoUpdatedName ); + return s.update( 10, new GuineaPig[] { guineaPigOne, guineaPigTwo } ); + } ) + ) + .thenAccept( vo -> { + // We expect only one update query + assertThat( sqlTracker.getLoggedQueries() ).hasSize( 1 ); + // Parameters are different for different dbs, so we cannot do an exact match + assertThat( sqlTracker.getLoggedQueries().get( 0 ) ).matches( "update pig set name=.* where id=.*" ); + } ) + .thenCompose( vo -> getSessionFactory().withStatelessTransaction( s -> s + .createQuery( "select p from GuineaPig p order by id", GuineaPig.class ) + .getResultList() + .thenAccept( guineaPigs -> + checkPigsAreCorrectlyUpdated( guineaPigs, pigOneUpdatedName, pigTwoUpdatedName ) + ) + ) ) ) + ); + } + + @Test + public void testStageBatchingUpdateMultiple(VertxTestContext context) { + final String pigOneUpdatedName = "One updated"; + final String pigTwoUpdatedName = "Two updated"; + test(context, getSessionFactory().withStatelessTransaction( s -> s.insert( 10, PIGS ) ) + .thenAccept( v -> sqlTracker.clear() ) + .thenCompose( v -> getSessionFactory().withStatelessTransaction(s -> s + .createQuery( "select p from GuineaPig p order by p.id", GuineaPig.class ) + .getResultList() + .thenApply( pigs -> { + sqlTracker.clear(); + GuineaPig guineaPigOne = pigs.get( 0 ); + guineaPigOne.setName( pigOneUpdatedName ); + GuineaPig guineaPigTwo = pigs.get( 1 ); + guineaPigTwo.setName( pigTwoUpdatedName ); + return s.updateMultiple( List.of( guineaPigOne, guineaPigTwo ) ); + } ) + ) + .thenAccept( vo -> { + // We expect only one update query + assertThat( sqlTracker.getLoggedQueries() ).hasSize( 1 ); + // Parameters are different for different dbs, so we cannot do an exact match + assertThat( sqlTracker.getLoggedQueries().get( 0 ) ).matches( "update pig set name=.* where id=.*" ); + } ) + .thenCompose( vo -> getSessionFactory().withStatelessTransaction( s -> s + .createQuery( "select p from GuineaPig p order by id", GuineaPig.class ) + .getResultList() + .thenAccept( guineaPigs -> + checkPigsAreCorrectlyUpdated( guineaPigs, pigOneUpdatedName, pigTwoUpdatedName ) + ) + ) ) ) + ); + } + + @Test + public void testStageBatchingUpdateNoBatchSizeParameter(VertxTestContext context) { + final String pigOneUpdatedName = "One updated"; + final String pigTwoUpdatedName = "Two updated"; + test(context, getSessionFactory().withStatelessTransaction( s -> s.insert( 10, PIGS ) ) + .thenAccept( v -> sqlTracker.clear() ) + .thenCompose( v -> getSessionFactory().withStatelessTransaction(s -> s + .createQuery( "select p from GuineaPig p order by p.id", GuineaPig.class ) + .getResultList() + .thenApply( pigs -> { + sqlTracker.clear(); + GuineaPig guineaPigOne = pigs.get( 0 ); + guineaPigOne.setName( pigOneUpdatedName ); + GuineaPig guineaPigTwo = pigs.get( 1 ); + guineaPigTwo.setName( pigTwoUpdatedName ); + return s.update( guineaPigOne, guineaPigTwo ); + } ) + ) + .thenAccept( vo -> { + // We expect only one update query + assertThat( sqlTracker.getLoggedQueries() ).hasSize( 1 ); + // Parameters are different for different dbs, so we cannot do an exact match + assertThat( sqlTracker.getLoggedQueries().get( 0 ) ).matches( "update pig set name=.* where id=.*" ); + } ) + .thenCompose( vo -> getSessionFactory().withStatelessTransaction( s -> s + .createQuery( "select p from GuineaPig p order by id", GuineaPig.class ) + .getResultList() + .thenAccept( guineaPigs -> + checkPigsAreCorrectlyUpdated( guineaPigs, pigOneUpdatedName, pigTwoUpdatedName ) + ) + ) ) ) + ); + } + + private static void checkPigsAreCorrectlyUpdated(List guineaPigs, String pigOneUpdatedName, String pigTwoUpdatedName) { + assertThat( guineaPigs.get( 0 ).getName() ).isEqualTo( pigOneUpdatedName ); + assertThat( guineaPigs.get( 1 ).getName() ).isEqualTo( pigTwoUpdatedName ); + assertThat( guineaPigs.get( 2 ).getName() ).isEqualTo( PIG_THREE_NAME ); + assertThat( guineaPigs.get( 3 ).getName() ).isEqualTo( PIG_FOUR_NAME ); + assertThat( guineaPigs.get( 4 ).getName() ).isEqualTo( PIG_FIVE_NAME ); + assertThat( guineaPigs.get( 5 ).getName() ).isEqualTo( PIG_SIX_NAME ); + } + + @Entity(name = "GuineaPig") + @Table(name = "pig") + public static class GuineaPig { + @Id + private Integer id; + private String name; + + public GuineaPig() { + } + + public GuineaPig(Integer id, String name) { + this.id = id; + this.name = name; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String toString() { + return id + ": " + name; + } + + @Override + public boolean equals(Object o) { + if ( this == o ) { + return true; + } + if ( o == null || getClass() != o.getClass() ) { + return false; + } + GuineaPig guineaPig = (GuineaPig) o; + return Objects.equals( name, guineaPig.name ); + } + + @Override + public int hashCode() { + return Objects.hash( name ); + } + } +} diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/BatchingConnectionTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/BatchingConnectionTest.java index 0ce3d2e9d..75d90375e 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/BatchingConnectionTest.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/BatchingConnectionTest.java @@ -145,6 +145,75 @@ public void testBatching(VertxTestContext context) { ); } + @Test + public void testBatchingWithStateless(VertxTestContext context) { + final GuineaPig[] pigs = { + new GuineaPig( 11, "One" ), + new GuineaPig( 22, "Two" ), + new GuineaPig( 33, "Three" ), + new GuineaPig( 44, "Four" ), + new GuineaPig( 55, "Five" ), + new GuineaPig( 66, "Six" ), + }; + test( context, getMutinySessionFactory() + .withStatelessTransaction( s -> s.insertAll( 10, pigs ) ) + .invoke( () -> { + // We expect only one insert query + assertThat( sqlTracker.getLoggedQueries() ).hasSize( 1 ); + // Parameters are different for different dbs, so we cannot do an exact match + assertThat( sqlTracker.getLoggedQueries().get( 0 ) ) + .matches("insert into pig \\(name,version,id\\) values (.*)" ); + sqlTracker.clear(); + } ) + ); + } + + @Test + public void testMutinyInsertAllWithStateless(VertxTestContext context) { + final GuineaPig[] pigs = { + new GuineaPig( 11, "One" ), + new GuineaPig( 22, "Two" ), + new GuineaPig( 33, "Three" ), + new GuineaPig( 44, "Four" ), + new GuineaPig( 55, "Five" ), + new GuineaPig( 66, "Six" ), + }; + test( context, getMutinySessionFactory() + .withStatelessTransaction( s -> s.insertAll( pigs ) ) + .invoke( () -> { + // We expect only 1 insert query, despite hibernate.jdbc.batch_size is set to 5, insertAll by default use the pigs.length as batch size + assertThat( sqlTracker.getLoggedQueries() ).hasSize( 1 ); + // Parameters are different for different dbs, so we cannot do an exact match + assertThat( sqlTracker.getLoggedQueries().get( 0 ) ) + .matches("insert into pig \\(name,version,id\\) values (.*)" ); + sqlTracker.clear(); + } ) + ); + } + + @Test + public void testStageInsertWithStateless(VertxTestContext context) { + final GuineaPig[] pigs = { + new GuineaPig( 11, "One" ), + new GuineaPig( 22, "Two" ), + new GuineaPig( 33, "Three" ), + new GuineaPig( 44, "Four" ), + new GuineaPig( 55, "Five" ), + new GuineaPig( 66, "Six" ), + }; + test( context, getSessionFactory() + .withStatelessTransaction( s -> s.insert( pigs ) ) + .thenAccept( v -> { + // We expect only 1 insert query, despite hibernate.jdbc.batch_size is set to 5, insertAll by default use the pigs.length as batch size + assertThat( sqlTracker.getLoggedQueries() ).hasSize( 1 ); + // Parameters are different for different dbs, so we cannot do an exact match + assertThat( sqlTracker.getLoggedQueries().get( 0 ) ) + .matches("insert into pig \\(name,version,id\\) values (.*)" ); + sqlTracker.clear(); + } ) + ); + } + @Test public void testBatchingConnection(VertxTestContext context) { test( context, openSession()