#1221 - ENH: Add a merge(bean) and merge(bean, paths)

This commit is contained in:
Rob Bygrave
2018-03-13 23:55:47 +13:00
parent 607cf3156c
commit afead2da41
37 changed files with 1804 additions and 66 deletions
+10
View File
@@ -638,6 +638,16 @@ public final class Ebean {
serverMgr.getDefaultServer().updateAll(beans);
}
/**
* Merge the bean using the given merge options.
*
* @param bean The bean to merge
* @param options The options to control the merge
*/
public static void merge(Object bean, MergeOptions options) {
serverMgr.getDefaultServer().merge(bean, options);
}
/**
* Save all the beans from a Collection.
*/
+16
View File
@@ -1615,6 +1615,22 @@ public interface EbeanServer {
*/
void updateAll(Collection<?> beans, Transaction transaction) throws OptimisticLockException;
/**
* Merge the bean using the given merge options.
*
* @param bean The bean to merge
* @param options The options to control the merge
*/
void merge(Object bean, MergeOptions options);
/**
* Merge the bean using the given merge options and a transaction.
*
* @param bean The bean to merge
* @param options The options to control the merge
*/
void merge(Object bean, MergeOptions options, Transaction transaction);
/**
* Insert the bean.
* <p>
+29
View File
@@ -0,0 +1,29 @@
package io.ebean;
import java.util.Set;
/**
* Options used to control a merge. Use MergeOptionsBuilder to create an instance.
* <p>
* Instances of MergeOptions are thread safe and safe to share across threads.
*/
public interface MergeOptions {
/**
* Returns true if Id values are supplied by the client.
* <p>
* This would be the case when for example a mobile creates data in it's own local database
* and then sync's. In this case often the id values are UUID.
*/
boolean isClientGeneratedIds();
/**
* Return true if delete permanent should be used and false for 'normal' delete that allows soft deletes.
*/
boolean isDeletePermanent();
/**
* Return the paths included in the merge.
*/
Set<String> paths();
}
@@ -0,0 +1,80 @@
package io.ebean;
import java.util.LinkedHashSet;
import java.util.Set;
/**
* Builds a MergeOptions which is immutable and thread safe.
*/
public class MergeOptionsBuilder {
private Set<String> paths = new LinkedHashSet<>();
private boolean clientGeneratedIds;
private boolean deletePermanent = true;
/**
* Add a path that will included in the merge.
*
* @param path The path relative to the root type.
* @return The builder to chain another addPath() or build().
*/
public MergeOptionsBuilder addPath(String path) {
paths.add(path);
return this;
}
/**
* Set to true if Id values are supplied by the client.
* <p>
* This would be the case when for example a mobile creates data in it's own local database
* and then sync's. In this case often the id values are UUID.
*/
public MergeOptionsBuilder setClientGeneratedIds() {
this.clientGeneratedIds = true;
return this;
}
/**
* Set that deletions should use delete permanent (rather than default which allows soft deletes).
*/
public MergeOptionsBuilder setDeletePermanent() {
this.deletePermanent = true;
return this;
}
/**
* Build and return the MergeOptions instance.
*/
public MOptions build() {
return new MOptions(paths, clientGeneratedIds, deletePermanent);
}
private static class MOptions implements MergeOptions {
private final boolean clientGeneratedIds;
private final boolean deletePermanent;
private final Set<String> paths;
private MOptions(Set<String> paths, boolean clientGeneratedIds, boolean deletePermanent) {
this.paths = paths;
this.clientGeneratedIds = clientGeneratedIds;
this.deletePermanent = deletePermanent;
}
public Set<String> paths() {
return paths;
}
@Override
public boolean isClientGeneratedIds() {
return clientGeneratedIds;
}
@Override
public boolean isDeletePermanent() {
return deletePermanent;
}
}
}
@@ -52,6 +52,8 @@ public final class EntityBeanIntercept implements Serializable {
*/
private int state;
private boolean forceUpdate;
private boolean readOnly;
private boolean dirty;
@@ -292,6 +294,20 @@ public final class EntityBeanIntercept implements Serializable {
this.readOnly = readOnly;
}
/**
* Set the bean to be updated when persisted (for merge).
*/
public void setForceUpdate(boolean forceUpdate) {
this.forceUpdate = forceUpdate;
}
/**
* Return true if the entity should be updated.
*/
public boolean isUpdate() {
return forceUpdate || state == STATE_LOADED;
}
/**
* Return true if the entity has been loaded.
*/
@@ -263,4 +263,9 @@ public interface SpiEbeanServer extends EbeanServer, BeanLoader, BeanCollectionL
* Visit all the metrics (typically reporting them).
*/
void visitMetrics(MetricVisitor visitor);
/**
* Return true if a row for the bean type and id exists.
*/
boolean exists(Class<?> beanType, Object beanId, Transaction transaction);
}
@@ -12,6 +12,7 @@ import io.ebean.Filter;
import io.ebean.FutureIds;
import io.ebean.FutureList;
import io.ebean.FutureRowCount;
import io.ebean.MergeOptions;
import io.ebean.PagedList;
import io.ebean.PersistenceContextScope;
import io.ebean.ProfileLocation;
@@ -915,6 +916,20 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer {
return new DefaultUpdateQuery<>(createQuery(beanType));
}
@Override
public void merge(Object bean, MergeOptions options) {
merge(bean, options, null);
}
@Override
public void merge(Object bean, MergeOptions options, Transaction transaction) {
BeanDescriptor<?> desc = getBeanDescriptor(bean.getClass());
if (desc == null) {
throw new PersistenceException(bean.getClass().getName() + " is NOT an Entity Bean registered with this server?");
}
executeInTrans((txn) -> persister.merge(desc, checkEntityBean(bean), options, txn), transaction);
}
@Override
public <T> Query<T> find(Class<T> beanType) {
return createQuery(beanType);
@@ -1271,6 +1286,12 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer {
}
}
@Override
public boolean exists(Class<?> beanType, Object beanId, Transaction transaction) {
List<Object> ids = findIds(find(beanType).setId(beanId), transaction);
return !ids.isEmpty();
}
@Override
public <A, T> List<A> findIds(Query<T> query, Transaction t) {
@@ -2234,8 +2255,7 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer {
/**
* Returns a set of properties if saving the bean will violate the unique constraints (defined by given properties).
*/
private Set<Property> checkUniqueness(EntityBean entityBean, BeanDescriptor<?> beanDesc, BeanProperty[] props,
Transaction transaction) {
private Set<Property> checkUniqueness(EntityBean entityBean, BeanDescriptor<?> beanDesc, BeanProperty[] props, Transaction transaction) {
BeanProperty idProperty = beanDesc.getIdProperty();
Query<?> query = new DefaultOrmQuery<>(beanDesc, this, expressionFactory);
ExpressionList<?> exprList = query.where();
@@ -23,6 +23,7 @@ import io.ebeaninternal.server.deploy.generatedproperty.GeneratedProperty;
import io.ebeaninternal.server.deploy.id.ImportedId;
import io.ebeaninternal.server.persist.BatchControl;
import io.ebeaninternal.server.persist.BatchedSqlException;
import io.ebeaninternal.server.persist.Flags;
import io.ebeaninternal.server.persist.PersistExecute;
import io.ebeaninternal.server.transaction.BeanPersistIdMap;
import io.ebeanservice.docstore.api.DocStoreUpdate;
@@ -75,6 +76,8 @@ public final class PersistRequestBean<T> extends PersistRequest implements BeanP
private final boolean publish;
private int flags;
private DocStoreMode docStoreMode;
private ConcurrencyMode concurrencyMode;
@@ -152,7 +155,7 @@ public final class PersistRequestBean<T> extends PersistRequest implements BeanP
private boolean getterCallback;
public PersistRequestBean(SpiEbeanServer server, T bean, Object parentBean, BeanManager<T> mgr, SpiTransaction t,
PersistExecute persistExecute, PersistRequest.Type type, boolean saveRecurse, boolean publish) {
PersistExecute persistExecute, PersistRequest.Type type, int flags) {
super(server, t, persistExecute);
this.entityBean = (EntityBean) bean;
@@ -165,7 +168,8 @@ public final class PersistRequestBean<T> extends PersistRequest implements BeanP
this.controller = beanDescriptor.getPersistController();
this.type = type;
this.docStoreMode = calcDocStoreMode(transaction, type);
if (saveRecurse) {
this.flags = flags;
if (Flags.isRecurse(flags)) {
this.persistCascade = t.isPersistCascade();
}
@@ -179,7 +183,7 @@ public final class PersistRequestBean<T> extends PersistRequest implements BeanP
beanDescriptor.checkMutableProperties(intercept);
}
this.concurrencyMode = beanDescriptor.getConcurrencyMode(intercept);
this.publish = publish;
this.publish = Flags.isPublish(flags);
if (isMarkDraftDirty(publish)) {
beanDescriptor.setDraftDirty(entityBean, true);
}
@@ -231,6 +235,13 @@ public final class PersistRequestBean<T> extends PersistRequest implements BeanP
if (createImplicitTransIfRequired()) {
docStoreMode = calcDocStoreMode(transaction, type);
}
checkBatchEscalationOnCascade();
}
/**
* Check for batch escalation on cascade.
*/
public void checkBatchEscalationOnCascade() {
if (transaction.checkBatchEscalationOnCascade(this)) {
// we escalated to use batch mode so flush when done
// but if createdTransaction then commit will flush it
@@ -1076,6 +1087,13 @@ public final class PersistRequestBean<T> extends PersistRequest implements BeanP
return requestUpdateAllLoadedProps;
}
/**
* Return the flags set on this persist request.
*/
public int getFlags() {
return flags;
}
/**
* Return true if this request is a 'publish' action.
*/
@@ -1226,4 +1244,25 @@ public final class PersistRequestBean<T> extends PersistRequest implements BeanP
public void profile() {
profileBase(type.profileEventId, profileOffset, beanDescriptor.getProfileId(), 1);
}
/**
* Set the request flags indicating this is an insert.
*/
public void flagInsert() {
flags = Flags.setInsert(flags);
}
/**
* Unset the request insert flag indicating this is an update.
*/
public void flagUpdate() {
flags = Flags.unsetInsert(flags);
}
/**
* Return true if this request is an insert.
*/
public boolean isInsertedParent() {
return Flags.isInsert(flags);
}
}
@@ -1,12 +1,15 @@
package io.ebeaninternal.server.core;
import io.ebean.CallableSql;
import io.ebean.MergeOptions;
import io.ebean.Query;
import io.ebean.SqlUpdate;
import io.ebean.Transaction;
import io.ebean.Update;
import io.ebean.bean.EntityBean;
import io.ebean.meta.MetricVisitor;
import io.ebeaninternal.api.SpiTransaction;
import io.ebeaninternal.server.deploy.BeanDescriptor;
import java.util.Collection;
import java.util.List;
@@ -16,6 +19,11 @@ import java.util.List;
*/
public interface Persister {
/**
* Merge the bean.
*/
int merge(BeanDescriptor<?> desc, EntityBean entityBean, MergeOptions options, SpiTransaction transaction);
/**
* Update the bean.
*/
@@ -0,0 +1,17 @@
package io.ebeaninternal.server.deploy;
import io.ebean.bean.BeanCollection;
import java.util.Collection;
abstract class BaseCollectionHelp<T> implements BeanCollectionHelp<T> {
@Override
public Collection underlying(Object value) {
if (value instanceof BeanCollection) {
return ((BeanCollection)value).getActualDetails();
} else {
return (Collection)value;
}
}
}
@@ -10,6 +10,7 @@ import io.ebean.bean.EntityBean;
import io.ebeaninternal.server.text.json.SpiJsonWriter;
import java.io.IOException;
import java.util.Collection;
/**
* Helper functions for performing tasks on Lists Sets or Maps.
@@ -21,6 +22,11 @@ public interface BeanCollectionHelp<T> {
*/
void setLoader(BeanCollectionLoader loader);
/**
* Return the underlying collection of beans.
*/
Collection underlying(Object value);
/**
* Return the mechanism to add beans to the underlying collection.
* <p>
@@ -17,7 +17,7 @@ import java.util.List;
/**
* Helper object for dealing with Lists.
*/
public final class BeanListHelp<T> implements BeanCollectionHelp<T> {
public final class BeanListHelp<T> extends BaseCollectionHelp<T> {
private final BeanPropertyAssocMany<T> many;
private final BeanDescriptor<T> targetDescriptor;
@@ -18,7 +18,7 @@ import java.util.Map.Entry;
/**
* Helper specifically for dealing with Maps.
*/
public final class BeanMapHelp<T> implements BeanCollectionHelp<T> {
public final class BeanMapHelp<T> extends BaseCollectionHelp<T> {
private final BeanPropertyAssocMany<T> many;
private final BeanDescriptor<T> targetDescriptor;
@@ -205,6 +205,13 @@ public class BeanPropertyAssocMany<T> extends BeanPropertyAssoc<T> {
// by default not including "Many" properties in document store
}
/**
* Return the underlying collection of beans.
*/
public Collection getRawCollection(EntityBean bean) {
return help.underlying(getVal(bean));
}
/**
* Copy collection value if existing is empty.
*/
@@ -17,7 +17,7 @@ import java.util.Set;
/**
* Helper specifically for dealing with Sets.
*/
public final class BeanSetHelp<T> implements BeanCollectionHelp<T> {
public final class BeanSetHelp<T> extends BaseCollectionHelp<T> {
private final BeanPropertyAssocMany<T> many;
private final BeanDescriptor<T> targetDescriptor;
@@ -1,6 +1,7 @@
package io.ebeaninternal.server.persist;
import io.ebean.CallableSql;
import io.ebean.MergeOptions;
import io.ebean.Query;
import io.ebean.SqlUpdate;
import io.ebean.Transaction;
@@ -53,8 +54,6 @@ import java.util.Set;
* <li>Handles cascading of save and delete</li>
* <li>Handles the batching and queueing</li>
* </p>
*
* @see io.ebeaninternal.server.persist.DefaultPersistExecute
*/
public final class DefaultPersister implements Persister {
@@ -161,7 +160,7 @@ public final class DefaultPersister implements Persister {
draftHandler.resetDraft(draftBean);
PUB.trace("draftRestore bean [{}] id[{}]", desc.getName(), draftHandler.getId());
update(createRequest(draftBean, transaction, null, mgr, Type.UPDATE, true, false));
update(createRequest(draftBean, transaction, null, mgr, Type.UPDATE, Flags.RECURSE));
}
PUB.debug("draftRestore - complete for [{}]", desc.getName());
@@ -211,7 +210,7 @@ public final class DefaultPersister implements Persister {
Type persistType = draftHandler.isInsert() ? Type.INSERT : Type.UPDATE;
PUB.trace("publish bean [{}] id[{}] type[{}]", desc.getName(), draftHandler.getId(), persistType);
PersistRequestBean<T> request = createRequest(liveBean, transaction, null, mgr, persistType, true, true);
PersistRequestBean<T> request = createRequest(liveBean, transaction, null, mgr, persistType, Flags.PUBLISH_RECURSE);
if (persistType == Type.INSERT) {
insert(request);
} else {
@@ -281,7 +280,7 @@ public final class DefaultPersister implements Persister {
// update the dirty status on the drafts that have been published
PUB.debug("publish - update dirty status on [{}] drafts", draftUpdates.size());
for (T draftUpdate : draftUpdates) {
update(createRequest(draftUpdate, transaction, null, mgr, Type.UPDATE, false, false));
update(createRequest(draftUpdate, transaction, null, mgr, Type.UPDATE, Flags.ZERO));
}
}
}
@@ -346,6 +345,28 @@ public final class DefaultPersister implements Persister {
return deleteRequest(createRequest(detailBean, t, deleteType));
}
@Override
public int merge(BeanDescriptor<?> desc, EntityBean bean, MergeOptions options, SpiTransaction transaction) {
MergeHandler merge = new MergeHandler(server, desc, bean, options, transaction);
List<EntityBean> deleteBeans = merge.merge();
if (!deleteBeans.isEmpty()) {
// all detected deletes for the merge paths
for (EntityBean deleteBean : deleteBeans) {
delete(deleteBean, transaction, options.isDeletePermanent());
}
}
// cascade save as normal with forceUpdate flags set
PersistRequestBean<?> request = createRequestRecurse(bean, transaction, null, Flags.MERGE);
request.checkBatchEscalationOnCascade();
saveRecurse(request);
request.flushBatchOnCascade();
// lambda expects a return
return 0;
}
/**
* Update the bean.
*/
@@ -368,7 +389,7 @@ public final class DefaultPersister implements Persister {
if (req.isReference()) {
// its a reference so see if there are manys to save...
if (req.isPersistCascade()) {
saveAssocMany(false, req, false);
saveAssocMany(req);
}
req.checkUpdatedManysOnly();
} else {
@@ -389,7 +410,7 @@ public final class DefaultPersister implements Persister {
*/
@Override
public void save(EntityBean bean, Transaction t) {
if (bean._ebean_getIntercept().isLoaded()) {
if (bean._ebean_getIntercept().isUpdate()) {
// deleteMissingChildren is false when using 'save' on 'loaded' beans
update(bean, t, false);
} else {
@@ -416,19 +437,22 @@ public final class DefaultPersister implements Persister {
}
}
void saveRecurse(EntityBean bean, Transaction t, Object parentBean, boolean insertMode, boolean publish) {
void saveRecurse(EntityBean bean, Transaction t, Object parentBean, int flags) {
// determine insert or update taking into account stateless updates
PersistRequestBean<?> request = createRequestRecurse(bean, t, parentBean, insertMode, publish);
PersistRequestBean<?> request = createRequestRecurse(bean, t, parentBean, flags);
saveRecurse(request);
}
private void saveRecurse(PersistRequestBean<?> request) {
if (request.isReference()) {
// its a reference...
if (request.isPersistCascade()) {
// save any associated List held beans
saveAssocMany(false, request, insertMode);
request.flagUpdate();
saveAssocMany(request);
}
request.checkUpdatedManysOnly();
} else {
if (request.isInsert()) {
insert(request);
@@ -447,11 +471,11 @@ public final class DefaultPersister implements Persister {
// skip as already inserted/updated in this request (recursive cascading)
return;
}
request.flagInsert();
try {
if (request.isPersistCascade()) {
// save associated One beans recursively first
saveAssocOne(request, true);
saveAssocOne(request);
}
// set the IDGenerated value if required
@@ -460,7 +484,7 @@ public final class DefaultPersister implements Persister {
if (request.isPersistCascade()) {
// save any associated List held beans
saveAssocMany(true, request, true);
saveAssocMany(request);
}
} finally {
request.unRegisterBean();
@@ -476,11 +500,11 @@ public final class DefaultPersister implements Persister {
// skip as already inserted/updated in this request (recursive cascading)
return;
}
request.flagUpdate();
try {
if (request.isPersistCascade()) {
// save associated One beans recursively first
saveAssocOne(request, false);
saveAssocOne(request);
}
if (request.isDirty()) {
@@ -495,7 +519,7 @@ public final class DefaultPersister implements Persister {
if (request.isPersistCascade()) {
// save all the beans in assocMany's after
saveAssocMany(false, request, false);
saveAssocMany(request);
}
request.checkUpdatedManysOnly();
@@ -518,7 +542,7 @@ public final class DefaultPersister implements Persister {
if (originalRequest.isHardDeleteDraft()) {
// a hard delete of a draftable bean so first we need to delete the associated 'live' bean
// due to FK constraint and then after that execute the original delete of the draft bean
return deleteRequest(createPublishRequest(originalRequest.createReference(), t, Type.DELETE_PERMANENT, true), originalRequest);
return deleteRequest(createPublishRequest(originalRequest.createReference(), t, Type.DELETE_PERMANENT, Flags.PUBLISH), originalRequest);
} else {
// normal delete or soft delete
@@ -610,7 +634,7 @@ public final class DefaultPersister implements Persister {
int rowCount = deleteRecurse(bean, transaction, permanent);
if (rowCount == -1) {
total = -1;
} else if (total != -1){
} else if (total != -1) {
total += rowCount;
}
}
@@ -838,7 +862,7 @@ public final class DefaultPersister implements Persister {
* bean to the child beans.
* </p>
*/
private void saveAssocMany(boolean insertedParent, PersistRequestBean<?> request, boolean insertMode) {
private void saveAssocMany(PersistRequestBean<?> request) {
EntityBean parentBean = request.getEntityBean();
BeanDescriptor<?> desc = request.getBeanDescriptor();
@@ -854,7 +878,7 @@ public final class DefaultPersister implements Persister {
if (!prop.isSaveRecurseSkippable(detailBean)) {
t.depth(+1);
prop.setParentBeanToChild(parentBean, detailBean);
saveRecurse(detailBean, t, parentBean, insertMode, request.isPublish());
saveRecurse(detailBean, t, parentBean, request.getFlags());
t.depth(-1);
}
}
@@ -863,10 +887,11 @@ public final class DefaultPersister implements Persister {
// many's with cascade save
BeanPropertyAssocMany<?>[] manys = desc.propertiesManySave();
boolean insertedParent = request.isInsertedParent();
for (BeanPropertyAssocMany<?> many : manys) {
// check that property is loaded and collection should be cascaded to
if (request.isLoadedProperty(many) && !many.isSkipSaveBeanCollection(parentBean, insertedParent)) {
saveMany(new SaveManyPropRequest(insertedParent, many, parentBean, request), insertMode);
saveMany(new SaveManyPropRequest(insertedParent, many, parentBean, request));
if (!insertedParent) {
request.addUpdatedManyProperty(many);
}
@@ -874,7 +899,7 @@ public final class DefaultPersister implements Persister {
}
}
private void saveMany(SaveManyPropRequest saveMany, boolean insertMode) {
private void saveMany(SaveManyPropRequest saveMany) {
if (saveMany.getMany().hasJoinTable()) {
@@ -882,7 +907,7 @@ public final class DefaultPersister implements Persister {
// we only allow one direction based on first traversed basis
boolean saveIntersectionFromThisDirection = saveMany.isSaveIntersection();
if (saveMany.isCascade()) {
saveAssocManyDetails(saveMany, false, insertMode);
saveAssocManyDetails(saveMany, false);
}
// for ManyToMany save the 'relationship' via inserts/deletes
// into/from the intersection table
@@ -900,7 +925,7 @@ public final class DefaultPersister implements Persister {
}
if (saveMany.isCascade()) {
// potentially deletes 'missing children' for 'stateless update'
saveAssocManyDetails(saveMany, saveMany.isDeleteMissingChildren(), insertMode);
saveAssocManyDetails(saveMany, saveMany.isDeleteMissingChildren());
}
}
}
@@ -922,7 +947,7 @@ public final class DefaultPersister implements Persister {
EntityBean eb = (EntityBean) removedBean;
if (eb._ebean_getIntercept().isLoaded()) {
// only delete if the bean was loaded meaning that it is known to exist in the DB
deleteRequest(createPublishRequest(removedBean, saveMany.getTransaction(), PersistRequest.Type.DELETE, saveMany.isPublish()));
deleteRequest(createPublishRequest(removedBean, saveMany.getTransaction(), PersistRequest.Type.DELETE, saveMany.getFlags()));
}
}
}
@@ -933,9 +958,9 @@ public final class DefaultPersister implements Persister {
/**
* Save the details from a OneToMany collection.
*/
private void saveAssocManyDetails(SaveManyPropRequest saveMany, boolean deleteMissingChildren, boolean insertMode) {
private void saveAssocManyDetails(SaveManyPropRequest saveMany, boolean deleteMissingChildren) {
saveMany.saveDetails(this, deleteMissingChildren, insertMode);
saveMany.saveDetails(this, deleteMissingChildren);
}
/**
@@ -1056,13 +1081,12 @@ public final class DefaultPersister implements Persister {
return false;
}
private int deleteAssocManyIntersection(EntityBean bean, BeanPropertyAssocMany<?> many, Transaction t, boolean publish) {
private void deleteAssocManyIntersection(EntityBean bean, BeanPropertyAssocMany<?> many, Transaction t, boolean publish) {
// delete all intersection rows for this bean
IntersectionRow intRow = many.buildManyToManyDeleteChildren(bean, publish);
SqlUpdate sqlDelete = intRow.createDeleteChildren(server);
return executeSqlUpdate(sqlDelete, t);
executeSqlUpdate(sqlDelete, t);
}
/**
@@ -1154,7 +1178,7 @@ public final class DefaultPersister implements Persister {
* </p>
*/
void deleteManyDetails(SpiTransaction t, BeanDescriptor<?> desc, EntityBean parentBean,
BeanPropertyAssocMany<?> many, List<Object> excludeDetailIds, boolean softDelete) {
BeanPropertyAssocMany<?> many, List<Object> excludeDetailIds, boolean softDelete) {
if (many.getCascadeInfo().isDelete()) {
// cascade delete the beans in the collection
@@ -1202,7 +1226,7 @@ public final class DefaultPersister implements Persister {
/**
* Save any associated one beans.
*/
private void saveAssocOne(PersistRequestBean<?> request, boolean insertMode) {
private void saveAssocOne(PersistRequestBean<?> request) {
BeanDescriptor<?> desc = request.getBeanDescriptor();
@@ -1219,7 +1243,7 @@ public final class DefaultPersister implements Persister {
&& !request.isParent(detailBean)) {
SpiTransaction t = request.getTransaction();
t.depth(-1);
saveRecurse(detailBean, t, null, insertMode, request.isPublish());
saveRecurse(detailBean, t, null, request.getFlags());
t.depth(+1);
}
}
@@ -1315,25 +1339,25 @@ public final class DefaultPersister implements Persister {
* perform an insert, update or delete.
*/
private <T> PersistRequestBean<T> createRequest(T bean, Transaction t, PersistRequest.Type type) {
return createRequestInternal(bean, t, type, false, false);
return createRequestInternal(bean, t, type, Flags.ZERO);
}
/**
* Create the Persist Request Object additionally specifying the publish status.
*/
private <T> PersistRequestBean<T> createPublishRequest(T bean, Transaction t, PersistRequest.Type type, boolean publish) {
return createRequestInternal(bean, t, type, false, publish);
private <T> PersistRequestBean<T> createPublishRequest(T bean, Transaction t, PersistRequest.Type type, int flags) {
return createRequestInternal(bean, t, type, Flags.unsetRecuse(flags));
}
/**
* Create the Persist Request Object additionally specifying the publish status.
*/
private <T> PersistRequestBean<T> createRequestInternal(T bean, Transaction t, PersistRequest.Type type, boolean saveRecurse, boolean publish) {
private <T> PersistRequestBean<T> createRequestInternal(T bean, Transaction t, PersistRequest.Type type, int flags) {
BeanManager<T> mgr = getBeanManager(bean);
if (mgr == null) {
throw new PersistenceException(errNotRegistered(bean.getClass()));
}
return createRequest(bean, t, null, mgr, type, saveRecurse, publish);
return createRequest(bean, t, null, mgr, type, flags);
}
/**
@@ -1341,7 +1365,7 @@ public final class DefaultPersister implements Persister {
* <p>
* This call determines the PersistRequest.Type based on bean state and the insert flag (root persist type).
*/
private <T> PersistRequestBean<T> createRequestRecurse(T bean, Transaction t, Object parentBean, boolean insertMode, boolean publish) {
private <T> PersistRequestBean<T> createRequestRecurse(T bean, Transaction t, Object parentBean, int flags) {
BeanManager<T> mgr = getBeanManager(bean);
if (mgr == null) {
throw new PersistenceException(errNotRegistered(bean.getClass()));
@@ -1349,14 +1373,15 @@ public final class DefaultPersister implements Persister {
BeanDescriptor<T> desc = mgr.getBeanDescriptor();
EntityBean entityBean = (EntityBean) bean;
PersistRequest.Type type;
if (publish) {
if (Flags.isPublishOrMerge(flags)) {
// insert if it is a new bean (as publish created it)
type = entityBean._ebean_getIntercept().isNew() ? Type.INSERT : Type.UPDATE;
type = entityBean._ebean_getIntercept().isUpdate() ? Type.UPDATE : Type.INSERT;
} else {
// determine Insert or Update based on bean state and insert flag
boolean insertMode = Flags.isInsert(flags);
type = desc.isInsertMode(entityBean._ebean_getIntercept(), insertMode) ? Type.INSERT : Type.UPDATE;
}
return createRequest(bean, t, parentBean, mgr, type, true, publish);
return createRequest(bean, t, parentBean, mgr, type, Flags.setRecurse(flags));
}
/**
@@ -1365,7 +1390,7 @@ public final class DefaultPersister implements Persister {
*/
@SuppressWarnings({"unchecked", "rawtypes"})
private <T> PersistRequestBean<T> createRequest(T bean, Transaction t, Object parentBean, BeanManager<?> mgr,
PersistRequest.Type type, boolean saveRecurse, boolean publish) {
PersistRequest.Type type, int flags) {
if (type == Type.DELETE_PERMANENT) {
type = Type.DELETE;
@@ -1374,7 +1399,7 @@ public final class DefaultPersister implements Persister {
type = Type.SOFT_DELETE;
}
return new PersistRequestBean(server, bean, parentBean, mgr, (SpiTransaction) t, persistExecute, type, saveRecurse, publish);
return new PersistRequestBean(server, bean, parentBean, mgr, (SpiTransaction) t, persistExecute, type, flags);
}
private String errNotRegistered(Class<?> beanClass) {
@@ -0,0 +1,135 @@
package io.ebeaninternal.server.persist;
/**
* Flags used in persistence.
* <p>
* Allows passing of flag state when recursively persisting.
*/
public class Flags {
/**
* Indicates the bean is being inserted.
*/
public static final int INSERT = 0x00000001;
/**
* Indicates persist cascade.
*/
public static final int RECURSE = 0x00000002;
/**
* Indicates Publish mode.
*/
public static final int PUBLISH = 0x00000004;
/**
* Indicates Merge mode.
*/
public static final int MERGE = 0x00000008;
/**
* No flags set.
*/
public static final int ZERO = 0;
public static final int PUBLISH_RECURSE = PUBLISH + RECURSE;
private static final int PUBLISH_MERGE = PUBLISH + MERGE;
/**
* Return true if the bean is being inserted.
*/
public static boolean isInsert(int state) {
return isSet(state, INSERT);
}
/**
* Return true if persist cascading.
*/
public static boolean isRecurse(int state) {
return isSet(state, RECURSE);
}
/**
* Return true if part of a Publish.
*/
public static boolean isPublish(int state) {
return isSet(state, PUBLISH);
}
/**
* Return true if part of a Merge.
*/
public static boolean isMerge(int state) {
return isSet(state, PUBLISH);
}
/**
* Return true if part of a Merge or Publish.
*/
public static boolean isPublishOrMerge(long state) {
return (state & PUBLISH_MERGE) != 0;
}
/**
* Return true if the given flag is set.
*/
public static boolean isSet(int state, int flag) {
return (state & flag) == flag;
}
/**
* Set Insert flag.
*/
public static int setInsert(int state) {
return set(state, INSERT, true);
}
/**
* Parent was not inserted.
*/
public static int unsetInsert(int state) {
return set(state, INSERT, false);
}
/**
* Set Recurse flag.
*/
public static int setRecurse(int state) {
return set(state, RECURSE, true);
}
public static int unsetRecuse(int state) {
return set(state, RECURSE, false);
}
/**
* Set Publish flag.
*/
public static int setPublish(int state) {
return set(state, PUBLISH, true);
}
public static int unsetPublish(int state) {
return set(state, PUBLISH, false);
}
/**
* Set Merge flag.
*/
public static int setMerge(int state) {
return set(state, MERGE, true);
}
public static int unsetMerge(int state) {
return set(state, MERGE, false);
}
private static int set(int state, int flag, boolean setFlag) {
if (setFlag) {
return (state |= flag);
} else {
return state &= ~flag;
}
}
}
@@ -0,0 +1,56 @@
package io.ebeaninternal.server.persist;
import io.ebean.bean.EntityBean;
import io.ebeaninternal.api.SpiEbeanServer;
import io.ebeaninternal.api.SpiTransaction;
import java.util.ArrayList;
import java.util.List;
/**
* Context used for merge processing.
*/
class MergeContext {
private final SpiEbeanServer server;
private final SpiTransaction transaction;
private final List<EntityBean> deleteBeans = new ArrayList<>();
private boolean clientGeneratedIds;
MergeContext(SpiEbeanServer server, SpiTransaction transaction, boolean clientGeneratedIds) {
this.server = server;
this.transaction = transaction;
this.clientGeneratedIds = clientGeneratedIds;
}
/**
* Add to the list of beans to delete.
*/
void addDelete(EntityBean deleteBean) {
deleteBeans.add(deleteBean);
}
/**
* Return true if Ids must be checked against the DB.
*/
boolean isClientGeneratedIds() {
return clientGeneratedIds;
}
/**
* Check if the Id / bean exists in the database.
*/
boolean idExists(Class<?> beanType, Object beanId) {
return server.exists(beanType, beanId, transaction);
}
/**
* Return the list of beans to delete.
*/
List<EntityBean> getDeletedBeans() {
return deleteBeans;
}
}
@@ -0,0 +1,147 @@
package io.ebeaninternal.server.persist;
import io.ebean.CacheMode;
import io.ebean.MergeOptions;
import io.ebean.PersistenceContextScope;
import io.ebean.Query;
import io.ebean.bean.EntityBean;
import io.ebeaninternal.api.SpiEbeanServer;
import io.ebeaninternal.api.SpiTransaction;
import io.ebeaninternal.server.deploy.BeanDescriptor;
import io.ebeaninternal.server.deploy.BeanProperty;
import io.ebeaninternal.server.deploy.BeanPropertyAssoc;
import io.ebeaninternal.server.deploy.BeanPropertyAssocMany;
import io.ebeaninternal.server.deploy.BeanPropertyAssocOne;
import javax.persistence.PersistenceException;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
/**
* Drives the merge processing.
*/
class MergeHandler {
private final Pattern PATH_SPLIT = Pattern.compile("\\.");
private final SpiEbeanServer server;
private final BeanDescriptor<?> desc;
private final EntityBean bean;
private final MergeOptions options;
private final SpiTransaction transaction;
private final Map<String, MergeNode> nodes = new LinkedHashMap<>();
MergeHandler(SpiEbeanServer server, BeanDescriptor<?> desc, EntityBean bean, MergeOptions options, SpiTransaction transaction) {
this.server = server;
this.desc = desc;
this.bean = bean;
this.options = options;
this.transaction = transaction;
}
/**
* Fetch the Ids for the graph and use them to determine inserts, updates and deletes for the merge paths.
*/
List<EntityBean> merge() {
Set<String> paths = options.paths();
if (paths.isEmpty() && !options.isClientGeneratedIds()) {
// just do a single insert or update based on Id value present
Object id = desc.getId(bean);
if (id != null) {
bean._ebean_getIntercept().setForceUpdate(true);
}
return Collections.emptyList();
}
EntityBean outline = fetchOutline(paths);
if (outline == null) {
// considered an insert ...
return Collections.emptyList();
}
// the top level bean is an update
bean._ebean_getIntercept().setForceUpdate(true);
// detect what beans are updates and recursively set forceUpdate as needed
// and outline beans not in the merge graph are generally considered as deletes
MergeContext context = new MergeContext(server, transaction, options.isClientGeneratedIds());
MergeRequest request = new MergeRequest(context, bean, outline);
for (MergeNode value : nodes.values()) {
value.merge(request);
}
return context.getDeletedBeans();
}
/**
* Fetch the outline bean with associated one and associated many beans loaded with Id values only.
* <p>
* We use the Id values to determine what are inserts, updates and deletes as part of the merge.
*/
private EntityBean fetchOutline(Set<String> paths) {
Query<?> query = server.find(desc.getBeanType());
query.setBeanCacheMode(CacheMode.OFF);
query.setPersistenceContextScope(PersistenceContextScope.QUERY);
query.setId(desc.getId(bean));
query.select(desc.getIdProperty().getName());
for (String path : paths) {
MergeNode node = buildNode(path);
node.addSelectId(query);
}
return (EntityBean) server.findOne(query, transaction);
}
private MergeNode buildNode(String path) {
String[] split = PATH_SPLIT.split(path);
if (split.length == 1) {
return addRootLevelNode(split[0]);
} else {
return addSubNode(path, split);
}
}
private MergeNode addSubNode(String fullPath, String[] split) {
MergeNode parent = nodes.get(split[0]);
if (parent == null) {
throw new PersistenceException("Unable to find parent path " + split[0] + " in merge paths?");
}
for (int i = 1; i < split.length - 1; i++) {
parent = parent.get(split[i]);
if (parent == null) {
throw new PersistenceException("Unable to find parent path " + split[0] + " in merge paths?");
}
}
return parent.addChild(fullPath, split[split.length - 1]);
}
private MergeNode addRootLevelNode(String rootPath) {
MergeNode node = createMergeNode(rootPath, desc, rootPath);
nodes.put(rootPath, node);
return node;
}
static MergeNode createMergeNode(String fullPath, BeanDescriptor<?> targetDesc, String path) {
BeanProperty prop = targetDesc.getBeanProperty(path);
if (prop == null || !(prop instanceof BeanPropertyAssoc)) {
throw new PersistenceException("merge path [" + path + "] is not a ToMany or ToOne property of " + targetDesc.getFullName());
}
if (prop instanceof BeanPropertyAssocMany<?>) {
return new MergeNodeAssocMany(fullPath, (BeanPropertyAssocMany<?>) prop);
} else {
return new MergeNodeAssocOne(fullPath, (BeanPropertyAssocOne<?>) prop);
}
}
}
@@ -0,0 +1,74 @@
package io.ebeaninternal.server.persist;
import io.ebean.Query;
import io.ebean.bean.EntityBean;
import io.ebeaninternal.server.deploy.BeanDescriptor;
import io.ebeaninternal.server.deploy.BeanProperty;
import io.ebeaninternal.server.deploy.BeanPropertyAssoc;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Base class for merge nodes.
*/
abstract class MergeNode {
protected final String fullPath;
protected final BeanDescriptor<?> targetDescriptor;
protected Map<String,MergeNode> children;
MergeNode(String fullPath, BeanPropertyAssoc<?> property) {
this.fullPath = fullPath;
this.targetDescriptor = property.getTargetDescriptor();
}
/**
* Perform the merge processing.
*/
abstract void merge(MergeRequest request);
/**
* Add a child node given the fullPath and relative path.
*/
MergeNode addChild(String fullPath, String path) {
MergeNode childNode = MergeHandler.createMergeNode(fullPath, targetDescriptor, path);
if (children == null) {
children = new LinkedHashMap<>();
}
children.put(path, childNode);
return childNode;
}
/**
* Return the node given the relative path.
*/
MergeNode get(String path) {
if (children != null) {
return children.get(path);
}
return null;
}
/**
* Add to the query to fetch the Ids values for the foreign keys basically.
*/
void addSelectId(Query<?> query) {
BeanProperty idProperty = targetDescriptor.getIdProperty();
query.fetch(fullPath, idProperty.getName());
}
/**
* Cascade the merge processing if this has child nodes.
*/
protected void cascade(EntityBean entityBean, EntityBean outlineBean, MergeRequest request) {
if (children != null && !children.isEmpty()) {
MergeRequest sub = request.sub(entityBean, outlineBean);
for (MergeNode node : children.values()) {
node.merge(sub);
}
}
}
}
@@ -0,0 +1,57 @@
package io.ebeaninternal.server.persist;
import io.ebean.bean.EntityBean;
import io.ebeaninternal.server.deploy.BeanPropertyAssocMany;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
/**
* Node for processing merge on ToMany properties.
*/
class MergeNodeAssocMany extends MergeNode {
private final BeanPropertyAssocMany<?> many;
MergeNodeAssocMany(String fullPath, BeanPropertyAssocMany<?> property) {
super(fullPath, property);
this.many = property;
}
public void merge(MergeRequest request) {
Collection beans = many.getRawCollection(request.getBean());
Collection outlines = many.getRawCollection(request.getOutline());
Map<Object, EntityBean> outlineIds = new HashMap<>();
if (outlines != null) {
for (Object outline : outlines) {
EntityBean outlineBean = (EntityBean) outline;
Object outlineId = targetDescriptor.getId(outlineBean);
outlineIds.put(outlineId, outlineBean);
}
}
if (beans != null) {
for (Object bean : beans) {
EntityBean entityBean = (EntityBean) bean;
Object beanId = targetDescriptor.getId(entityBean);
if (beanId != null) {
EntityBean outlineBean = outlineIds.remove(beanId);
if (outlineBean != null) {
// must be an update
entityBean._ebean_getIntercept().setForceUpdate(true);
cascade(entityBean, outlineBean, request);
}
}
}
}
// any remaining are considered deletes
for (EntityBean outlineBean : outlineIds.values()) {
request.addDelete(outlineBean);
}
}
}
@@ -0,0 +1,60 @@
package io.ebeaninternal.server.persist;
import io.ebean.bean.EntityBean;
import io.ebeaninternal.server.deploy.BeanPropertyAssocOne;
import java.util.Objects;
/**
* Node for processing merge on ToOne properties.
*/
class MergeNodeAssocOne extends MergeNode {
private final BeanPropertyAssocOne<?> one;
MergeNodeAssocOne(String fullPath, BeanPropertyAssocOne<?> property) {
super(fullPath, property);
this.one = property;
}
public void merge(MergeRequest request) {
EntityBean entityBean = getEntityBean(request.getBean());
if (entityBean == null) {
checkOrphanRemoval(request);
} else {
Object beanId = targetDescriptor.getId(entityBean);
if (beanId == null) {
checkOrphanRemoval(request);
} else {
EntityBean outlineBean = getEntityBean(request.getOutline());
Object outlineId = (outlineBean == null) ? null : targetDescriptor.getId(outlineBean);
if (isUpdate(beanId, outlineId, request)) {
entityBean._ebean_getIntercept().setForceUpdate(true);
cascade(entityBean, outlineBean, request);
}
}
}
}
private void checkOrphanRemoval(MergeRequest request) {
if (one.isOrphanRemoval()) {
EntityBean outlineBean = getEntityBean(request.getOutline());
if (outlineBean != null) {
request.addDelete(outlineBean);
}
}
}
private boolean isUpdate(Object beanId, Object outlineId, MergeRequest request) {
return Objects.equals(beanId, outlineId)
|| !request.isClientGeneratedIds()
|| request.idExists(targetDescriptor.getBeanType(), beanId);
}
private EntityBean getEntityBean(Object bean) {
return (EntityBean) one.getVal(bean);
}
}
@@ -0,0 +1,64 @@
package io.ebeaninternal.server.persist;
import io.ebean.bean.EntityBean;
/**
* Request object used for processing the merge.
*/
class MergeRequest {
private final EntityBean bean;
private final EntityBean outline;
private final MergeContext context;
MergeRequest(MergeContext context, EntityBean bean, EntityBean outline) {
this.context = context;
this.bean = bean;
this.outline = outline;
}
/**
* Create a sub request with the given beans (to cascade the processing).
*/
public MergeRequest sub(EntityBean entityBean, EntityBean outlineBean) {
return new MergeRequest(context, entityBean, outlineBean);
}
/**
* Return the entity bean being merged.
*/
public EntityBean getBean() {
return bean;
}
/**
* Return the outline bean (only has Id property).
*/
public EntityBean getOutline() {
return outline;
}
/**
* Add a bean to the deletion list.
*/
public void addDelete(EntityBean deleteRemain) {
context.addDelete(deleteRemain);
}
/**
* Return true if the Ids are generated by the client. This means we can't know if a bean
* should be inserted or updated based on having an Id value.
*/
public boolean isClientGeneratedIds() {
return context.isClientGeneratedIds();
}
/**
* Return true if a bean of the type with the given Id exists in the database.
*/
public boolean idExists(Class<?> beanType, Object beanId) {
return context.idExists(beanType, beanId);
}
}
@@ -21,6 +21,7 @@ import java.util.Map;
*/
class SaveManyPropRequest {
private final PersistRequestBean<?> request;
private final boolean insertedParent;
private final BeanPropertyAssocMany<?> many;
private final EntityBean parentBean;
@@ -37,10 +38,10 @@ class SaveManyPropRequest {
private Collection<?> collection;
private DefaultPersister persister;
private boolean deleteMissing;
private boolean insertMode;
private int sortOrder;
SaveManyPropRequest(boolean insertedParent, BeanPropertyAssocMany<?> many, EntityBean parentBean, PersistRequestBean<?> request) {
this.request = request;
this.insertedParent = insertedParent;
this.many = many;
this.cascade = many.getCascadeInfo().isSave();
@@ -70,6 +71,10 @@ class SaveManyPropRequest {
return deleteMissingChildren;
}
public int getFlags() {
return request.getFlags();
}
boolean isInsertedParent() {
return insertedParent;
}
@@ -108,11 +113,10 @@ class SaveManyPropRequest {
}
}
void saveDetails(DefaultPersister persister, boolean deleteMissing, boolean insertMode) {
void saveDetails(DefaultPersister persister, boolean deleteMissing) {
this.persister = persister;
this.deleteMissing = deleteMissing;
this.insertMode = insertMode;
// check that the list is not null and if it is a BeanCollection
// check that is has been populated (don't trigger lazy loading)
@@ -198,7 +202,7 @@ class SaveManyPropRequest {
}
if (!skipSavingThisBean) {
persister.saveRecurse(detail, transaction, parentBean, insertMode, publish);
persister.saveRecurse(detail, transaction, parentBean, request.getFlags());
}
}
}