diff --git a/hibernate-core/src/main/java/org/hibernate/jdbc/Expectation.java b/hibernate-core/src/main/java/org/hibernate/jdbc/Expectation.java index bd595d58f0a6..941eb5e65d97 100644 --- a/hibernate-core/src/main/java/org/hibernate/jdbc/Expectation.java +++ b/hibernate-core/src/main/java/org/hibernate/jdbc/Expectation.java @@ -181,7 +181,7 @@ protected int expectedRowCount() { /** * Essentially identical to {@link RowCount} except that the row count - * is obtained via an output parameter of a {@link CallableStatement + * is obtained via an output parameter of a {@linkplain CallableStatement * stored procedure}. *

* Statement batching is disabled when {@code OutParameter} is used. diff --git a/hibernate-core/src/main/java/org/hibernate/sql/model/jdbc/DeleteOrUpsertOperation.java b/hibernate-core/src/main/java/org/hibernate/sql/model/jdbc/DeleteOrUpsertOperation.java index e0ad8a7a438e..c3fea08c8745 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/model/jdbc/DeleteOrUpsertOperation.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/model/jdbc/DeleteOrUpsertOperation.java @@ -16,6 +16,7 @@ import org.hibernate.engine.jdbc.mutation.spi.Binding; import org.hibernate.engine.jdbc.spi.JdbcServices; import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.jdbc.Expectation; import org.hibernate.persister.entity.mutation.EntityMutationTarget; import org.hibernate.persister.entity.mutation.EntityTableMapping; import org.hibernate.persister.entity.mutation.UpdateValuesAnalysis; @@ -40,6 +41,8 @@ public class DeleteOrUpsertOperation implements SelfExecutingUpdateOperation { private final OptionalTableUpdate optionalTableUpdate; + private final Expectation expectation = new Expectation.RowCount(); + public DeleteOrUpsertOperation( EntityMutationTarget mutationTarget, EntityTableMapping tableMapping, @@ -123,6 +126,16 @@ private void performDelete(JdbcValueBindings jdbcValueBindings, SharedSessionCon final int rowCount = session.getJdbcCoordinator().getResultSetReturn() .executeUpdate( upsertDeleteStatement, statementDetails.getSqlString() ); MODEL_MUTATION_LOGGER.tracef( "`%s` rows upsert-deleted from `%s`", rowCount, tableMapping.getTableName() ); + try { + expectation.verifyOutcome( rowCount, upsertDeleteStatement, -1, statementDetails.getSqlString() ); + } + catch (SQLException e) { + throw jdbcServices.getSqlExceptionHelper().convert( + e, + "Unable to verify outcome for upsert delete", + statementDetails.getSqlString() + ); + } } finally { statementDetails.releaseStatement( session ); @@ -182,12 +195,23 @@ private void performUpsert(JdbcValueBindings jdbcValueBindings, SharedSessionCon final var statementDetails = statementGroup.resolvePreparedStatementDetails( tableMapping.getTableName() ); try { final PreparedStatement updateStatement = statementDetails.resolveStatement(); - session.getJdbcServices().getSqlStatementLogger().logStatement( statementDetails.getSqlString() ); + final JdbcServices jdbcServices = session.getJdbcServices(); + jdbcServices.getSqlStatementLogger().logStatement( statementDetails.getSqlString() ); jdbcValueBindings.beforeStatement( statementDetails ); final int rowCount = session.getJdbcCoordinator().getResultSetReturn() .executeUpdate( updateStatement, statementDetails.getSqlString() ); MODEL_MUTATION_LOGGER.tracef( "`%s` rows upserted into `%s`", rowCount, tableMapping.getTableName() ); + try { + expectation.verifyOutcome( rowCount, updateStatement, -1, statementDetails.getSqlString() ); + } + catch (SQLException e) { + throw jdbcServices.getSqlExceptionHelper().convert( + e, + "Unable to verify outcome for upsert", + statementDetails.getSqlString() + ); + } } finally { statementDetails.releaseStatement( session ); diff --git a/hibernate-core/src/main/java/org/hibernate/sql/model/jdbc/MergeOperation.java b/hibernate-core/src/main/java/org/hibernate/sql/model/jdbc/MergeOperation.java index 2f98e88e0d5a..0e1bf7d0dd94 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/model/jdbc/MergeOperation.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/model/jdbc/MergeOperation.java @@ -23,7 +23,7 @@ public MergeOperation( MutationTarget mutationTarget, String sql, List parameterBinders) { - super( tableDetails, mutationTarget, sql, false, Expectation.None.INSTANCE, parameterBinders ); + super( tableDetails, mutationTarget, sql, false, new Expectation.RowCount(), parameterBinders ); } @Override @@ -31,5 +31,4 @@ public MutationType getMutationType() { return MutationType.UPDATE; } - } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/model/jdbc/OptionalTableUpdateOperation.java b/hibernate-core/src/main/java/org/hibernate/sql/model/jdbc/OptionalTableUpdateOperation.java index 79d67eff5870..6dcf099caa6b 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/model/jdbc/OptionalTableUpdateOperation.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/model/jdbc/OptionalTableUpdateOperation.java @@ -12,6 +12,7 @@ import java.util.Locale; import java.util.Objects; +import org.hibernate.StaleStateException; import org.hibernate.engine.jdbc.mutation.JdbcValueBindings; import org.hibernate.engine.jdbc.mutation.ParameterUsage; import org.hibernate.engine.jdbc.mutation.internal.JdbcValueDescriptorImpl; @@ -23,6 +24,7 @@ import org.hibernate.engine.jdbc.spi.JdbcServices; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.exception.ConstraintViolationException; import org.hibernate.internal.util.collections.CollectionHelper; import org.hibernate.jdbc.Expectation; import org.hibernate.persister.entity.mutation.EntityMutationTarget; @@ -48,6 +50,7 @@ import org.hibernate.sql.model.internal.TableUpdateCustomSql; import org.hibernate.sql.model.internal.TableUpdateStandard; +import static org.hibernate.exception.ConstraintViolationException.ConstraintKind.UNIQUE; import static org.hibernate.sql.model.ModelMutationLogging.MODEL_MUTATION_LOGGER; /** @@ -149,7 +152,16 @@ public void performMutation( "Upsert update altered no rows - inserting : %s", tableMapping.getTableName() ); - performInsert( jdbcValueBindings, session ); + try { + performInsert( jdbcValueBindings, session ); + } + catch (ConstraintViolationException cve) { + throw cve.getKind() == UNIQUE + // assume it was the primary key constraint which was violated, + // due to a new version of the row existing in the database + ? new StaleStateException( mutationTarget.getRolePath(), cve ) + : cve; + } } } } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/model/jdbc/UpsertOperation.java b/hibernate-core/src/main/java/org/hibernate/sql/model/jdbc/UpsertOperation.java index a1a262c36240..6ba371d381c8 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/model/jdbc/UpsertOperation.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/model/jdbc/UpsertOperation.java @@ -23,7 +23,7 @@ public UpsertOperation( MutationTarget mutationTarget, String sql, List parameterBinders) { - super( tableDetails, mutationTarget, sql, false, Expectation.None.INSTANCE, parameterBinders ); + super( tableDetails, mutationTarget, sql, false, new Expectation.RowCount(), parameterBinders ); } @Override @@ -31,5 +31,4 @@ public MutationType getMutationType() { return MutationType.UPDATE; } - } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/stateless/UpsertVersionedTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/stateless/UpsertVersionedTest.java index 1438f4483a54..0c2b71a64a60 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/stateless/UpsertVersionedTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/stateless/UpsertVersionedTest.java @@ -7,17 +7,21 @@ import jakarta.persistence.Entity; import jakarta.persistence.Id; import jakarta.persistence.Version; +import org.hibernate.StaleStateException; import org.hibernate.testing.orm.junit.DomainModel; import org.hibernate.testing.orm.junit.SessionFactory; import org.hibernate.testing.orm.junit.SessionFactoryScope; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; @SessionFactory @DomainModel(annotatedClasses = UpsertVersionedTest.Record.class) public class UpsertVersionedTest { + @Test void test(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncate(); scope.inStatelessTransaction(s-> { s.upsert(new Record(123L,null,"hello earth")); s.upsert(new Record(456L,2L,"hello mars")); @@ -41,6 +45,29 @@ public class UpsertVersionedTest { assertEquals( "goodbye mars", s.get( Record.class,456L).message ); }); } + + @Test void testStaleUpsert(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncate(); + scope.inStatelessTransaction( s -> { + s.insert(new Record(789L, 1L, "hello world")); + } ); + scope.inStatelessTransaction( s -> { + s.upsert(new Record(789L, 1L, "hello mars")); + } ); + try { + scope.inStatelessTransaction( s -> { + s.upsert(new Record( 789L, 1L, "hello venus")); + } ); + fail(); + } + catch (StaleStateException sse) { + //expected + } + scope.inStatelessTransaction( s-> { + assertEquals( "hello mars", s.get(Record.class,789L).message ); + } ); + } + @Entity(name = "Record") static class Record { @Id Long id;