mirror of
https://github.com/ebean-orm/ebean.git
synced 2024-04-21 10:51:47 +00:00
Merge pull request #2962 from ebean-orm/feature/2961
#2961 - Followup for #2952 - @OneToMany + orphanRemoval + @SoftDelete + non-BeanCollection collection results in hard deletes
This commit is contained in:
@@ -35,8 +35,8 @@ final class SaveManyBeans extends SaveManyBase {
|
||||
private final boolean untouchedBeanCollection;
|
||||
private final Collection<?> collection;
|
||||
private final boolean hasOrderColumn;
|
||||
private final boolean forcedUpdate;
|
||||
private int sortOrder;
|
||||
private boolean insertAllChildren;
|
||||
private boolean forceOrphanRemoval;
|
||||
|
||||
SaveManyBeans(DefaultPersister persister, boolean insertedParent, BeanPropertyAssocMany<?> many, EntityBean parentBean, PersistRequestBean<?> request) {
|
||||
@@ -50,6 +50,7 @@ final class SaveManyBeans extends SaveManyBase {
|
||||
this.untouchedBeanCollection = untouchedBeanCollection();
|
||||
this.collection = cascade ? BeanCollectionUtil.getActualEntries(value) : null;
|
||||
this.hasOrderColumn = many.hasOrderColumn();
|
||||
this.forcedUpdate = request.isForcedUpdate();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -128,10 +129,8 @@ final class SaveManyBeans extends SaveManyBase {
|
||||
targetDescriptor.preAllocateIds(collection.size());
|
||||
}
|
||||
if (forcedUpdateOrphanRemoval()) {
|
||||
// collect the Id's (to exclude from deleteManyDetails)
|
||||
List<Object> detailIds = collectIds(collection, targetDescriptor, isMap);
|
||||
// deleting missing children - children not in our collected detailIds
|
||||
persister.deleteManyDetails(transaction, many.descriptor(), parentBean, many, detailIds, deleteMode);
|
||||
// deleting orphans, anything not in our detailsIds
|
||||
persister.deleteManyDetails(transaction, many.descriptor(), parentBean, many, detailIds(), deleteMode);
|
||||
}
|
||||
transaction.depth(+1);
|
||||
saveAllBeans(orderColumn);
|
||||
@@ -142,7 +141,7 @@ final class SaveManyBeans extends SaveManyBase {
|
||||
}
|
||||
|
||||
private boolean forcedUpdateOrphanRemoval() {
|
||||
return !insertedParent && many.isOrphanRemoval() && (forceOrphanRemoval || request.isForcedUpdate());
|
||||
return !insertedParent && many.isOrphanRemoval() && (forceOrphanRemoval || forcedUpdate);
|
||||
}
|
||||
|
||||
private void saveAllBeans(final BeanProperty orderColumn) {
|
||||
@@ -178,10 +177,6 @@ final class SaveManyBeans extends SaveManyBase {
|
||||
skipSavingThisBean = false;
|
||||
// set the parent bean to detailBean
|
||||
many.setJoinValuesToChild(parentBean, detail, mapKeyValue);
|
||||
} else if (insertAllChildren) {
|
||||
ebi.setNew();
|
||||
skipSavingThisBean = false;
|
||||
many.setJoinValuesToChild(parentBean, detail, mapKeyValue);
|
||||
} else {
|
||||
skipSavingThisBean = saveRecurseSkippable;
|
||||
}
|
||||
@@ -216,12 +211,10 @@ final class SaveManyBeans extends SaveManyBase {
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect the Id values of the details to remove 'missing children' for stateless updates.
|
||||
* Return the Id values of beans we know are being updated (any others are orphans)
|
||||
*/
|
||||
private List<Object> collectIds(Collection<?> collection, BeanDescriptor<?> targetDescriptor, boolean isMap) {
|
||||
List<Object> detailIds = new ArrayList<>();
|
||||
// stateless update with deleteMissingChildren so first
|
||||
// collect the Id values to remove the 'missing children'
|
||||
private List<Object> detailIds() {
|
||||
final var detailIds = new ArrayList<>();
|
||||
for (Object detailBean : collection) {
|
||||
if (isMap) {
|
||||
detailBean = ((Map.Entry<?, ?>) detailBean).getValue();
|
||||
@@ -229,8 +222,8 @@ final class SaveManyBeans extends SaveManyBase {
|
||||
if (detailBean instanceof EntityBean) {
|
||||
Object id = targetDescriptor.id(detailBean);
|
||||
if (!isNullOrZero(id)) {
|
||||
if (!forceOrphanRemoval || !((EntityBean)detailBean)._ebean_getIntercept().isNew()) {
|
||||
// remember the Id (other details not in the collection) will be removed
|
||||
if (forcedUpdate || !((EntityBean) detailBean)._ebean_getIntercept().isNew()) {
|
||||
// Id of bean that will be updated, exclude it from orphan removal
|
||||
detailIds.add(id);
|
||||
}
|
||||
}
|
||||
@@ -264,8 +257,7 @@ final class SaveManyBeans extends SaveManyBase {
|
||||
}
|
||||
|
||||
private void saveAssocManyIntersection(boolean queue) {
|
||||
boolean forcedUpdate = request.isForcedUpdate();
|
||||
boolean vanillaCollection = !(value instanceof BeanCollection<?>);
|
||||
final boolean vanillaCollection = !(value instanceof BeanCollection<?>);
|
||||
if (vanillaCollection || forcedUpdate) {
|
||||
// delete all intersection rows and then treat all
|
||||
// beans in the collection as additions
|
||||
@@ -324,7 +316,7 @@ final class SaveManyBeans extends SaveManyBase {
|
||||
CoreLog.log.log(System.Logger.Level.WARNING, m);
|
||||
} else {
|
||||
if (!many.hasImportedId(otherBean)) {
|
||||
throw new PersistenceException("ManyToMany bean " + otherBean + " does not have an Id value.");
|
||||
throw new PersistenceException("ManyToMany bean does not have an Id value? " + otherBean);
|
||||
} else {
|
||||
// build a intersection row for 'insert'
|
||||
IntersectionRow intRow = many.buildManyToManyMapBean(parentBean, otherBean, publish);
|
||||
@@ -347,10 +339,7 @@ final class SaveManyBeans extends SaveManyBase {
|
||||
return;
|
||||
}
|
||||
if (!(value instanceof BeanCollection<?>)) {
|
||||
if (!forcedUpdateOrphanRemoval() && (!insertedParent && cascade && isChangedProperty())) {
|
||||
persister.addToFlushQueue(many.deleteByParentId(request.beanId(), null), transaction, 0);
|
||||
insertAllChildren = true;
|
||||
}
|
||||
forceOrphanRemoval = !insertedParent && isChangedProperty();
|
||||
} else {
|
||||
BeanCollection<?> c = (BeanCollection<?>) value;
|
||||
Set<?> modifyRemovals = c.getModifyRemovals();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.tests.cascade;
|
||||
|
||||
import io.ebean.DB;
|
||||
import io.ebean.test.LoggedSql;
|
||||
import io.ebean.xtest.BaseTestCase;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@@ -9,6 +10,7 @@ import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
class TestOrphanCollectionReplacement extends BaseTestCase {
|
||||
@@ -41,7 +43,16 @@ class TestOrphanCollectionReplacement extends BaseTestCase {
|
||||
updatedRoles.addAll(List.of(role));
|
||||
fetchedParent.setChildren(updatedRoles);
|
||||
|
||||
LoggedSql.start();
|
||||
DB.save(fetchedParent);
|
||||
var sql = LoggedSql.stop();
|
||||
if (isH2() || isPostgresCompatible()) { // using deleted=true vs deleted=1
|
||||
assertThat(sql).hasSize(4);
|
||||
assertThat(sql.get(0)).contains("update coone_many set deleted=true where coone_id = ? and not ( id in (?) )");
|
||||
assertThat(sql.get(1)).contains(" -- bind(");
|
||||
assertThat(sql.get(2)).contains("insert into coone_many (coone_id, name, deleted) values (?,?,?)");
|
||||
assertThat(sql.get(3)).contains(" -- bind(");
|
||||
}
|
||||
}
|
||||
|
||||
COOne fetchedUser2 = DB.find(COOne.class, parentId);
|
||||
|
||||
+56
-7
@@ -137,7 +137,14 @@ class TestOneToManyStatelessUpdateResultsInSoftDelete extends BaseTestCase {
|
||||
assertThat(goodsAfterInsert.getWorkflowEntity().getOperations()).hasSize(1);
|
||||
goodsAfterInsert.getWorkflowEntity().setOperations(List.of());
|
||||
|
||||
LoggedSql.start();
|
||||
DB.save(goodsAfterInsert);
|
||||
var sql = LoggedSql.collect();
|
||||
if (isH2() || isPostgresCompatible()) { // using deleted=true vs deleted=1
|
||||
assertThat(sql).hasSize(2);
|
||||
assertThat(sql.get(0)).contains("update workflow_operation_entity set deleted=true where workflow_id = ?");
|
||||
}
|
||||
|
||||
assertThat(goodsAfterInsert.getWorkflowEntity().getOperations()).isEmpty();
|
||||
assertThat(DB.find(GoodsEntity.class, goods.getId()).getWorkflowEntity().getOperations()).isEmpty();
|
||||
|
||||
@@ -154,12 +161,27 @@ class TestOneToManyStatelessUpdateResultsInSoftDelete extends BaseTestCase {
|
||||
|
||||
// Using save() throws io.ebean.DuplicateKeyException: Error when batch flush on sql: insert into workflow_entity ...
|
||||
// Must be an update() and not save() for this to be a "stateless update"
|
||||
LoggedSql.collect();
|
||||
DB.update(goodsStateless);
|
||||
sql = LoggedSql.collect();
|
||||
assertThat(sql).isNotEmpty();
|
||||
if (isH2() || isPostgresCompatible()) { // using deleted=true vs deleted=1
|
||||
assertThat(sql).hasSize(7);
|
||||
assertThat(sql.get(0)).contains("update workflow_entity set when_modified=? where id=?");
|
||||
assertThat(sql.get(1)).contains(" -- bind(");
|
||||
assertThat(sql.get(2)).contains("update workflow_operation_entity set deleted=true where workflow_id = ?");
|
||||
assertThat(sql.get(3)).contains(" -- bind(");
|
||||
assertThat(sql.get(4)).contains("insert into workflow_operation_entity (name, version, when_created, when_modified");
|
||||
assertThat(sql.get(5)).contains(" -- bind(");
|
||||
assertThat(sql.get(6)).contains("update goods_entity set when_modified=?, workflow_entity_id=? where id=?");
|
||||
}
|
||||
|
||||
var ops = workflow.getOperations();
|
||||
// shouldn't contain deleted operations
|
||||
assertThat(ops).hasSize(1);
|
||||
assertThat(goodsStateless.getWorkflowEntity().getOperations().get(0).getId()).isNotEqualTo(operation1.getId());
|
||||
|
||||
LoggedSql.stop();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -194,17 +216,23 @@ class TestOneToManyStatelessUpdateResultsInSoftDelete extends BaseTestCase {
|
||||
// uncommenting this lines makes the test pass
|
||||
//assertThat(goodsStateless.getWorkflowEntity().getOperations().size()).isEqualTo(0);
|
||||
|
||||
var sql = LoggedSql.stop();
|
||||
sql.forEach(System.out::println);
|
||||
|
||||
System.out.println("BEFORE TRY");
|
||||
LoggedSql.start();
|
||||
var sql = LoggedSql.collect();
|
||||
if (isH2() || isPostgresCompatible()) { // using deleted=true vs deleted=1
|
||||
assertThat(sql).hasSize(5);
|
||||
assertThat(sql.get(0)).contains("update workflow_entity set when_modified=? where id=?");
|
||||
assertThat(sql.get(1)).contains(" -- bind(");
|
||||
assertThat(sql.get(2)).contains("update workflow_operation_entity set deleted=true where workflow_id = ?");
|
||||
assertThat(sql.get(3)).contains(" -- bind(");
|
||||
assertThat(sql.get(4)).contains("update goods_entity set when_modified=?, workflow_entity_id=? where id=?");
|
||||
}
|
||||
|
||||
try (var writer = new StringWriter()) {
|
||||
var mapper = new ObjectMapper();
|
||||
mapper.writeValue(writer, goodsStateless);
|
||||
sql = LoggedSql.stop();
|
||||
sql.forEach(System.out::println);
|
||||
// queries fired to load the object graph for writing as json
|
||||
assertThat(sql).hasSize(7);
|
||||
sql.forEach(s -> assertThat(s).startsWith("select "));
|
||||
/*
|
||||
select t0.id, t0.name, t0.workflow_entity_id, t0.version, t0.when_created, t0.when_modified from goods_entity t0 where t0.id = ?; --bind(4, ) --micros(161)
|
||||
select t0.id, t0.name, t0.version, t0.when_created, t0.when_modified, t0.created_by, t0.updated_by, t0.workflow_entity_id from goods_entity t0 where t0.id = ?; --bind(4, ) --micros(525)
|
||||
@@ -227,7 +255,8 @@ class TestOneToManyStatelessUpdateResultsInSoftDelete extends BaseTestCase {
|
||||
|
||||
writer.flush();
|
||||
var serialized = writer.toString();
|
||||
System.out.println(serialized);
|
||||
// System.out.println(serialized);
|
||||
assertThat(serialized).isNotEmpty();
|
||||
var readGoods = mapper.readValue(writer.toString(), GoodsEntity.class);
|
||||
assertThat(readGoods.getWorkflowEntity().getOperations()).hasSize(0);
|
||||
}
|
||||
@@ -264,7 +293,16 @@ class TestOneToManyStatelessUpdateResultsInSoftDelete extends BaseTestCase {
|
||||
logger.error("Insert instead update", e);
|
||||
}
|
||||
var sql = LoggedSql.collect();
|
||||
assertThat(sql).isNotEmpty();
|
||||
assertThat(sql.get(0)).contains("delete from attachment where goods_entity_id = ?");
|
||||
assertThat(sql.get(1)).contains(" -- bind(");
|
||||
assertThat(sql.get(2)).contains("insert into attachment (id, goods_entity_id, name, ");
|
||||
assertThat(sql.get(3)).contains(" -- bind(");
|
||||
assertThat(sql.get(4)).contains(" -- bind(");
|
||||
if (isH2() || isPostgresCompatible() || isMySql()) { // i.e. using identity, not using sequence
|
||||
assertThat(sql.get(5)).contains("insert into attachment (goods_entity_id, name,");
|
||||
assertThat(sql.get(6)).contains(" -- bind(");
|
||||
}
|
||||
|
||||
persistedGoods = DB.find(GoodsEntity.class, goods.getId());
|
||||
|
||||
@@ -286,7 +324,18 @@ class TestOneToManyStatelessUpdateResultsInSoftDelete extends BaseTestCase {
|
||||
logger.error("Insert instead update", e);
|
||||
}
|
||||
sql = LoggedSql.stop();
|
||||
assertThat(sql).hasSize(9);
|
||||
assertThat(sql.get(0)).contains("delete from attachment where id=?");
|
||||
assertThat(sql.get(1)).contains(" -- bind(");
|
||||
assertThat(sql.get(2)).contains(" -- bind(");
|
||||
assertThat(sql.get(3)).contains(" -- bind(");
|
||||
assertThat(sql.get(4)).contains("insert into attachment (id, goods_entity_id, name,");
|
||||
assertThat(sql.get(5)).contains(" -- bind(");
|
||||
assertThat(sql.get(6)).contains(" -- bind(");
|
||||
if (isH2() || isPostgresCompatible()) { // using identity, not using sequence
|
||||
assertThat(sql.get(7)).contains("insert into attachment (goods_entity_id, name, ");
|
||||
assertThat(sql.get(8)).contains(" -- bind(");
|
||||
}
|
||||
|
||||
var persistedGoods2 = DB.find(GoodsEntity.class, goods.getId());
|
||||
assertThat(persistedGoods2.getAttachments()).hasSize(3);
|
||||
|
||||
Reference in New Issue
Block a user