diff --git a/ebean-api/src/main/java/io/ebean/Transaction.java b/ebean-api/src/main/java/io/ebean/Transaction.java index 4f7c148189..ecb395c1e7 100644 --- a/ebean-api/src/main/java/io/ebean/Transaction.java +++ b/ebean-api/src/main/java/io/ebean/Transaction.java @@ -260,6 +260,27 @@ static Transaction current() { */ void setUpdateAllLoadedProperties(boolean updateAllLoadedProperties); + /** + * Set to false to disable auto-generation of {@code @WhenCreated}, {@code @WhenModified}, + * {@code @WhoCreated} and {@code @WhoModified} values for this transaction. + *
+ * When disabled, Ebean will only set a generated property value if the property currently + * has a null value (for inserts) or is a {@code @Version} property. Any value already set + * on the bean is preserved. + *
+ * This is useful in backup and restore scenarios where you need to retain the original + * audit timestamps and user values rather than have them overwritten. + *
{@code
+ * try (Transaction txn = DB.beginTransaction()) {
+ * txn.setGeneratedPropertiesEnabled(false);
+ * bean.setWhenCreated(originalTimestamp);
+ * DB.save(bean);
+ * txn.commit();
+ * }
+ * }
+ */
+ void setGeneratedPropertiesEnabled(boolean enable);
+
/**
* Set if the L2 cache should be skipped for "find by id" and "find by natural key" queries.
* diff --git a/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransaction.java b/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransaction.java index 79095cd040..ffaeb7df64 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransaction.java +++ b/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransaction.java @@ -104,6 +104,11 @@ public interface SpiTransaction extends Transaction { */ Boolean isUpdateAllLoadedProperties(); + /** + * Return true if generated properties ({@code @WhenCreated} etc.) are enabled for this transaction. + */ + boolean isGeneratedPropertiesEnabled(); + /** * Return the batchSize specifically set for this transaction or 0. *
diff --git a/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransactionProxy.java b/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransactionProxy.java index e160a4063c..c36c5deff2 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransactionProxy.java +++ b/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransactionProxy.java @@ -244,6 +244,16 @@ public void setUpdateAllLoadedProperties(boolean updateAllLoaded) { transaction.setUpdateAllLoadedProperties(updateAllLoaded); } + @Override + public void setGeneratedPropertiesEnabled(boolean enable) { + transaction.setGeneratedPropertiesEnabled(enable); + } + + @Override + public boolean isGeneratedPropertiesEnabled() { + return transaction.isGeneratedPropertiesEnabled(); + } + @Override public Boolean isUpdateAllLoadedProperties() { return transaction.isUpdateAllLoadedProperties(); 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 ecbd8e5f8a..6e78863709 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 @@ -262,13 +262,13 @@ private void onUpdateGeneratedProperties() { GeneratedProperty generatedProperty = prop.generatedProperty(); if (prop.isVersion()) { if (isLoadedProperty(prop)) { - // @Version property must be loaded to be involved + // @Version property must be loaded to be involved — always auto-incremented Object value = generatedProperty.getUpdateValue(prop, entityBean, now()); Object oldVal = prop.getValue(entityBean); setVersionValue(value); intercept.setOldValue(prop.propertyIndex(), oldVal); } - } else { + } else if (transaction == null || transaction.isGeneratedPropertiesEnabled()) { // @WhenModified set without invoking interception Object oldVal = prop.getValue(entityBean); Object value = generatedProperty.getUpdateValue(prop, entityBean, now()); @@ -280,17 +280,22 @@ private void onUpdateGeneratedProperties() { private void onFailedUpdateUndoGeneratedProperties() { for (BeanProperty prop : beanDescriptor.propertiesGenUpdate()) { - Object oldVal = intercept.origValue(prop.propertyIndex()); - if (oldVal != null) { - prop.setValue(entityBean, oldVal); + if (prop.isVersion() || transaction == null || transaction.isGeneratedPropertiesEnabled()) { + // undo version always (it was always set); undo others only if they were set + Object oldVal = intercept.origValue(prop.propertyIndex()); + if (oldVal != null) { + prop.setValue(entityBean, oldVal); + } } } } private void onInsertGeneratedProperties() { for (BeanProperty prop : beanDescriptor.propertiesGenInsert()) { - Object value = prop.generatedProperty().getInsertValue(prop, entityBean, now()); - prop.setValueChanged(entityBean, value); + if (prop.isVersion() || transaction == null || transaction.isGeneratedPropertiesEnabled() || prop.getValue(entityBean) == null) { + Object value = prop.generatedProperty().getInsertValue(prop, entityBean, now()); + prop.setValueChanged(entityBean, value); + } } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/transaction/ImplicitReadOnlyTransaction.java b/ebean-core/src/main/java/io/ebeaninternal/server/transaction/ImplicitReadOnlyTransaction.java index 5beb06c261..b8cdbbfe29 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/transaction/ImplicitReadOnlyTransaction.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/transaction/ImplicitReadOnlyTransaction.java @@ -276,6 +276,15 @@ public Boolean isUpdateAllLoadedProperties() { return null; } + @Override + public void setGeneratedPropertiesEnabled(boolean enable) { + } + + @Override + public boolean isGeneratedPropertiesEnabled() { + return true; + } + @Override public void setBatchMode(boolean batchMode) { } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/transaction/JdbcTransaction.java b/ebean-core/src/main/java/io/ebeaninternal/server/transaction/JdbcTransaction.java index bfe87075c5..3543b22a0f 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/transaction/JdbcTransaction.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/transaction/JdbcTransaction.java @@ -54,6 +54,7 @@ class JdbcTransaction implements SpiTransaction, TxnProfileEventCodes { private boolean queryOnly = true; private boolean localReadOnly; private Boolean updateAllLoadedProperties; + private boolean generatedPropertiesEnabled = true; private boolean oldBatchMode; private boolean batchMode; private boolean batchOnCascadeMode; @@ -439,6 +440,16 @@ public final Boolean isUpdateAllLoadedProperties() { return updateAllLoadedProperties; } + @Override + public final void setGeneratedPropertiesEnabled(boolean enable) { + this.generatedPropertiesEnabled = enable; + } + + @Override + public final boolean isGeneratedPropertiesEnabled() { + return generatedPropertiesEnabled; + } + @Override public final void setBatchMode(boolean batchMode) { this.batchMode = batchMode; diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/transaction/NoTransaction.java b/ebean-core/src/main/java/io/ebeaninternal/server/transaction/NoTransaction.java index f3053ec6ac..bdbd78961f 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/transaction/NoTransaction.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/transaction/NoTransaction.java @@ -231,6 +231,15 @@ public void setPersistCascade(boolean persistCascade) { public void setUpdateAllLoadedProperties(boolean updateAllLoadedProperties) { } + @Override + public void setGeneratedPropertiesEnabled(boolean enable) { + } + + @Override + public boolean isGeneratedPropertiesEnabled() { + return true; + } + @Override public void setSkipCache(boolean skipCache) { } diff --git a/ebean-test/src/test/java/org/tests/generated/TestGeneratedProperties.java b/ebean-test/src/test/java/org/tests/generated/TestGeneratedProperties.java index ced209f8bc..1df83b5a86 100644 --- a/ebean-test/src/test/java/org/tests/generated/TestGeneratedProperties.java +++ b/ebean-test/src/test/java/org/tests/generated/TestGeneratedProperties.java @@ -1,10 +1,13 @@ package org.tests.generated; -import io.ebean.xtest.BaseTestCase; import io.ebean.DB; +import io.ebean.Transaction; +import io.ebean.xtest.BaseTestCase; import org.junit.jupiter.api.Test; import org.tests.model.EGenProps; +import java.time.Instant; + import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -47,4 +50,70 @@ public void test_insert() { DB.delete(bean); } + + @Test + public void test_insert_generatedPropertiesDisabled_preservesBeanValues() { + Instant created = Instant.parse("2022-01-01T00:00:00Z"); + Instant updated = Instant.parse("2022-01-02T00:00:00Z"); + + EGenProps bean = new EGenProps(); + bean.setName("restore-insert"); + bean.setInstantCreated(created); + bean.setInstantUpdated(updated); + + try (Transaction txn = DB.beginTransaction()) { + txn.setGeneratedPropertiesEnabled(false); + DB.save(bean); + txn.commit(); + } + + bean = DB.find(EGenProps.class, bean.getId()); + assertThat(bean.getInstantCreated()).isEqualTo(created); + assertThat(bean.getInstantUpdated()).isEqualTo(updated); + DB.delete(bean); + } + + @Test + public void test_insert_generatedPropertiesDisabled_nullValueStillFilled() { + EGenProps bean = new EGenProps(); + bean.setName("restore-insert-null"); + // intentionally NOT setting instantCreated / instantUpdated + + try (Transaction txn = DB.beginTransaction()) { + txn.setGeneratedPropertiesEnabled(false); + DB.save(bean); + txn.commit(); + } + + bean = DB.find(EGenProps.class, bean.getId()); + // null values must still be filled by the generator + assertThat(bean.getInstantCreated()).isNotNull(); + assertThat(bean.getInstantUpdated()).isNotNull(); + DB.delete(bean); + } + + @Test + public void test_update_generatedPropertiesDisabled_preservesBeanValues() { + EGenProps bean = new EGenProps(); + bean.setName("restore-update"); + DB.save(bean); + + Instant created = Instant.parse("2022-03-01T00:00:00Z"); + Instant updated = Instant.parse("2022-03-02T00:00:00Z"); + + bean = DB.find(EGenProps.class, bean.getId()); + bean.setInstantCreated(created); + bean.setInstantUpdated(updated); + + try (Transaction txn = DB.beginTransaction()) { + txn.setGeneratedPropertiesEnabled(false); + DB.save(bean); + txn.commit(); + } + + bean = DB.find(EGenProps.class, bean.getId()); + assertThat(bean.getInstantCreated()).isEqualTo(created); + assertThat(bean.getInstantUpdated()).isEqualTo(updated); + DB.delete(bean); + } }