Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ public final class PersistRequestBean<T> extends PersistRequest implements BeanP
*/
private List<SaveMany> saveMany;
private InsertOptions insertOptions;
private BeanProperty[] dirtyGenerated;

public PersistRequestBean(SpiEbeanServer server, T bean, Object parentBean, BeanManager<T> mgr, SpiTransaction t,
PersistExecute persistExecute, PersistRequest.Type type, int flags) {
Expand Down Expand Up @@ -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()) {
Expand All @@ -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.
*/
Expand Down Expand Up @@ -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");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() {

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -215,4 +220,5 @@ private void closeInputStreams() {
inputStreams = null;
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
Expand Down
41 changes: 41 additions & 0 deletions ebean-test/src/test/java/org/tests/cascade/TestDeleteRestrict.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {
Expand Down Expand Up @@ -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();
}
}
}