diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/PersistRequestBean.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/PersistRequestBean.java index 93358d0c6d..7716f61653 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/core/PersistRequestBean.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/PersistRequestBean.java @@ -126,6 +126,7 @@ public final class PersistRequestBean extends PersistRequest implements BeanP */ private List saveMany; private InsertOptions insertOptions; + private BeanProperty[] dirtyGenerated; public PersistRequestBean(SpiEbeanServer server, T bean, Object parentBean, BeanManager mgr, SpiTransaction t, PersistExecute persistExecute, PersistRequest.Type type, int flags) { @@ -262,6 +263,7 @@ private void initGeneratedProperties() { } private void onUpdateGeneratedProperties() { + dirtyGenerated = beanDescriptor.propertiesGenUpdate(); for (BeanProperty prop : beanDescriptor.propertiesGenUpdate()) { GeneratedProperty generatedProperty = prop.generatedProperty(); if (prop.isVersion()) { @@ -282,20 +284,32 @@ private void onUpdateGeneratedProperties() { } } - private void onFailedUpdateUndoGeneratedProperties() { - for (BeanProperty prop : beanDescriptor.propertiesGenUpdate()) { - Object oldVal = intercept.origValue(prop.propertyIndex()); - prop.setValue(entityBean, oldVal); - } - } - private void onInsertGeneratedProperties() { + dirtyGenerated = beanDescriptor.propertiesGenInsert(); for (BeanProperty prop : beanDescriptor.propertiesGenInsert()) { Object value = prop.generatedProperty().getInsertValue(prop, entityBean, now()); prop.setValueChanged(entityBean, value); } } + /** + * Undos the update of generated properties. + */ + @Override + public void undo() { + if (dirtyGenerated != null) { + // Do an undo once, and undo only modified properties. + for (BeanProperty prop : dirtyGenerated) { + if (!prop.isVersion() || isLoadedProperty(prop)) { + Object oldVal = intercept.origValue(prop.propertyIndex()); + prop.setValue(entityBean, oldVal); + } + } + dirtyGenerated = null; + } + } + + /** * If using batch on cascade flush if required. */ @@ -809,7 +823,6 @@ public void setBoundId(Object idValue) { public void checkRowCount(int rowCount) { if (rowCount != 1 && rowCount != Statement.SUCCESS_NO_INFO) { if (ConcurrencyMode.VERSION == concurrencyMode) { - onFailedUpdateUndoGeneratedProperties(); throw new OptimisticLockException("Data has changed. updated row count " + rowCount, null, bean); } else if (rowCount == 0 && type == Type.UPDATE) { throw new EntityNotFoundException("No rows updated"); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/persist/BatchPostExecute.java b/ebean-core/src/main/java/io/ebeaninternal/server/persist/BatchPostExecute.java index c17be92d15..b7827cb7da 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/persist/BatchPostExecute.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/persist/BatchPostExecute.java @@ -45,4 +45,11 @@ public interface BatchPostExecute { * Add timing metrics for batch persist. */ void addTimingBatch(long startNanos, int batch); + + /** + * Tries to undo the request. + */ + default void undo() { + + } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/persist/BatchedPstmt.java b/ebean-core/src/main/java/io/ebeaninternal/server/persist/BatchedPstmt.java index e39f79629d..7263653542 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/persist/BatchedPstmt.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/persist/BatchedPstmt.java @@ -117,7 +117,7 @@ public void executeBatch(boolean getGeneratedKeys) throws SQLException { } postExecute(); addTimingMetrics(); - list.clear(); + list.clear(); // CHECKME: This list may cause problems when undo is done on multiple batches. transaction.profileEvent(this); } @@ -171,6 +171,11 @@ private void executeAndCheckRowCounts() throws SQLException { } } + public void undo() { + list.forEach(BatchPostExecute::undo); + } + + private void getGeneratedKeys() throws SQLException { if (DB2_HACK.getGeneratedKeys(pstmt, list)) { return; @@ -215,4 +220,5 @@ private void closeInputStreams() { inputStreams = null; } } + } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/persist/BatchedPstmtHolder.java b/ebean-core/src/main/java/io/ebeaninternal/server/persist/BatchedPstmtHolder.java index 46b6673f75..5acb21d51b 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/persist/BatchedPstmtHolder.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/persist/BatchedPstmtHolder.java @@ -119,6 +119,7 @@ public void flush(boolean getGeneratedKeys, boolean reset) throws BatchedSqlExce loadBack(copyMap); } } catch (BatchedSqlException e) { + copy.forEach(BatchedPstmt::undo); closeStatements(copy); throw e; } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/persist/dml/DmlHandler.java b/ebean-core/src/main/java/io/ebeaninternal/server/persist/dml/DmlHandler.java index f7f2f669fd..e1142aef81 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/persist/dml/DmlHandler.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/persist/dml/DmlHandler.java @@ -8,8 +8,8 @@ import io.ebeaninternal.server.persist.BatchedPstmtHolder; import io.ebeaninternal.server.persist.dmlbind.BindableRequest; import io.ebeaninternal.server.bind.DataBind; - import jakarta.persistence.OptimisticLockException; + import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; @@ -86,6 +86,9 @@ public final int executeNoBatch() throws SQLException { final long startNanos = System.nanoTime(); try { return execute(); + } catch (Throwable t) { + persistRequest.undo(); + throw t; } finally { persistRequest.addTimingNoBatch(startNanos); } diff --git a/ebean-test/src/test/java/org/tests/cascade/TestDeleteRestrict.java b/ebean-test/src/test/java/org/tests/cascade/TestDeleteRestrict.java new file mode 100644 index 0000000000..379432218c --- /dev/null +++ b/ebean-test/src/test/java/org/tests/cascade/TestDeleteRestrict.java @@ -0,0 +1,41 @@ +package org.tests.cascade; + +import io.ebean.DataIntegrityException; +import io.ebean.xtest.BaseTestCase; +import org.junit.jupiter.api.Test; +import org.tests.model.basic.Customer; +import org.tests.model.basic.Order; +import org.tests.model.basic.ResetBasicData; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * @author Roland Praml, Foconis Analytics GmbH + */ +public class TestDeleteRestrict extends BaseTestCase { + + @Test + void test() { + ResetBasicData.reset(); + + Customer customer = new Customer(); + customer.setName("Roland"); + server().save(customer); + + Order order = new Order(); + order.setCustomer(customer); + server().save(order); + + assertThat(customer.getVersion()).isEqualTo(1L); + assertThatThrownBy(() -> server().delete(customer)).isInstanceOf(DataIntegrityException.class); + assertThat(customer.getVersion()).isEqualTo(1L); + + customer.setName("Roland-inactive"); + server().save(customer); + + // cleanup + server().delete(order); + server().delete(customer); + } +} diff --git a/ebean-test/src/test/java/org/tests/insert/TestInsertDuplicateKey.java b/ebean-test/src/test/java/org/tests/insert/TestInsertDuplicateKey.java index b894d4de6a..f6b63fc703 100644 --- a/ebean-test/src/test/java/org/tests/insert/TestInsertDuplicateKey.java +++ b/ebean-test/src/test/java/org/tests/insert/TestInsertDuplicateKey.java @@ -2,6 +2,7 @@ import io.ebean.DB; import io.ebean.DuplicateKeyException; +import io.ebean.Transaction; import io.ebean.annotation.Transactional; import io.ebean.xtest.BaseTestCase; import org.junit.jupiter.api.BeforeEach; @@ -13,6 +14,7 @@ import java.util.List; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; public class TestInsertDuplicateKey extends BaseTestCase { @@ -100,4 +102,61 @@ private void insertTheBatch_duplicateKey_catchAndContinue() { doc0.setBody("insertTheBatch_duplicateKey_catchAndContinue-1"); doc0.save(); } + + + @Test + public void insert_duplicateKey_retry() { + Document doc1 = new Document(); + doc1.setTitle("Key1ABC"); + doc1.setBody("one"); + doc1.save(); + + Document doc2 = new Document(); + doc2.setTitle("Key1ABC"); + doc2.setBody("clashes with doc1"); + Long version = doc2.getVersion(); + assertThrows(DuplicateKeyException.class, doc2::save); + assertEquals(version, doc2.getVersion()); + + doc2.setTitle("Key1ABCD"); + + doc2.save(); + + doc1.setTitle("Key1ABCD"); + assertThrows(DuplicateKeyException.class, doc1::save); + doc1.setTitle("Key1ABCDE"); + doc1.save(); + } + + @Test + public void insert_duplicateKey_retryWithBatch() { + Document doc1 = new Document(); + doc1.setTitle("Key2ABC"); + doc1.setBody("one"); + doc1.save(); + + Document doc2 = new Document(); + doc2.setTitle("Key2ABC"); + doc2.setBody("clashes with doc1"); + Long version = doc2.getVersion(); + try (Transaction tx = DB.beginTransaction()) { + tx.setBatchMode(true); + doc2.save(); + assertThrows(DuplicateKeyException.class, tx::commit); + } + assertEquals(version, doc2.getVersion()); + + doc2.setTitle("Key2ABCD"); + + doc2.save(); + + doc1.setTitle("Key2ABCD"); + assertThrows(DuplicateKeyException.class, doc1::save); + doc1.setTitle("Key2ABCDE"); + try (Transaction tx = DB.beginTransaction()) { + tx.setBatchMode(true); + doc1.save(); + tx.commit(); + } + } }