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 {
*
* This call determines the PersistRequest.Type based on bean state and the insert flag (root persist type).
*/
- private PersistRequestBean createRequestRecurse(T bean, Transaction t, Object parentBean, boolean insertMode, boolean publish) {
+ private PersistRequestBean createRequestRecurse(T bean, Transaction t, Object parentBean, int flags) {
BeanManager mgr = getBeanManager(bean);
if (mgr == null) {
throw new PersistenceException(errNotRegistered(bean.getClass()));
@@ -1349,14 +1373,15 @@ public final class DefaultPersister implements Persister {
BeanDescriptor 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 PersistRequestBean 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) {
diff --git a/src/main/java/io/ebeaninternal/server/persist/Flags.java b/src/main/java/io/ebeaninternal/server/persist/Flags.java
new file mode 100644
index 000000000..ffde410c5
--- /dev/null
+++ b/src/main/java/io/ebeaninternal/server/persist/Flags.java
@@ -0,0 +1,135 @@
+package io.ebeaninternal.server.persist;
+
+/**
+ * Flags used in persistence.
+ *
+ * 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;
+ }
+ }
+}
diff --git a/src/main/java/io/ebeaninternal/server/persist/MergeContext.java b/src/main/java/io/ebeaninternal/server/persist/MergeContext.java
new file mode 100644
index 000000000..8de5b7ee4
--- /dev/null
+++ b/src/main/java/io/ebeaninternal/server/persist/MergeContext.java
@@ -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 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 getDeletedBeans() {
+ return deleteBeans;
+ }
+}
diff --git a/src/main/java/io/ebeaninternal/server/persist/MergeHandler.java b/src/main/java/io/ebeaninternal/server/persist/MergeHandler.java
new file mode 100644
index 000000000..c82febba7
--- /dev/null
+++ b/src/main/java/io/ebeaninternal/server/persist/MergeHandler.java
@@ -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 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 merge() {
+
+ Set 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.
+ *
+ * We use the Id values to determine what are inserts, updates and deletes as part of the merge.
+ */
+ private EntityBean fetchOutline(Set 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);
+ }
+ }
+}
diff --git a/src/main/java/io/ebeaninternal/server/persist/MergeNode.java b/src/main/java/io/ebeaninternal/server/persist/MergeNode.java
new file mode 100644
index 000000000..0f7e47a48
--- /dev/null
+++ b/src/main/java/io/ebeaninternal/server/persist/MergeNode.java
@@ -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 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);
+ }
+ }
+ }
+}
diff --git a/src/main/java/io/ebeaninternal/server/persist/MergeNodeAssocMany.java b/src/main/java/io/ebeaninternal/server/persist/MergeNodeAssocMany.java
new file mode 100644
index 000000000..d6e1a8e8f
--- /dev/null
+++ b/src/main/java/io/ebeaninternal/server/persist/MergeNodeAssocMany.java
@@ -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 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);
+ }
+ }
+
+}
diff --git a/src/main/java/io/ebeaninternal/server/persist/MergeNodeAssocOne.java b/src/main/java/io/ebeaninternal/server/persist/MergeNodeAssocOne.java
new file mode 100644
index 000000000..b33c393c5
--- /dev/null
+++ b/src/main/java/io/ebeaninternal/server/persist/MergeNodeAssocOne.java
@@ -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);
+ }
+}
diff --git a/src/main/java/io/ebeaninternal/server/persist/MergeRequest.java b/src/main/java/io/ebeaninternal/server/persist/MergeRequest.java
new file mode 100644
index 000000000..303ccb382
--- /dev/null
+++ b/src/main/java/io/ebeaninternal/server/persist/MergeRequest.java
@@ -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);
+ }
+
+}
diff --git a/src/main/java/io/ebeaninternal/server/persist/SaveManyPropRequest.java b/src/main/java/io/ebeaninternal/server/persist/SaveManyPropRequest.java
index 4556edcd2..97f9d1131 100644
--- a/src/main/java/io/ebeaninternal/server/persist/SaveManyPropRequest.java
+++ b/src/main/java/io/ebeaninternal/server/persist/SaveManyPropRequest.java
@@ -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());
}
}
}
diff --git a/src/test/java/io/ebeaninternal/api/TDSpiEbeanServer.java b/src/test/java/io/ebeaninternal/api/TDSpiEbeanServer.java
index 62265c0fb..5755f6ed5 100644
--- a/src/test/java/io/ebeaninternal/api/TDSpiEbeanServer.java
+++ b/src/test/java/io/ebeaninternal/api/TDSpiEbeanServer.java
@@ -11,6 +11,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.Query;
@@ -236,6 +237,16 @@ public class TDSpiEbeanServer implements SpiEbeanServer {
return 0;
}
+ @Override
+ public void merge(Object bean, MergeOptions options) {
+
+ }
+
+ @Override
+ public void merge(Object bean, MergeOptions options, Transaction transaction) {
+
+ }
+
@Override
public List> findVersions(Query query, Transaction transaction) {
return null;
@@ -551,6 +562,11 @@ public class TDSpiEbeanServer implements SpiEbeanServer {
}
+ @Override
+ public boolean exists(Class> beanType, Object beanId, Transaction transaction) {
+ return false;
+ }
+
@Override
public T find(Class beanType, Object uid) {
return null;
diff --git a/src/test/java/io/ebeaninternal/server/persist/BatchedBeanHolderTest.java b/src/test/java/io/ebeaninternal/server/persist/BatchedBeanHolderTest.java
index 2e200abd1..12a7acef0 100644
--- a/src/test/java/io/ebeaninternal/server/persist/BatchedBeanHolderTest.java
+++ b/src/test/java/io/ebeaninternal/server/persist/BatchedBeanHolderTest.java
@@ -6,8 +6,8 @@ import io.ebeaninternal.server.core.PersistRequest;
import io.ebeaninternal.server.core.PersistRequestBean;
import io.ebeaninternal.server.deploy.BeanDescriptor;
import io.ebeaninternal.server.deploy.BeanManager;
-import org.tests.model.basic.Customer;
import org.junit.Test;
+import org.tests.model.basic.Customer;
import java.sql.Timestamp;
@@ -17,8 +17,7 @@ public class BatchedBeanHolderTest extends BaseTestCase {
@SuppressWarnings({ "rawtypes", "unchecked" })
@Test
- public void testAppend() throws Exception {
-
+ public void testAppend() {
TDSpiEbeanServer server = new TDSpiEbeanServer("foo");
@@ -32,13 +31,13 @@ public class BatchedBeanHolderTest extends BaseTestCase {
Customer customer = new Customer();
customer.setUpdtime(new Timestamp(System.currentTimeMillis()));
- PersistRequestBean req1 = new PersistRequestBean(server, customer, null, beanManager, null, null, PersistRequest.Type.INSERT, false, false);
+ PersistRequestBean req1 = new PersistRequestBean(server, customer, null, beanManager, null, null, PersistRequest.Type.INSERT, 0);
int size = holder.append(req1);
assertEquals(1, size);
- PersistRequestBean req2 = new PersistRequestBean(server, customer, null, beanManager, null, null, PersistRequest.Type.INSERT, false, false);
+ PersistRequestBean req2 = new PersistRequestBean(server, customer, null, beanManager, null, null, PersistRequest.Type.INSERT, 0);
size = holder.append(req2);
assertEquals(0, size);
diff --git a/src/test/java/io/ebeaninternal/server/persist/FlagsTest.java b/src/test/java/io/ebeaninternal/server/persist/FlagsTest.java
new file mode 100644
index 000000000..4e4d947c4
--- /dev/null
+++ b/src/test/java/io/ebeaninternal/server/persist/FlagsTest.java
@@ -0,0 +1,44 @@
+package io.ebeaninternal.server.persist;
+
+
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class FlagsTest {
+
+
+ @Test
+ public void test() {
+
+ int state = 0;
+
+ state = Flags.setPublish(state);
+ assertThat(Flags.isSet(state, Flags.PUBLISH)).isTrue();
+
+ state = Flags.setMerge(state);
+ state = Flags.setInsert(state);
+ assertThat(Flags.isSet(state, Flags.PUBLISH)).isTrue();
+ assertThat(Flags.isSet(state, Flags.MERGE)).isTrue();
+ assertThat(Flags.isSet(state, Flags.INSERT)).isTrue();
+
+ state = Flags.unsetPublish(state);
+ assertThat(Flags.isSet(state, Flags.PUBLISH)).isFalse();
+ assertThat(Flags.isSet(state, Flags.MERGE)).isTrue();
+ assertThat(Flags.isSet(state, Flags.INSERT)).isTrue();
+
+ }
+
+ @Test
+ public void isPublishOrMerge() {
+
+ assertThat(Flags.isPublishOrMerge(0)).isFalse();
+ assertThat(Flags.isPublishOrMerge(Flags.INSERT)).isFalse();
+
+ assertThat(Flags.isPublishOrMerge(Flags.PUBLISH)).isTrue();
+ assertThat(Flags.isPublishOrMerge(Flags.MERGE)).isTrue();
+
+ int mergePublish = Flags.setMerge(Flags.setPublish(0));
+ assertThat(Flags.isPublishOrMerge(mergePublish)).isTrue();
+ }
+}
diff --git a/src/test/java/org/tests/merge/MAddress.java b/src/test/java/org/tests/merge/MAddress.java
new file mode 100644
index 000000000..cfeb31bb6
--- /dev/null
+++ b/src/test/java/org/tests/merge/MAddress.java
@@ -0,0 +1,33 @@
+package org.tests.merge;
+
+import javax.persistence.Entity;
+
+@Entity
+public class MAddress extends MBase {
+
+ private String street;
+
+ private String city;
+
+ public MAddress(String street, String city) {
+ this.street = street;
+ this.city = city;
+ }
+
+ public String getStreet() {
+ return street;
+ }
+
+ public void setStreet(String street) {
+ this.street = street;
+ }
+
+ public String getCity() {
+ return city;
+ }
+
+ public void setCity(String city) {
+ this.city = city;
+ }
+
+}
diff --git a/src/test/java/org/tests/merge/MBase.java b/src/test/java/org/tests/merge/MBase.java
new file mode 100644
index 000000000..0172e9893
--- /dev/null
+++ b/src/test/java/org/tests/merge/MBase.java
@@ -0,0 +1,32 @@
+package org.tests.merge;
+
+import javax.persistence.Id;
+import javax.persistence.MappedSuperclass;
+import javax.persistence.Version;
+import java.util.UUID;
+
+@MappedSuperclass
+public class MBase {
+
+ @Id
+ private UUID id;
+
+ @Version
+ private long version;
+
+ public UUID getId() {
+ return id;
+ }
+
+ public void setId(UUID id) {
+ this.id = id;
+ }
+
+ public long getVersion() {
+ return version;
+ }
+
+ public void setVersion(long version) {
+ this.version = version;
+ }
+}
diff --git a/src/test/java/org/tests/merge/MContact.java b/src/test/java/org/tests/merge/MContact.java
new file mode 100644
index 000000000..9331bbb43
--- /dev/null
+++ b/src/test/java/org/tests/merge/MContact.java
@@ -0,0 +1,67 @@
+package org.tests.merge;
+
+import javax.persistence.CascadeType;
+import javax.persistence.Entity;
+import javax.persistence.ManyToOne;
+import javax.persistence.OneToMany;
+import java.util.List;
+
+@Entity
+public class MContact extends MBase {
+
+ String email;
+ String firstName;
+ String lastName;
+
+ @ManyToOne
+ MCustomer customer;
+
+ @OneToMany(cascade = CascadeType.ALL, mappedBy = "contact")
+ List messages;
+
+ public MContact(String email, String firstName, String lastName) {
+ this.email = email;
+ this.firstName = firstName;
+ this.lastName = lastName;
+ }
+
+ public MCustomer getCustomer() {
+ return customer;
+ }
+
+ public void setCustomer(MCustomer customer) {
+ this.customer = customer;
+ }
+
+ public String getEmail() {
+ return email;
+ }
+
+ public void setEmail(String email) {
+ this.email = email;
+ }
+
+ public String getFirstName() {
+ return firstName;
+ }
+
+ public void setFirstName(String firstName) {
+ this.firstName = firstName;
+ }
+
+ public String getLastName() {
+ return lastName;
+ }
+
+ public void setLastName(String lastName) {
+ this.lastName = lastName;
+ }
+
+ public List getMessages() {
+ return messages;
+ }
+
+ public void setMessages(List messages) {
+ this.messages = messages;
+ }
+}
diff --git a/src/test/java/org/tests/merge/MContactMessage.java b/src/test/java/org/tests/merge/MContactMessage.java
new file mode 100644
index 000000000..95d22026f
--- /dev/null
+++ b/src/test/java/org/tests/merge/MContactMessage.java
@@ -0,0 +1,54 @@
+package org.tests.merge;
+
+import javax.persistence.Entity;
+import javax.persistence.ManyToOne;
+
+@Entity
+public class MContactMessage extends MBase {
+
+ private String title;
+
+ private String subject;
+
+ private String notes;
+
+ @ManyToOne(optional = false)
+ private MContact contact;
+
+ public MContactMessage(String title, String subject) {
+ this.title = title;
+ this.subject = subject;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public String getNotes() {
+ return notes;
+ }
+
+ public void setNotes(String notes) {
+ this.notes = notes;
+ }
+
+ public String getSubject() {
+ return subject;
+ }
+
+ public void setSubject(String subject) {
+ this.subject = subject;
+ }
+
+ public MContact getContact() {
+ return contact;
+ }
+
+ public void setContact(MContact contact) {
+ this.contact = contact;
+ }
+}
diff --git a/src/test/java/org/tests/merge/MCustomer.java b/src/test/java/org/tests/merge/MCustomer.java
new file mode 100644
index 000000000..802f8000b
--- /dev/null
+++ b/src/test/java/org/tests/merge/MCustomer.java
@@ -0,0 +1,68 @@
+package org.tests.merge;
+
+import javax.persistence.CascadeType;
+import javax.persistence.Entity;
+import javax.persistence.ManyToOne;
+import javax.persistence.OneToMany;
+import java.util.List;
+
+@Entity
+public class MCustomer extends MBase {
+
+ private String name;
+
+ private String notes;
+
+ @OneToMany(cascade = CascadeType.ALL, mappedBy = "customer")
+ private List contacts;
+
+ @ManyToOne(cascade = CascadeType.ALL)
+ private MAddress shippingAddress;
+
+ @ManyToOne(cascade = CascadeType.ALL)
+ private MAddress billingAddress;
+
+ public MCustomer(String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getNotes() {
+ return notes;
+ }
+
+ public void setNotes(String notes) {
+ this.notes = notes;
+ }
+
+ public List getContacts() {
+ return contacts;
+ }
+
+ public void setContacts(List contacts) {
+ this.contacts = contacts;
+ }
+
+ public MAddress getShippingAddress() {
+ return shippingAddress;
+ }
+
+ public void setShippingAddress(MAddress shippingAddress) {
+ this.shippingAddress = shippingAddress;
+ }
+
+ public MAddress getBillingAddress() {
+ return billingAddress;
+ }
+
+ public void setBillingAddress(MAddress billingAddress) {
+ this.billingAddress = billingAddress;
+ }
+}
diff --git a/src/test/java/org/tests/merge/TestMergeBasic.java b/src/test/java/org/tests/merge/TestMergeBasic.java
new file mode 100644
index 000000000..03938fa71
--- /dev/null
+++ b/src/test/java/org/tests/merge/TestMergeBasic.java
@@ -0,0 +1,117 @@
+package org.tests.merge;
+
+import io.ebean.BaseTestCase;
+import io.ebean.Ebean;
+import io.ebean.MergeOptions;
+import io.ebean.MergeOptionsBuilder;
+import org.ebeantest.LoggedSqlCollector;
+import org.junit.Test;
+import org.tests.model.basic.UUOne;
+import org.tests.model.basic.UUTwo;
+
+import java.util.List;
+import java.util.UUID;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class TestMergeBasic extends BaseTestCase {
+
+
+ private UUOne rebuildViaJson(UUOne input) {
+ String asJson = Ebean.json().toJson(input);
+ return Ebean.json().toBean(UUOne.class, asJson);
+ }
+
+ private UUOne build() {
+ UUOne uuOne = buildGraph();
+ Ebean.save(uuOne);
+ return rebuildViaJson(uuOne);
+ }
+
+ @Test
+ public void with_setClientGeneratedIds_expect_noDifferenceWithToMany() {
+
+ UUOne one = build();
+
+ one.setDescription("mod");
+ List comments = one.getComments();
+ comments.remove(0);
+ comments.add(0, new UUTwo("twoMore", UUID.randomUUID()));
+ comments.add(new UUTwo("twoExtra", UUID.randomUUID()));
+
+ MergeOptions options = new MergeOptionsBuilder()
+ .addPath("comments")
+ .setClientGeneratedIds()
+ .build();
+
+ LoggedSqlCollector.start();
+ Ebean.merge(one, options);
+
+
+ List sql = LoggedSqlCollector.stop();
+ // fetch the Ids ... used to identity inserts, updates and deletes
+ assertThat(sql.get(0)).contains("select t0.id, t1.id from uuone t0 left join uutwo t1 on t1.master_id = t0.id where t0.id = ?");
+
+ // deletes of Ids that are no longer in the graph
+ assertThat(sql.get(1)).contains("delete from uutwo where id=?");
+
+ // cascade persist ... master
+ assertThat(sql.get(2)).contains("update uuone set name=?, description=?, version=? where id=? and version=?");
+
+ // persist children ...
+ assertThat(sql.get(3)).contains("insert into uutwo (id, name, notes, version, master_id) values (?,?,?,?,?);");
+ assertThat(sql.get(4)).contains("insert into uutwo (id, name, notes, version, master_id) values (?,?,?,?,?);");
+
+ assertThat(sql.get(5)).contains("update uutwo set name=?, notes=?, version=?, master_id=? where id=? and version=?");
+ assertThat(sql.get(6)).contains("update uutwo set name=?, notes=?, version=?, master_id=? where id=? and version=?");
+ assertThat(sql.get(7)).contains("update uutwo set name=?, notes=?, version=?, master_id=? where id=? and version=?");
+ }
+
+ @Test
+ public void test() {
+
+ UUOne one = build();
+ one.setDescription("mod");
+
+ List comments = one.getComments();
+ comments.remove(0);
+ comments.add(0, new UUTwo("twoMore", UUID.randomUUID()));
+ comments.add(new UUTwo("twoExtra", UUID.randomUUID()));
+
+ MergeOptions options = new MergeOptionsBuilder()
+ .addPath("comments")
+ .build();
+
+ LoggedSqlCollector.start();
+ Ebean.merge(one, options);
+
+
+ List sql = LoggedSqlCollector.stop();
+ // fetch the Ids ... used to identity inserts, updates and deletes
+ assertThat(sql.get(0)).contains("select t0.id, t1.id from uuone t0 left join uutwo t1 on t1.master_id = t0.id where t0.id = ?");
+
+ // deletes of Ids that are no longer in the graph
+ assertThat(sql.get(1)).contains("delete from uutwo where id=?");
+
+ // cascade persist ... master
+ assertThat(sql.get(2)).contains("update uuone set name=?, description=?, version=? where id=? and version=?");
+
+ // persist children ...
+ assertThat(sql.get(3)).contains("insert into uutwo (id, name, notes, version, master_id) values (?,?,?,?,?);");
+ assertThat(sql.get(4)).contains("insert into uutwo (id, name, notes, version, master_id) values (?,?,?,?,?);");
+
+ assertThat(sql.get(5)).contains("update uutwo set name=?, notes=?, version=?, master_id=? where id=? and version=?");
+ assertThat(sql.get(6)).contains("update uutwo set name=?, notes=?, version=?, master_id=? where id=? and version=?");
+ assertThat(sql.get(7)).contains("update uutwo set name=?, notes=?, version=?, master_id=? where id=? and version=?");
+
+ }
+
+ private UUOne buildGraph() {
+ UUOne one = new UUOne("one1", UUID.randomUUID());
+ for (int i = 1; i < 5; i++) {
+ UUTwo two = new UUTwo("two" + i, UUID.randomUUID());
+ one.getComments().add(two);
+ }
+ return one;
+ }
+}
diff --git a/src/test/java/org/tests/merge/TestMergeCustomer.java b/src/test/java/org/tests/merge/TestMergeCustomer.java
new file mode 100644
index 000000000..8f314cac5
--- /dev/null
+++ b/src/test/java/org/tests/merge/TestMergeCustomer.java
@@ -0,0 +1,376 @@
+package org.tests.merge;
+
+import io.ebean.BaseTestCase;
+import io.ebean.Ebean;
+import io.ebean.FetchPath;
+import io.ebean.MergeOptions;
+import io.ebean.MergeOptionsBuilder;
+import io.ebean.text.PathProperties;
+import org.ebeantest.LoggedSqlCollector;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+import java.util.UUID;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class TestMergeCustomer extends BaseTestCase {
+
+ private Random random = new Random();
+
+ /**
+ * So this is effectively the same as a stateless update.
+ */
+ @Test
+ public void customerOnly_expect_updateOnly() {
+
+
+ MCustomer mCustomer = partial("cust1", "(id,name,version)");
+ mCustomer.setName("NotCust1");
+
+ MergeOptions options = new MergeOptionsBuilder().build();
+
+
+ LoggedSqlCollector.start();
+
+ Ebean.merge(mCustomer, options);
+
+ List sql = LoggedSqlCollector.stop();
+ assertThat(sql).hasSize(1);
+ assertThat(sql.get(0)).contains("update mcustomer set name=?, version=? where id=? and version=?");
+ }
+
+ @Test
+ public void customerOnly_setClientGeneratedIds_expect_selectAndUpdate() {
+
+
+ MCustomer mCustomer = partial("cust2", "(id,name,version)");
+ mCustomer.setName("NotCust2");
+
+ MergeOptions options = new MergeOptionsBuilder().setClientGeneratedIds().build();
+
+ LoggedSqlCollector.start();
+
+ server().merge(mCustomer, options);
+
+ List sql = LoggedSqlCollector.stop();
+ assertThat(sql).hasSize(2);
+ assertThat(sql.get(0)).contains("select t0.id from mcustomer t0 where t0.id = ?");
+ assertThat(sql.get(1)).contains("update mcustomer set name=?, version=? where id=? and version=?");
+ }
+
+ @Test
+ public void customerWithAddresses_setClientGeneratedIds_expect_selectAndUpdate() {
+
+
+ MCustomer mCustomer = partial("cust3", "(id,name,version,shippingAddress(*),billingAddress(*))");
+ mCustomer.setName("NotCust3");
+ mCustomer.getBillingAddress().setStreet("modBillStreet");
+ mCustomer.getShippingAddress().setCity("modShipCity");
+
+ MergeOptions options = new MergeOptionsBuilder()
+ .addPath("shippingAddress")
+ .addPath("billingAddress")
+ .setClientGeneratedIds()
+ .build();
+
+ LoggedSqlCollector.start();
+
+ server().merge(mCustomer, options);
+
+ List sql = LoggedSqlCollector.stop();
+ assertThat(sql).hasSize(4);
+ assertThat(sql.get(0)).contains("select t0.id, t2.id, t1.id from mcustomer t0 left join maddress t2 on t2.id = t0.shipping_address_id left join maddress t1 on t1.id = t0.billing_address_id where t0.id = ?");
+ assertThat(sql.get(1)).contains("update maddress set street=?, city=?, version=? where id=? and version=?");
+ assertThat(sql.get(2)).contains("update maddress set street=?, city=?, version=? where id=? and version=?");
+ assertThat(sql.get(3)).contains("update mcustomer set name=?, version=?, shipping_address_id=?, billing_address_id=? where id=? and version=?");
+ }
+
+ @Test
+ public void customerWithAddresses_newAddress_setClientGeneratedIds_expect_insertAddress() {
+
+
+ MCustomer mCustomer = partial("cust3", "(id,name,version,shippingAddress(*),billingAddress(*))");
+ mCustomer.setName("NotCust3");
+
+ // new billing address - no Id value so must be insert
+ mCustomer.setBillingAddress(new MAddress("Short", "Mid Wicket"));
+ mCustomer.getShippingAddress().setCity("modShipCity");
+
+ MergeOptions options = new MergeOptionsBuilder()
+ .addPath("shippingAddress")
+ .addPath("billingAddress")
+ .setClientGeneratedIds()
+ .build();
+
+ LoggedSqlCollector.start();
+
+ server().merge(mCustomer, options);
+
+ List sql = LoggedSqlCollector.stop();
+ assertThat(sql).hasSize(4);
+ assertThat(sql.get(0)).contains("select t0.id, t2.id, t1.id from mcustomer t0 left join maddress t2 on t2.id = t0.shipping_address_id left join maddress t1 on t1.id = t0.billing_address_id where t0.id = ?");
+ assertThat(sql.get(1)).contains("update maddress set street=?, city=?, version=? where id=? and version=?");
+ assertThat(sql.get(2)).contains("insert into maddress (id, street, city, version) values (?,?,?,?);");
+ assertThat(sql.get(3)).contains("update mcustomer set name=?, version=?, shipping_address_id=?, billing_address_id=? where id=? and version=?");
+ }
+
+ @Test
+ public void customerWithAddresses_newAddressWithId_setClientGeneratedIds_expect_additionalCheckForAddressInsert() {
+
+
+ MCustomer mCustomer = partial("cust3", "(id,name,version,shippingAddress(*),billingAddress(*))");
+ mCustomer.setName("NotCust3");
+
+ // new billing address - has Id value + setClientGeneratedIds ... so extra query to check
+ MAddress mAddress = new MAddress("Short", "Mid Wicket");
+ mAddress.setId(UUID.randomUUID());
+ mCustomer.setBillingAddress(mAddress);
+ mCustomer.getShippingAddress().setCity("modShipCity");
+
+ MergeOptions options = new MergeOptionsBuilder()
+ .addPath("shippingAddress")
+ .addPath("billingAddress")
+ .setClientGeneratedIds() // As we are using clientIds ... we don't know if the new UUID is an insert or update without checking
+ .build();
+
+ LoggedSqlCollector.start();
+
+ server().merge(mCustomer, options);
+
+ List sql = LoggedSqlCollector.stop();
+ assertThat(sql).hasSize(5);
+ assertThat(sql.get(0)).contains("select t0.id, t2.id, t1.id from mcustomer t0 left join maddress t2 on t2.id = t0.shipping_address_id left join maddress t1 on t1.id = t0.billing_address_id where t0.id = ?");
+
+ // Additional check to see if the address with the unknown UUID is 'insert' or 'update'
+ assertThat(sql.get(1)).contains("select t0.id from maddress t0 where t0.id = ?");
+
+ assertThat(sql.get(2)).contains("update maddress set street=?, city=?, version=? where id=? and version=?");
+ assertThat(sql.get(3)).contains("insert into maddress (id, street, city, version) values (?,?,?,?);");
+ assertThat(sql.get(4)).contains("update mcustomer set name=?, version=?, shipping_address_id=?, billing_address_id=? where id=? and version=?");
+ }
+
+ @Test
+ public void assocOne_onlineIsNull() {
+
+ MCustomer c = new MCustomer("Null Address");
+ c.setBillingAddress(new MAddress("Cow corner", "Mid Wicket"));
+
+ Ebean.save(c);
+
+ MCustomer mCustomer = rebuildViaJson(c);
+
+ MAddress mAddress = new MAddress("Silly", "Mid Wicket");
+ mAddress.setId(UUID.randomUUID());
+ mCustomer.setShippingAddress(mAddress);
+
+ MergeOptions options = new MergeOptionsBuilder()
+ .addPath("shippingAddress")
+ .addPath("billingAddress")
+ .setClientGeneratedIds()
+ .build();
+
+ Ebean.merge(mCustomer, options);
+
+ }
+
+ private MCustomer rebuildViaJson(MCustomer input) {
+ String asJson = Ebean.json().toJson(input);
+ return Ebean.json().toBean(MCustomer.class, asJson);
+ }
+
+ @Test
+ public void whenContacts_isNull_expect_deleteContacts() {
+
+ MCustomer mCustomer = partial("cust6", "(id,name,version,shippingAddress(id),billingAddress(id),contacts(*))");
+
+ // null contacts ... but in path so this means - delete all contacts
+ mCustomer.setContacts(null);
+
+ MergeOptions options = new MergeOptionsBuilder()
+ .addPath("contacts")
+ .build();
+
+ LoggedSqlCollector.start();
+ server().merge(mCustomer, options);
+
+ List sql = LoggedSqlCollector.stop();
+ assertThat(sql.get(0)).contains("select t0.id, t1.id from mcustomer t0 left join mcontact t1 on t1.customer_id = t0.id where t0.id = ?");
+ assertThat(sql.get(1)).contains("delete from mcontact_message where contact_id = ?");
+ assertThat(sql.get(2)).contains("delete from mcontact where id=?");
+
+ assertThat(sql.get(13)).contains("update mcustomer set name=?, version=?, shipping_address_id=?, billing_address_id=? where id=? and version=?");
+ }
+
+
+ @Test
+ public void whenContacts_isEmpty_expect_deleteContacts() {
+
+ MCustomer mCustomer = partial("cust6", "(id,name,version,shippingAddress(id),billingAddress(id),contacts(*))");
+
+ // empty contacts ... but in path so this means - delete all contacts
+ mCustomer.setContacts(new ArrayList<>());
+
+ MergeOptions options = new MergeOptionsBuilder()
+ .addPath("contacts")
+ .build();
+
+ LoggedSqlCollector.start();
+ server().merge(mCustomer, options);
+
+ List sql = LoggedSqlCollector.stop();
+ assertThat(sql.get(0)).contains("select t0.id, t1.id from mcustomer t0 left join mcontact t1 on t1.customer_id = t0.id where t0.id = ?");
+ assertThat(sql.get(1)).contains("delete from mcontact_message where contact_id = ?");
+ assertThat(sql.get(2)).contains("delete from mcontact where id=?");
+
+ assertThat(sql.get(13)).contains("update mcustomer set name=?, version=?, shipping_address_id=?, billing_address_id=? where id=? and version=?");
+ }
+
+ @Test
+ public void whenContacts_mixed_expect_deleteInsertUpdateContacts() {
+
+ MCustomer mCustomer = partial("cust6", "(id,name,version,shippingAddress(id),billingAddress(id),contacts(*))");
+ List contacts = mCustomer.getContacts();
+
+ MContact mContact = new MContact("z@a.com", "z", "zed");
+ mContact.setId(UUID.randomUUID());
+
+ contacts.add(mContact);
+
+ contacts.get(0).setEmail("a@beta.com");
+ contacts.get(1).setEmail("b@beta.com");
+ contacts.remove(4);
+ contacts.remove(2);
+
+ MContact mContactEnd = new MContact("z@z.com", "zx", "zedXtra");
+ mContactEnd.setId(UUID.randomUUID());
+ contacts.add(mContactEnd);
+
+
+ MergeOptions options = new MergeOptionsBuilder()
+ .addPath("contacts")
+ .build();
+
+ LoggedSqlCollector.start();
+ server().merge(mCustomer, options);
+
+ List sql = LoggedSqlCollector.stop();
+ assertThat(sql.get(0)).contains("select t0.id, t1.id from mcustomer t0 left join mcontact t1 on t1.customer_id = t0.id where t0.id = ?");
+ assertThat(sql.get(1)).contains("delete from mcontact_message where contact_id = ?");
+ assertThat(sql.get(2)).contains("delete from mcontact where id=?");
+
+ assertThat(sql.get(5)).contains("update mcustomer set name=?, version=?, shipping_address_id=?, billing_address_id=? where id=? and version=?");
+ assertThat(sql.get(6)).contains("insert into mcontact");
+ assertThat(sql.get(7)).contains("insert into mcontact");
+ assertThat(sql.get(8)).contains("update mcontact set email=?, first_name=?, last_name=?, version=?, customer_id=? where id=? and version=?");
+ assertThat(sql.get(11)).contains("update mcontact set email=?, first_name=?, last_name=?, version=?, customer_id=? where id=? and version=?");
+ }
+
+ @Test
+ public void fullMonty() {
+
+ MCustomer cust1 = customer("monty1");
+
+ modify(cust1);
+
+ MergeOptions options = new MergeOptionsBuilder()
+ .addPath("billingAddress")
+ .addPath("shippingAddress")
+ .addPath("contacts")
+ .addPath("contacts.messages")
+ .setClientGeneratedIds()
+ .setDeletePermanent()
+ .build();
+
+
+ LoggedSqlCollector.start();
+ server().merge(cust1, options);
+
+ List sql = LoggedSqlCollector.stop();
+ assertThat(sql.get(0)).contains("select t0.id, t3.id, t1.id, t2.id from mcustomer t0 left join maddress t3 on t3.id = t0.shipping_address_id left join maddress t1 on t1.id = t0.billing_address_id left join mcontact t2 on t2.customer_id = t0.id where t0.id = ?");
+ if (isH2()) {
+ // with nested OneToMany .. we need a second query to read the contact message ids
+ assertThat(sql.get(1)).contains("select t0.contact_id, t0.id from mcontact_message t0 where (t0.contact_id) in (?, ?, ?, ?, ?, ?, ?, ?, ?, ? )");
+ }
+ assertThat(sql.get(2)).contains("delete from mcontact_message where contact_id = ?");
+ assertThat(sql.get(3)).contains("delete from mcontact where id=?");
+ assertThat(sql.get(4)).contains("delete from mcontact_message where contact_id = ?");
+ assertThat(sql.get(5)).contains("delete from mcontact where id=?");
+
+ assertThat(sql.get(6)).contains("update maddress set street=?, city=?, version=? where id=? and version=?");
+ assertThat(sql.get(7)).contains("update mcustomer set name=?, notes=?, version=?, shipping_address_id=?, billing_address_id=? where id=? and version=?");
+
+ assertThat(sql.get(8)).contains("insert into mcontact");
+ assertThat(sql.get(9)).contains("update mcontact set email=?, first_name=?, last_name=?, version=?, customer_id=? where id=? and version=?");
+
+ assertThat(sql.get(13)).contains("update mcontact_message set title=?, subject=?, notes=?, version=?, contact_id=? where id=? and version=?");
+
+ }
+
+ private void modify(MCustomer cust) {
+
+ List contacts = cust.getContacts();
+ MContact mContact = new MContact("z@a.com", "z", "zed");
+ mContact.setId(UUID.randomUUID());
+
+ contacts.add(mContact);
+
+ contacts.get(0).setEmail("a@beta.com");
+ contacts.get(1).setEmail("b@beta.com");
+ contacts.remove(4);
+ contacts.remove(2);
+
+ cust.setName(cust.getName()+" modified");
+ cust.getBillingAddress().setStreet("Short");
+
+ MAddress mAddress = new MAddress("Broken", "Dreams");
+ mAddress.setId(UUID.randomUUID());
+ cust.setShippingAddress(null);
+ }
+
+ private MCustomer partial(String name, String fetchGraph) {
+ MCustomer cust1 = buildCustomer(name);
+ Ebean.save(cust1);
+
+ FetchPath fetchPath = PathProperties.parse(fetchGraph);
+ String asJson = Ebean.json().toJson(cust1, fetchPath);
+ return Ebean.json().toBean(MCustomer.class, asJson);
+ }
+
+ private MCustomer customer(String name) {
+
+ MCustomer cust1 = buildCustomer(name);
+ Ebean.save(cust1);
+
+ String asJson = Ebean.json().toJson(cust1);
+
+ return Ebean.json().toBean(MCustomer.class, asJson);
+ }
+
+ private MCustomer buildCustomer(String name) {
+
+ MCustomer c = new MCustomer(name);
+ c.setShippingAddress(new MAddress("Fleet st", "London"));
+ c.setBillingAddress(new MAddress("Cow corner", "Mid Wicket"));
+
+ c.getContacts().add(addContact("a@a.com", "a", "alligator"));
+ c.getContacts().add(addContact("b@a.com", "b", "beaver"));
+ c.getContacts().add(addContact("c@a.com", "c", "crow"));
+ c.getContacts().add(addContact("d@a.com", "d", "dog"));
+ c.getContacts().add(addContact("e@a.com", "e", "ent"));
+ c.getContacts().add(addContact("f@a.com", "f", "frog"));
+
+ return c;
+ }
+
+ private MContact addContact(String email, String first, String last) {
+ MContact mContact = new MContact(email, first, last);
+ int i = random.nextInt(2);
+ for (int j = 0; j < i; j++) {
+ mContact.getMessages().add(new MContactMessage(first+" "+i, last+" "+i));
+ }
+ return mContact;
+ }
+}
diff --git a/src/test/java/org/tests/model/basic/UUOne.java b/src/test/java/org/tests/model/basic/UUOne.java
index 8efe6da67..53c88446a 100644
--- a/src/test/java/org/tests/model/basic/UUOne.java
+++ b/src/test/java/org/tests/model/basic/UUOne.java
@@ -6,6 +6,7 @@ import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.OneToMany;
+import javax.persistence.Version;
import java.util.List;
import java.util.UUID;
@@ -18,10 +19,22 @@ public class UUOne {
String name;
+ String description;
@OneToMany(cascade = CascadeType.ALL, mappedBy = "master")
List comments;
+ @Version
+ long version;
+
+ public UUOne() {
+ }
+
+ public UUOne(String name, UUID id) {
+ this.name = name;
+ this.id = id;
+ }
+
public UUID getId() {
return id;
}
@@ -38,6 +51,14 @@ public class UUOne {
this.name = name;
}
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
public List getComments() {
return comments;
}
@@ -46,4 +67,11 @@ public class UUOne {
this.comments = comments;
}
+ public long getVersion() {
+ return version;
+ }
+
+ public void setVersion(long version) {
+ this.version = version;
+ }
}
diff --git a/src/test/java/org/tests/model/basic/UUTwo.java b/src/test/java/org/tests/model/basic/UUTwo.java
index cea9a9efd..f446fe397 100644
--- a/src/test/java/org/tests/model/basic/UUTwo.java
+++ b/src/test/java/org/tests/model/basic/UUTwo.java
@@ -4,6 +4,7 @@ import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.ManyToOne;
+import javax.persistence.Version;
import java.util.UUID;
@Entity
@@ -14,9 +15,22 @@ public class UUTwo {
String name;
+ String notes;
+
@ManyToOne(cascade = CascadeType.PERSIST)
UUOne master;
+ @Version
+ long version;
+
+ public UUTwo() {
+ }
+
+ public UUTwo(String name, UUID id) {
+ this.name = name;
+ this.id = id;
+ }
+
public UUID getId() {
return id;
}
@@ -33,6 +47,14 @@ public class UUTwo {
this.name = name;
}
+ public String getNotes() {
+ return notes;
+ }
+
+ public void setNotes(String notes) {
+ this.notes = notes;
+ }
+
public UUOne getMaster() {
return master;
}
@@ -41,4 +63,11 @@ public class UUTwo {
this.master = master;
}
+ public long getVersion() {
+ return version;
+ }
+
+ public void setVersion(long version) {
+ this.version = version;
+ }
}
diff --git a/src/test/java/org/tests/query/joins/TestDisjunctWhereOuterOnMany.java b/src/test/java/org/tests/query/joins/TestDisjunctWhereOuterOnMany.java
index 1cdd79c0d..cb7c6522d 100644
--- a/src/test/java/org/tests/query/joins/TestDisjunctWhereOuterOnMany.java
+++ b/src/test/java/org/tests/query/joins/TestDisjunctWhereOuterOnMany.java
@@ -3,10 +3,10 @@ package org.tests.query.joins;
import io.ebean.BaseTestCase;
import io.ebean.Ebean;
import io.ebean.Query;
-import org.tests.model.basic.UUOne;
-import org.tests.model.basic.UUTwo;
import org.junit.Assert;
import org.junit.Test;
+import org.tests.model.basic.UUOne;
+import org.tests.model.basic.UUTwo;
import java.util.List;
@@ -58,11 +58,11 @@ public class TestDisjunctWhereOuterOnMany extends BaseTestCase {
Assert.assertEquals(2, rowCount);
if (isPostgres()) {
- String expectedSql = "select distinct on (t0.id) t0.id, t0.name from uuone t0 left join uutwo u1 on u1.master_id = t0.id where (t0.name = ? or u1.name = ? ) ";
+ String expectedSql = "select distinct on (t0.id) t0.id, t0.name, t0.description, t0.version from uuone t0 left join uutwo u1 on u1.master_id = t0.id where (t0.name = ? or u1.name = ? ) ";
assertThat(sqlOf(query, 1)).contains(expectedSql);
} else {
- String expectedSql = "select distinct t0.id, t0.name from uuone t0 left join uutwo u1 on u1.master_id = t0.id where (t0.name = ? or u1.name = ? ) ";
+ String expectedSql = "select distinct t0.id, t0.name, t0.description, t0.version from uuone t0 left join uutwo u1 on u1.master_id = t0.id where (t0.name = ? or u1.name = ? ) ";
assertThat(sqlOf(query, 1)).contains(expectedSql);
}