You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cayenne.apache.org by nt...@apache.org on 2019/04/24 14:53:58 UTC
[cayenne] 01/06: CAY-2571 DataDomainFlushAction redesign initial
version and db operations API
This is an automated email from the ASF dual-hosted git repository.
ntimofeev pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/cayenne.git
commit f55efff11227671dc89ab2d5561858e9e2e75100
Author: Nikita Timofeev <st...@gmail.com>
AuthorDate: Mon Apr 22 18:01:56 2019 +0300
CAY-2571 DataDomainFlushAction redesign
initial version and db operations API
---
.../java/org/apache/cayenne/access/ObjectDiff.java | 22 +-
.../org/apache/cayenne/access/ObjectResolver.java | 4 +-
.../org/apache/cayenne/access/ObjectStore.java | 38 ++-
.../cayenne/access/ObjectStoreGraphDiff.java | 4 +-
.../org/apache/cayenne/access/flush/ArcTarget.java | 87 +++++++
.../access/flush/ArcValuesCreationHandler.java | 275 +++++++++++++++++++++
.../flush/DataDomainFlushAction.java} | 38 +--
.../flush/DataDomainFlushActionFactory.java} | 33 +--
.../flush/DataDomainIndirectDiffBuilder.java | 106 ++++++++
.../cayenne/access/flush/DbRowOpFactory.java | 138 +++++++++++
.../access/flush/DefaultDataDomainFlushAction.java | 210 ++++++++++++++++
.../DefaultDataDomainFlushActionFactory.java} | 43 ++--
.../apache/cayenne/access/flush/EffectiveOpId.java | 63 +++++
.../apache/cayenne/access/flush/FlushObserver.java | 152 ++++++++++++
.../access/flush/ObjectIdValueSupplier.java | 82 ++++++
.../flush/OptimisticLockQualifierBuilder.java | 85 +++++++
.../access/flush/PermanentObjectIdVisitor.java | 139 +++++++++++
.../cayenne/access/flush/PostprocessVisitor.java | 143 +++++++++++
.../cayenne/access/flush/QueryCreatorVisitor.java | 121 +++++++++
.../cayenne/access/flush/ReplacementIdVisitor.java | 106 ++++++++
.../cayenne/access/flush/RootRowOpProcessor.java | 82 ++++++
.../access/flush/ValuesCreationHandler.java | 77 ++++++
.../access/flush/operation/BaseDbRowOp.java | 77 ++++++
.../flush/operation/DbRowOp.java} | 40 ++-
.../access/flush/operation/DbRowOpMerger.java | 82 ++++++
.../flush/operation/DbRowOpSorter.java} | 34 +--
.../flush/operation/DbRowOpType.java} | 49 ++--
.../flush/operation/DbRowOpVisitor.java} | 39 +--
.../flush/operation/DbRowOpWithQualifier.java} | 32 +--
.../flush/operation/DbRowOpWithValues.java} | 32 +--
.../flush/operation/DefaultDbRowOpSorter.java | 150 +++++++++++
.../access/flush/operation/DeleteDbRowOp.java | 72 ++++++
.../flush/operation/DeleteInsertDbRowOp.java} | 48 ++--
.../access/flush/operation/InsertDbRowOp.java | 71 ++++++
.../cayenne/access/flush/operation/Qualifier.java | 147 +++++++++++
.../access/flush/operation/UpdateDbRowOp.java | 74 ++++++
.../cayenne/access/flush/operation/Values.java | 151 +++++++++++
.../cayenne/ashwood/AshwoodEntitySorter.java | 140 ++++++-----
.../main/java/org/apache/cayenne/map/DbEntity.java | 7 +-
.../java/org/apache/cayenne/map/EntitySorter.java | 21 ++
.../org/apache/cayenne/query/DeleteBatchQuery.java | 7 +-
41 files changed, 2980 insertions(+), 341 deletions(-)
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/ObjectDiff.java b/cayenne-server/src/main/java/org/apache/cayenne/access/ObjectDiff.java
index 97080eb..cc025f6 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/ObjectDiff.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/ObjectDiff.java
@@ -51,7 +51,7 @@ import java.util.Map;
* A dynamic GraphDiff that represents a delta between object simple properties
* at diff creation time and its current state.
*/
-class ObjectDiff extends NodeDiff {
+public class ObjectDiff extends NodeDiff {
private final String entityName;
@@ -144,11 +144,11 @@ class ObjectDiff extends NodeDiff {
return classDescriptor;
}
- Object getSnapshotValue(String propertyName) {
+ public Object getSnapshotValue(String propertyName) {
return snapshot != null ? snapshot.get(propertyName) : null;
}
- ObjectId getArcSnapshotValue(String propertyName) {
+ public ObjectId getArcSnapshotValue(String propertyName) {
Object value = arcSnapshot != null ? arcSnapshot.get(propertyName) : null;
if (value instanceof Fault) {
@@ -161,6 +161,20 @@ class ObjectDiff extends NodeDiff {
return (ObjectId) value;
}
+ /**
+ * @since 4.2
+ */
+ public ObjectId getCurrentArcSnapshotValue(String propertyName) {
+ Object value = currentArcSnapshot != null ? currentArcSnapshot.get(propertyName) : null;
+ if (value instanceof Fault) {
+ Persistent target = (Persistent) ((Fault) value).resolveFault(object, propertyName);
+
+ value = target != null ? target.getObjectId() : null;
+ currentArcSnapshot.put(propertyName, value);
+ }
+ return (ObjectId) value;
+ }
+
boolean containsArcSnapshot(String propertyName) {
return arcSnapshot != null && arcSnapshot.containsKey(propertyName);
}
@@ -462,7 +476,7 @@ class ObjectDiff extends NodeDiff {
@Override
public int hashCode() {
// assuming String and ObjectId provide a good hashCode
- return arcId.hashCode() + targetNodeId.hashCode() + 5;
+ return 31 * arcId.hashCode() + targetNodeId.hashCode();
}
@Override
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/ObjectResolver.java b/cayenne-server/src/main/java/org/apache/cayenne/access/ObjectResolver.java
index 088d341..14f8873 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/ObjectResolver.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/ObjectResolver.java
@@ -192,9 +192,9 @@ class ObjectResolver {
String path = entry.getKey();
int lastDot = path.lastIndexOf('.');
String prefix = lastDot == -1 ? path : path.substring(lastDot + 1);
- ObjectId objectId = createObjectId(row, dbEntity.getName(), dbEntity.getPrimaryKeys(), prefix + '.', false);
+ ObjectId objectId = createObjectId(row, "db:" + dbEntity.getName(), dbEntity.getPrimaryKeys(), prefix + '.', false);
if(objectId != null) {
- context.getObjectStore().markFlattenedPath(object.getObjectId(), path);
+ context.getObjectStore().markFlattenedPath(object.getObjectId(), path, objectId);
}
}
}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/ObjectStore.java b/cayenne-server/src/main/java/org/apache/cayenne/access/ObjectStore.java
index 8a821c4..4415aab 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/ObjectStore.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/ObjectStore.java
@@ -74,7 +74,7 @@ public class ObjectStore implements Serializable, SnapshotEventListener, GraphMa
* Presence of path in this map is used to separate insert from update case of flattened records.
* @since 4.1
*/
- protected Map<Object, Set<String>> trackedFlattenedPaths;
+ protected Map<Object, Map<String, ObjectId>> trackedFlattenedPaths;
// a sequential id used to tag GraphDiffs so that they can later be sorted in the
// original creation order
@@ -407,7 +407,7 @@ public class ObjectStore implements Serializable, SnapshotEventListener, GraphMa
*
* @since 1.2
*/
- void postprocessAfterCommit(GraphDiff parentChanges) {
+ public void postprocessAfterCommit(GraphDiff parentChanges) {
// scan through changed objects, set persistence state to committed
for (Object id : changes.keySet()) {
@@ -604,7 +604,7 @@ public class ObjectStore implements Serializable, SnapshotEventListener, GraphMa
}
if(trackedFlattenedPaths != null) {
- Set<String> paths = trackedFlattenedPaths.remove(nodeId);
+ Map<String, ObjectId> paths = trackedFlattenedPaths.remove(nodeId);
if(paths != null) {
trackedFlattenedPaths.put(newId, paths);
}
@@ -997,20 +997,44 @@ public class ObjectStore implements Serializable, SnapshotEventListener, GraphMa
return false;
}
return trackedFlattenedPaths
- .getOrDefault(objectId, Collections.emptySet()).contains(path);
+ .getOrDefault(objectId, Collections.emptyMap()).containsKey(path);
+ }
+
+ /**
+ * @since 4.2
+ */
+ public ObjectId getFlattenedId(ObjectId objectId, String path) {
+ if(trackedFlattenedPaths == null) {
+ return null;
+ }
+
+ return trackedFlattenedPaths
+ .getOrDefault(objectId, Collections.emptyMap()).get(path);
+ }
+
+ /**
+ * @since 4.2
+ */
+ public Collection<ObjectId> getFlattenedIds(ObjectId objectId) {
+ if(trackedFlattenedPaths == null) {
+ return Collections.emptyList();
+ }
+
+ return trackedFlattenedPaths
+ .getOrDefault(objectId, Collections.emptyMap()).values();
}
/**
* Mark that flattened path for object has data row in DB.
* @since 4.1
*/
- void markFlattenedPath(ObjectId objectId, String path) {
+ public void markFlattenedPath(ObjectId objectId, String path, ObjectId id) {
if(trackedFlattenedPaths == null) {
trackedFlattenedPaths = new ConcurrentHashMap<>();
}
trackedFlattenedPaths
- .computeIfAbsent(objectId, o -> Collections.newSetFromMap(new ConcurrentHashMap<>()))
- .add(path);
+ .computeIfAbsent(objectId, o -> new ConcurrentHashMap<>())
+ .put(path, id);
}
// an ObjectIdQuery optimized for retrieval of multiple snapshots - it can be reset
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/ObjectStoreGraphDiff.java b/cayenne-server/src/main/java/org/apache/cayenne/access/ObjectStoreGraphDiff.java
index 534a9cc..04e570f 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/ObjectStoreGraphDiff.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/ObjectStoreGraphDiff.java
@@ -44,7 +44,7 @@ import java.util.Map.Entry;
*
* @since 1.2
*/
-class ObjectStoreGraphDiff implements GraphDiff {
+public class ObjectStoreGraphDiff implements GraphDiff {
private ObjectStore objectStore;
private GraphDiff resolvedDiff;
@@ -55,7 +55,7 @@ class ObjectStoreGraphDiff implements GraphDiff {
preprocess(objectStore);
}
- Map<Object, ObjectDiff> getChangesByObjectId() {
+ public Map<Object, ObjectDiff> getChangesByObjectId() {
return objectStore.getChangesByObjectId();
}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/flush/ArcTarget.java b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/ArcTarget.java
new file mode 100644
index 0000000..8b6474a
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/ArcTarget.java
@@ -0,0 +1,87 @@
+/*****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.access.flush;
+
+import java.util.Objects;
+
+import org.apache.cayenne.ObjectId;
+import org.apache.cayenne.graph.ArcId;
+
+/**
+ * Value object describing exact arc between two objects.
+ * Implements {@link #equals(Object)} and {@link #hashCode()} methods.
+ *
+ * @since 4.2
+ */
+class ArcTarget {
+
+ private final ObjectId sourceId;
+ private final ObjectId targetId;
+ private final ArcId arcId;
+
+ ArcTarget(ObjectId sourceId, ObjectId targetId, ArcId arcId) {
+ this.sourceId = Objects.requireNonNull(sourceId);
+ this.targetId = Objects.requireNonNull(targetId);
+ this.arcId = Objects.requireNonNull(arcId);
+ }
+
+ ArcTarget getReversed() {
+ return new ArcTarget(targetId, sourceId, arcId.getReverseId());
+ }
+
+ ArcId getArcId() {
+ return arcId;
+ }
+
+ ObjectId getSourceId() {
+ return sourceId;
+ }
+
+ ObjectId getTargetId() {
+ return targetId;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ ArcTarget arcTarget = (ArcTarget) o;
+ if (!sourceId.equals(arcTarget.sourceId)) {
+ return false;
+ }
+ if (!targetId.equals(arcTarget.targetId)) {
+ return false;
+ }
+ return arcId.equals(arcTarget.arcId);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = sourceId.hashCode();
+ result = 31 * result + targetId.hashCode();
+ result = 31 * result + arcId.hashCode();
+ return result;
+ }
+}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/flush/ArcValuesCreationHandler.java b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/ArcValuesCreationHandler.java
new file mode 100644
index 0000000..181b0de
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/ArcValuesCreationHandler.java
@@ -0,0 +1,275 @@
+/*****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.access.flush;
+
+import java.util.Iterator;
+
+import org.apache.cayenne.ObjectId;
+import org.apache.cayenne.access.flush.operation.DbRowOp;
+import org.apache.cayenne.access.flush.operation.DbRowOpType;
+import org.apache.cayenne.access.flush.operation.DbRowOpVisitor;
+import org.apache.cayenne.access.flush.operation.DbRowOpWithValues;
+import org.apache.cayenne.access.flush.operation.DeleteDbRowOp;
+import org.apache.cayenne.access.flush.operation.InsertDbRowOp;
+import org.apache.cayenne.access.flush.operation.UpdateDbRowOp;
+import org.apache.cayenne.exp.parser.ASTDbPath;
+import org.apache.cayenne.graph.ArcId;
+import org.apache.cayenne.graph.GraphChangeHandler;
+import org.apache.cayenne.map.DbAttribute;
+import org.apache.cayenne.map.DbEntity;
+import org.apache.cayenne.map.DbJoin;
+import org.apache.cayenne.map.DbRelationship;
+import org.apache.cayenne.map.ObjEntity;
+import org.apache.cayenne.map.ObjRelationship;
+import org.apache.cayenne.util.CayenneMapEntry;
+
+/**
+ * Graph handler that collects information about arc changes into
+ * {@link org.apache.cayenne.access.flush.operation.Values} and/or {@link org.apache.cayenne.access.flush.operation.Qualifier}.
+ *
+ * @since 4.2
+ */
+class ArcValuesCreationHandler implements GraphChangeHandler {
+
+ final DbRowOpFactory factory;
+ final DbRowOpType defaultType;
+
+ ArcValuesCreationHandler(DbRowOpFactory factory, DbRowOpType defaultType) {
+ this.factory = factory;
+ this.defaultType = defaultType;
+ }
+
+ public void arcCreated(Object nodeId, Object targetNodeId, ArcId arcId) {
+ processArcChange(nodeId, targetNodeId, arcId, true);
+ }
+
+ public void arcDeleted(Object nodeId, Object targetNodeId, ArcId arcId) {
+ processArcChange(nodeId, targetNodeId, arcId, false);
+ }
+
+ private void processArcChange(Object nodeId, Object targetNodeId, ArcId arcId, boolean created) {
+ ObjectId actualTargetId = (ObjectId)targetNodeId;
+ ObjectId snapshotId = factory.getDiff().getCurrentArcSnapshotValue(arcId.getForwardArc());
+ if(snapshotId != null) {
+ actualTargetId = snapshotId;
+ }
+ ArcTarget arcTarget = new ArcTarget((ObjectId) nodeId, actualTargetId, arcId);
+ if(factory.getProcessedArcs().contains(arcTarget.getReversed())) {
+ return;
+ }
+
+ ObjEntity entity = factory.getDescriptor().getEntity();
+ ObjRelationship objRelationship = entity.getRelationship(arcTarget.getArcId().getForwardArc());
+ if(objRelationship == null) {
+ String arc = arcId.getForwardArc();
+ if(arc.startsWith(ASTDbPath.DB_PREFIX)) {
+ String relName = arc.substring(ASTDbPath.DB_PREFIX.length());
+ DbRelationship dbRelationship = entity.getDbEntity().getRelationship(relName);
+ processRelationship(dbRelationship, arcTarget.getSourceId(), arcTarget.getTargetId(), created);
+ }
+ return;
+ }
+
+ if(objRelationship.isFlattened()) {
+ processFlattenedPath(arcTarget.getSourceId(), arcTarget.getTargetId(), entity.getDbEntity(),
+ objRelationship.getDbRelationshipPath(), created);
+ } else {
+ DbRelationship dbRelationship = objRelationship.getDbRelationships().get(0);
+ processRelationship(dbRelationship, arcTarget.getSourceId(), arcTarget.getTargetId(), created);
+ }
+
+ factory.getProcessedArcs().add(arcTarget);
+ }
+
+ ObjectId processFlattenedPath(ObjectId id, ObjectId finalTargetId, DbEntity entity, String dbPath, boolean add) {
+ Iterator<CayenneMapEntry> dbPathIterator = entity.resolvePathComponents(dbPath);
+ StringBuilder path = new StringBuilder();
+
+ ObjectId srcId = id;
+ ObjectId targetId = null;
+
+ while(dbPathIterator.hasNext()) {
+ CayenneMapEntry entry = dbPathIterator.next();
+ if(path.length() > 0) {
+ path.append('.');
+ }
+
+ path.append(entry.getName());
+ if(entry instanceof DbRelationship) {
+ DbRelationship relationship = (DbRelationship)entry;
+ // intermediate db entity to be inserted
+ DbEntity target = relationship.getTargetEntity();
+ // if ID is present, just use it, otherwise create new
+ String flattenedPath = path.toString();
+
+ // if this is last segment and it's a relationship, use known target id from arc creation
+ if(!dbPathIterator.hasNext()) {
+ targetId = finalTargetId;
+ } else {
+ if(!relationship.isToMany()) {
+ targetId = factory.getStore().getFlattenedId(id, flattenedPath);
+ } else {
+ targetId = null;
+ }
+ }
+
+ if(targetId == null) {
+ // should insert, regardless of original operation (insert/update)
+ targetId = ObjectId.of(ASTDbPath.DB_PREFIX + target.getName());
+ if(!relationship.isToMany()) {
+ factory.getStore().markFlattenedPath(id, flattenedPath, targetId);
+ }
+
+ DbRowOpType type;
+ if(relationship.isToMany()) {
+ type = add ? DbRowOpType.INSERT : DbRowOpType.DELETE;
+ factory.getOrCreate(target, targetId, type);
+ } else {
+ type = add ? DbRowOpType.INSERT : DbRowOpType.UPDATE;
+ factory.<DbRowOpWithValues>getOrCreate(target, targetId, type)
+ .getValues()
+ .addFlattenedId(flattenedPath, targetId);
+ }
+ } else if(dbPathIterator.hasNext()) {
+ // should update existing DB row
+ factory.getOrCreate(target, targetId, add ? DbRowOpType.UPDATE : defaultType);
+ }
+ processRelationship(relationship, srcId, targetId, add);
+ srcId = targetId; // use target as next source..
+ }
+ }
+
+ return targetId;
+ }
+
+ protected void processRelationship(DbRelationship dbRelationship, ObjectId srcId, ObjectId targetId, boolean add) {
+ for(DbJoin join : dbRelationship.getJoins()) {
+ boolean srcPK = join.getSource().isPrimaryKey();
+ boolean targetPK = join.getTarget().isPrimaryKey();
+
+ Object valueToUse;
+ DbRowOp rowOp;
+ DbAttribute attribute;
+ ObjectId id;
+ boolean processDelete;
+
+ // We manage 3 cases here:
+ // 1. PK -> FK: just propagate value from PK and to FK
+ // 2. PK -> PK: check isToDep flag and set dependent one
+ // 3. NON-PK -> FK (not supported fully for now, see CAY-2488): also check isToDep flag,
+ // but get value from DbRow, not ObjID
+ if(srcPK != targetPK) {
+ // case 1
+ processDelete = true;
+ id = null;
+ if(srcPK) {
+ valueToUse = ObjectIdValueSupplier.getFor(srcId, join.getSourceName());
+ rowOp = factory.getOrCreate(dbRelationship.getTargetEntity(), targetId, DbRowOpType.UPDATE);
+ attribute = join.getTarget();
+ } else {
+ valueToUse = ObjectIdValueSupplier.getFor(targetId, join.getTargetName());
+ rowOp = factory.getOrCreate(dbRelationship.getSourceEntity(), srcId, defaultType);
+ attribute = join.getSource();
+ }
+ } else {
+ // case 2 and 3
+ processDelete = false;
+ if(dbRelationship.isToDependentPK()) {
+ valueToUse = ObjectIdValueSupplier.getFor(srcId, join.getSourceName());
+ rowOp = factory.getOrCreate(dbRelationship.getTargetEntity(), targetId, DbRowOpType.UPDATE);
+ attribute = join.getTarget();
+ id = targetId;
+ if(dbRelationship.isToMany()) {
+ // strange mapping toDepPK and toMany, but just skip it
+ rowOp = null;
+ }
+ } else {
+ valueToUse = ObjectIdValueSupplier.getFor(targetId, join.getTargetName());
+ rowOp = factory.getOrCreate(dbRelationship.getSourceEntity(), srcId, defaultType);
+ attribute = join.getSource();
+ id = srcId;
+ if(dbRelationship.getReverseRelationship().isToMany()) {
+ // strange mapping toDepPK and toMany, but just skip it
+ rowOp = null;
+ }
+ }
+ }
+
+ // propagated master -> child PK
+ if(id != null && attribute.isPrimaryKey()) {
+ id.getReplacementIdMap().put(attribute.getName(), valueToUse);
+ }
+ if(rowOp != null) {
+ rowOp.accept(new ValuePropagationVisitor(attribute, add, valueToUse, processDelete));
+ }
+ }
+ }
+
+ // not interested in following events in this handler
+ @Override
+ public void nodeIdChanged(Object nodeId, Object newId) {
+ }
+
+ @Override
+ public void nodeCreated(Object nodeId) {
+ }
+
+ @Override
+ public void nodeRemoved(Object nodeId) {
+ }
+
+ @Override
+ public void nodePropertyChanged(Object nodeId, String property, Object oldValue, Object newValue) {
+ }
+
+ private static class ValuePropagationVisitor implements DbRowOpVisitor<Void> {
+ private final DbAttribute attribute;
+ private final boolean add;
+ private final Object valueToUse;
+ private final boolean processDelete;
+
+ private ValuePropagationVisitor(DbAttribute attribute, boolean add, Object valueToUse, boolean processDelete) {
+ this.attribute = attribute;
+ this.add = add;
+ this.valueToUse = valueToUse;
+ this.processDelete = processDelete;
+ }
+
+ @Override
+ public Void visitInsert(InsertDbRowOp dbRow) {
+ dbRow.getValues().addValue(attribute, add ? valueToUse : null);
+ return null;
+ }
+
+ @Override
+ public Void visitUpdate(UpdateDbRowOp dbRow) {
+ dbRow.getValues().addValue(attribute, add ? valueToUse : null);
+ return null;
+ }
+
+ @Override
+ public Void visitDelete(DeleteDbRowOp dbRow) {
+ if(processDelete) {
+ dbRow.getQualifier().addAdditionalQualifier(attribute, valueToUse);
+ }
+ return null;
+ }
+ }
+}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/map/EntitySorter.java b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/DataDomainFlushAction.java
similarity index 53%
copy from cayenne-server/src/main/java/org/apache/cayenne/map/EntitySorter.java
copy to cayenne-server/src/main/java/org/apache/cayenne/access/flush/DataDomainFlushAction.java
index 5c7981e..c547d8f 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/map/EntitySorter.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/DataDomainFlushAction.java
@@ -17,37 +17,21 @@
* under the License.
****************************************************************/
-package org.apache.cayenne.map;
+package org.apache.cayenne.access.flush;
-import java.util.List;
+import org.apache.cayenne.access.DataContext;
+import org.apache.cayenne.graph.GraphDiff;
/**
- * Defines API for sorting of Cayenne entities based on their mutual dependencies.
- *
- * @since 1.1
+ * A stateful commit handler used by DataContext to perform commit operation.
+ * DataDomainFlushAction resolves primary key dependencies, referential integrity
+ * dependencies (including multi-reflexive entities), generates primary keys, creates
+ * batches for massive data modifications, assigns operations to data nodes.
+ *
+ * @since 4.2
*/
-public interface EntitySorter {
-
- /**
- * Sets EntityResolver for this sorter. All entities present in the resolver will be
- * used to determine sort ordering.
- *
- * @since 3.1
- */
- void setEntityResolver(EntityResolver resolver);
-
- /**
- * Sorts a list of DbEntities.
- */
- void sortDbEntities(List<DbEntity> dbEntities, boolean deleteOrder);
+public interface DataDomainFlushAction {
- /**
- * Sorts a list of ObjEntities.
- */
- void sortObjEntities(List<ObjEntity> objEntities, boolean deleteOrder);
+ GraphDiff flush(DataContext context, GraphDiff changes);
- /**
- * Sorts a list of objects belonging to the ObjEntity.
- */
- void sortObjectsForEntity(ObjEntity entity, List<?> objects, boolean deleteOrder);
}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/map/EntitySorter.java b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/DataDomainFlushActionFactory.java
similarity index 53%
copy from cayenne-server/src/main/java/org/apache/cayenne/map/EntitySorter.java
copy to cayenne-server/src/main/java/org/apache/cayenne/access/flush/DataDomainFlushActionFactory.java
index 5c7981e..437dab2 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/map/EntitySorter.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/DataDomainFlushActionFactory.java
@@ -17,37 +17,16 @@
* under the License.
****************************************************************/
-package org.apache.cayenne.map;
+package org.apache.cayenne.access.flush;
-import java.util.List;
+import org.apache.cayenne.access.DataDomain;
/**
- * Defines API for sorting of Cayenne entities based on their mutual dependencies.
- *
- * @since 1.1
+ * Factory that produces {@link DataDomainFlushAction}
+ * @since 4.2
*/
-public interface EntitySorter {
+public interface DataDomainFlushActionFactory {
- /**
- * Sets EntityResolver for this sorter. All entities present in the resolver will be
- * used to determine sort ordering.
- *
- * @since 3.1
- */
- void setEntityResolver(EntityResolver resolver);
+ DataDomainFlushAction createFlushAction(DataDomain dataDomain);
- /**
- * Sorts a list of DbEntities.
- */
- void sortDbEntities(List<DbEntity> dbEntities, boolean deleteOrder);
-
- /**
- * Sorts a list of ObjEntities.
- */
- void sortObjEntities(List<ObjEntity> objEntities, boolean deleteOrder);
-
- /**
- * Sorts a list of objects belonging to the ObjEntity.
- */
- void sortObjectsForEntity(ObjEntity entity, List<?> objects, boolean deleteOrder);
}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/flush/DataDomainIndirectDiffBuilder.java b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/DataDomainIndirectDiffBuilder.java
new file mode 100644
index 0000000..65e4190
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/DataDomainIndirectDiffBuilder.java
@@ -0,0 +1,106 @@
+/*****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.access.flush;
+
+import java.util.Collection;
+import java.util.HashSet;
+
+import org.apache.cayenne.CayenneRuntimeException;
+import org.apache.cayenne.ObjectId;
+import org.apache.cayenne.access.ObjectStoreGraphDiff;
+import org.apache.cayenne.graph.ArcId;
+import org.apache.cayenne.graph.GraphChangeHandler;
+import org.apache.cayenne.map.EntityResolver;
+import org.apache.cayenne.map.ObjEntity;
+import org.apache.cayenne.map.ObjRelationship;
+
+/**
+ * A processor of ObjectStore indirect changes, such as flattened relationships
+ * and to-many relationships.
+ */
+final class DataDomainIndirectDiffBuilder implements GraphChangeHandler {
+
+ private final EntityResolver resolver;
+ private Collection<ObjectId> indirectModifications;
+
+ DataDomainIndirectDiffBuilder(EntityResolver resolver) {
+ this.resolver = resolver;
+ }
+
+ void processChanges(ObjectStoreGraphDiff allChanges) {
+ // extract flattened and indirect changes and remove duplicate changes...
+ allChanges.getChangesByObjectId()
+ .forEach((obj, diff) -> diff.apply(this));
+ }
+
+ Collection<ObjectId> getIndirectModifications() {
+ return indirectModifications;
+ }
+
+ @Override
+ public void arcCreated(Object nodeId, Object targetNodeId, ArcId arcId) {
+ processArcChange((ObjectId) nodeId, arcId);
+ }
+
+ @Override
+ public void arcDeleted(Object nodeId, Object targetNodeId, ArcId arcId) {
+ processArcChange((ObjectId) nodeId, arcId);
+ }
+
+ private void processArcChange(ObjectId nodeId, ArcId arcId) {
+ ObjEntity entity = resolver.getObjEntity(nodeId.getEntityName());
+ ObjRelationship relationship = entity.getRelationship(arcId.getForwardArc());
+
+ if (relationship != null && relationship.isSourceIndependentFromTargetChange()) {
+ // do not record temporary id mods...
+ if (!nodeId.isTemporary()) {
+ if(indirectModifications == null) {
+ indirectModifications = new HashSet<>();
+ }
+ indirectModifications.add(nodeId);
+ }
+
+ if (relationship.isFlattened() && relationship.isReadOnly()) {
+ throw new CayenneRuntimeException("Cannot change the read-only flattened relationship %s in ObjEntity '%s'."
+ , relationship.getName(), relationship.getSourceEntity().getName());
+ }
+ }
+ }
+
+ @Override
+ public void nodeIdChanged(Object nodeId, Object newId) {
+ // noop
+ }
+
+ @Override
+ public void nodeCreated(Object nodeId) {
+ // noop
+ }
+
+ @Override
+ public void nodeRemoved(Object nodeId) {
+ // noop
+ }
+
+ @Override
+ public void nodePropertyChanged(Object nodeId, String property, Object oldValue, Object newValue) {
+ // noop
+ }
+}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/flush/DbRowOpFactory.java b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/DbRowOpFactory.java
new file mode 100644
index 0000000..47f4de1
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/DbRowOpFactory.java
@@ -0,0 +1,138 @@
+/*****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.access.flush;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+import org.apache.cayenne.CayenneRuntimeException;
+import org.apache.cayenne.ObjectId;
+import org.apache.cayenne.Persistent;
+import org.apache.cayenne.access.ObjectDiff;
+import org.apache.cayenne.access.ObjectStore;
+import org.apache.cayenne.access.flush.operation.DbRowOp;
+import org.apache.cayenne.access.flush.operation.DbRowOpType;
+import org.apache.cayenne.access.flush.operation.DeleteDbRowOp;
+import org.apache.cayenne.access.flush.operation.InsertDbRowOp;
+import org.apache.cayenne.access.flush.operation.UpdateDbRowOp;
+import org.apache.cayenne.exp.parser.ASTDbPath;
+import org.apache.cayenne.map.DbEntity;
+import org.apache.cayenne.map.EntityResolver;
+import org.apache.cayenne.map.ObjEntity;
+import org.apache.cayenne.reflect.ClassDescriptor;
+
+/**
+ * Factory that produces a collection of {@link DbRowOp} from given {@link ObjectDiff}.
+ *
+ * @since 4.2
+ */
+class DbRowOpFactory {
+
+ private final EntityResolver resolver;
+ private final ObjectStore store;
+ private final Set<ArcTarget> processedArcs;
+ private final Map<ObjectId, DbRowOp> dbRows;
+ private final RootRowOpProcessor rootRowOpProcessor;
+
+ private ClassDescriptor descriptor;
+ private Persistent object;
+ private ObjectDiff diff;
+
+ DbRowOpFactory(EntityResolver resolver, ObjectStore store, Set<ArcTarget> processedArcs) {
+ this.resolver = resolver;
+ this.store = store;
+ this.dbRows = new HashMap<>(4);
+ this.processedArcs = processedArcs;
+ this.rootRowOpProcessor = new RootRowOpProcessor(this);
+ }
+
+ private void udpateDiff(ObjectDiff diff) {
+ ObjectId id = (ObjectId)diff.getNodeId();
+ this.diff = diff;
+ this.descriptor = resolver.getClassDescriptor(id.getEntityName());
+ this.object = (Persistent) store.getNode(id);
+ this.dbRows.clear();
+ }
+
+ Collection<? extends DbRowOp> createRows(ObjectDiff diff) {
+ udpateDiff(diff);
+ DbEntity rootEntity = descriptor.getEntity().getDbEntity();
+ DbRowOp row = getOrCreate(rootEntity, object.getObjectId(), DbRowOpType.forObject(object));
+ rootRowOpProcessor.setDiff(diff);
+ row.accept(rootRowOpProcessor);
+ return dbRows.values();
+ }
+
+ @SuppressWarnings("unchecked")
+ <E extends DbRowOp> E get(ObjectId id) {
+ return Objects.requireNonNull((E) dbRows.get(id));
+ }
+
+ @SuppressWarnings("unchecked")
+ <E extends DbRowOp> E getOrCreate(DbEntity entity, ObjectId id, DbRowOpType type) {
+ return (E) dbRows.computeIfAbsent(id, nextId -> createRow(entity, id, type));
+ }
+
+ private DbRowOp createRow(DbEntity entity, ObjectId id, DbRowOpType type) {
+ switch (type) {
+ case INSERT:
+ return new InsertDbRowOp(object, entity, id);
+ case UPDATE:
+ return new UpdateDbRowOp(object, entity, id);
+ case DELETE:
+ return new DeleteDbRowOp(object, entity, id);
+ }
+ throw new CayenneRuntimeException("Unknown DbRowType '%s'", type);
+ }
+
+ ClassDescriptor getDescriptor() {
+ return descriptor;
+ }
+
+ Persistent getObject() {
+ return object;
+ }
+
+ ObjectStore getStore() {
+ return store;
+ }
+
+ ObjectDiff getDiff() {
+ return diff;
+ }
+
+ DbEntity getDbEntity(ObjectId id) {
+ String entityName = id.getEntityName();
+ if(entityName.startsWith(ASTDbPath.DB_PREFIX)) {
+ entityName = entityName.substring(ASTDbPath.DB_PREFIX.length());
+ return resolver.getDbEntity(entityName);
+ } else {
+ ObjEntity objEntity = resolver.getObjEntity(entityName);
+ return objEntity.getDbEntity();
+ }
+ }
+
+ Set<ArcTarget> getProcessedArcs() {
+ return processedArcs;
+ }
+}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/flush/DefaultDataDomainFlushAction.java b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/DefaultDataDomainFlushAction.java
new file mode 100644
index 0000000..25511db
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/DefaultDataDomainFlushAction.java
@@ -0,0 +1,210 @@
+/*****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.access.flush;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.apache.cayenne.CayenneRuntimeException;
+import org.apache.cayenne.ObjectId;
+import org.apache.cayenne.access.DataContext;
+import org.apache.cayenne.access.DataDomain;
+import org.apache.cayenne.access.ObjectDiff;
+import org.apache.cayenne.access.ObjectStore;
+import org.apache.cayenne.access.ObjectStoreGraphDiff;
+import org.apache.cayenne.access.OperationObserver;
+import org.apache.cayenne.access.flush.operation.DbRowOpMerger;
+import org.apache.cayenne.access.flush.operation.DbRowOpSorter;
+import org.apache.cayenne.access.flush.operation.DbRowOp;
+import org.apache.cayenne.access.flush.operation.DbRowOpVisitor;
+import org.apache.cayenne.graph.CompoundDiff;
+import org.apache.cayenne.graph.GraphDiff;
+import org.apache.cayenne.log.JdbcEventLogger;
+import org.apache.cayenne.map.EntityResolver;
+import org.apache.cayenne.query.Query;
+
+/**
+ * Default implementation of {@link DataDomainFlushAction}.
+ *
+ * @since 4.2
+ */
+public class DefaultDataDomainFlushAction implements DataDomainFlushAction {
+
+ protected final DataDomain dataDomain;
+ protected final DbRowOpSorter dbRowOpSorter;
+ protected final JdbcEventLogger jdbcEventLogger;
+ protected final OperationObserver observer;
+
+ protected DefaultDataDomainFlushAction(DataDomain dataDomain, DbRowOpSorter dbRowOpSorter, JdbcEventLogger jdbcEventLogger) {
+ this.dataDomain = dataDomain;
+ this.dbRowOpSorter = dbRowOpSorter;
+ this.jdbcEventLogger = jdbcEventLogger;
+ this.observer = new FlushObserver(jdbcEventLogger);
+ }
+
+ @Override
+ public GraphDiff flush(DataContext context, GraphDiff changes) {
+ CompoundDiff afterCommitDiff = new CompoundDiff();
+ if (changes == null) {
+ return afterCommitDiff;
+ }
+ if(!(changes instanceof ObjectStoreGraphDiff)) {
+ throw new CayenneRuntimeException("Instance of ObjectStoreGraphDiff expected, got %s", changes.getClass());
+ }
+
+ ObjectStore objectStore = context.getObjectStore();
+ ObjectStoreGraphDiff objectStoreGraphDiff = (ObjectStoreGraphDiff) changes;
+
+ List<DbRowOp> dbRowOps = createDbRowOps(objectStore, objectStoreGraphDiff);
+ updateObjectIds(dbRowOps);
+ List<DbRowOp> deduplicatedOps = mergeSameObjectIds(dbRowOps);
+ List<DbRowOp> sortedOps = sort(deduplicatedOps);
+ List<? extends Query> queries = createQueries(sortedOps);
+ executeQueries(queries);
+ createReplacementIds(objectStore, afterCommitDiff, sortedOps);
+ postprocess(context, objectStoreGraphDiff, afterCommitDiff, sortedOps);
+
+ return afterCommitDiff;
+ }
+
+ /**
+ * Create ops based on incoming graph changes
+ * @param objectStore originating object store
+ * @param changes object graph diff
+ * @return collection of {@link DbRowOp}
+ */
+ protected List<DbRowOp> createDbRowOps(ObjectStore objectStore, ObjectStoreGraphDiff changes) {
+ EntityResolver resolver = dataDomain.getEntityResolver();
+
+ Map<Object, ObjectDiff> changesByObjectId = changes.getChangesByObjectId();
+ List<DbRowOp> ops = new ArrayList<>(changesByObjectId.size());
+ Set<ArcTarget> processedArcs = new HashSet<>();
+
+ DbRowOpFactory factory = new DbRowOpFactory(resolver, objectStore, processedArcs);
+ changesByObjectId.forEach((obj, diff) -> ops.addAll(factory.createRows(diff)));
+
+ return ops;
+ }
+
+ /**
+ * Fill in replacement IDs' data for given operations
+ * @param dbRowOps collection of {@link DbRowOp}
+ */
+ protected void updateObjectIds(Collection<DbRowOp> dbRowOps) {
+ DbRowOpVisitor<Void> permIdVisitor = new PermanentObjectIdVisitor(dataDomain);
+ dbRowOps.forEach(row -> row.accept(permIdVisitor));
+ }
+
+ /**
+ * @param dbRowOps collection of {@link DbRowOp}
+ * @return collection of ops with merged duplicates
+ */
+ protected List<DbRowOp> mergeSameObjectIds(List<DbRowOp> dbRowOps) {
+ Map<ObjectId, DbRowOp> index = new HashMap<>(dbRowOps.size());
+ // new EffectiveOpId()
+ dbRowOps.forEach(row -> index.merge(row.getChangeId(), row, DbRowOpMerger.INSTANCE));
+ // reuse list
+ dbRowOps.clear();
+ dbRowOps.addAll(index.values());
+ return dbRowOps;
+ }
+
+ /**
+ * Sort all operations
+ * @param dbRowOps collection of {@link DbRowOp}
+ * @return sorted collection of operations
+ * @see DbRowOpSorter interface and it's default implementation
+ */
+ protected List<DbRowOp> sort(List<DbRowOp> dbRowOps) {
+ return dbRowOpSorter.sort(dbRowOps);
+ }
+
+ /**
+ *
+ * @param dbRowOps collection of {@link DbRowOp}
+ * @return collection of {@link Query} to perform
+ */
+ protected List<? extends Query> createQueries(List<DbRowOp> dbRowOps) {
+ QueryCreatorVisitor queryCreator = new QueryCreatorVisitor(dbRowOps.size());
+ dbRowOps.forEach(row -> row.accept(queryCreator));
+ return queryCreator.getQueryList();
+ }
+
+ /**
+ * Execute queries, grouping them by nodes
+ * @param queries to execute
+ */
+ protected void executeQueries(List<? extends Query> queries) {
+ EntityResolver entityResolver = dataDomain.getEntityResolver();
+ queries.stream()
+ .collect(Collectors.groupingBy(query
+ -> dataDomain.lookupDataNode(query.getMetaData(entityResolver).getDataMap())))
+ .forEach((node, nodeQueries)
+ -> node.performQueries(nodeQueries, observer));
+ }
+
+ /**
+ * Set final {@link ObjectId} for persistent objects
+ *
+ * @param store object store
+ * @param afterCommitDiff result graph diff
+ * @param dbRowOps collection of {@link DbRowOp}
+ */
+ protected void createReplacementIds(ObjectStore store, CompoundDiff afterCommitDiff, List<DbRowOp> dbRowOps) {
+ ReplacementIdVisitor visitor = new ReplacementIdVisitor(store, dataDomain.getEntityResolver(), afterCommitDiff);
+ dbRowOps.forEach(row -> row.accept(visitor));
+ }
+
+ /**
+ * Notify {@link ObjectStore} and it's data row cache about actual changes we performed.
+ *
+ * @param context originating context
+ * @param changes incoming diff
+ * @param afterCommitDiff resulting diff
+ * @param dbRowOps collection of {@link DbRowOp}
+ */
+ protected void postprocess(DataContext context, ObjectStoreGraphDiff changes, CompoundDiff afterCommitDiff, List<DbRowOp> dbRowOps) {
+ ObjectStore objectStore = context.getObjectStore();
+
+ PostprocessVisitor postprocessor = new PostprocessVisitor(context);
+ dbRowOps.forEach(row -> row.accept(postprocessor));
+
+ DataDomainIndirectDiffBuilder indirectDiffBuilder = new DataDomainIndirectDiffBuilder(context.getEntityResolver());
+ indirectDiffBuilder.processChanges(changes);
+
+ objectStore.getDataRowCache()
+ .processSnapshotChanges(
+ objectStore,
+ postprocessor.getUpdatedSnapshots(),
+ postprocessor.getDeletedIds(),
+ Collections.emptyList(),
+ indirectDiffBuilder.getIndirectModifications()
+ );
+ objectStore.postprocessAfterCommit(afterCommitDiff);
+ }
+
+}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/map/EntitySorter.java b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/DefaultDataDomainFlushActionFactory.java
similarity index 53%
copy from cayenne-server/src/main/java/org/apache/cayenne/map/EntitySorter.java
copy to cayenne-server/src/main/java/org/apache/cayenne/access/flush/DefaultDataDomainFlushActionFactory.java
index 5c7981e..47d08a3 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/map/EntitySorter.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/DefaultDataDomainFlushActionFactory.java
@@ -17,37 +17,28 @@
* under the License.
****************************************************************/
-package org.apache.cayenne.map;
+package org.apache.cayenne.access.flush;
-import java.util.List;
+import org.apache.cayenne.access.DataDomain;
+import org.apache.cayenne.access.flush.operation.DbRowOpSorter;
+import org.apache.cayenne.di.Inject;
+import org.apache.cayenne.log.JdbcEventLogger;
/**
- * Defines API for sorting of Cayenne entities based on their mutual dependencies.
- *
- * @since 1.1
+ * Factory that produces {@link DefaultDataDomainFlushAction}.
+ *
+ * @since 4.2
*/
-public interface EntitySorter {
-
- /**
- * Sets EntityResolver for this sorter. All entities present in the resolver will be
- * used to determine sort ordering.
- *
- * @since 3.1
- */
- void setEntityResolver(EntityResolver resolver);
+public class DefaultDataDomainFlushActionFactory implements DataDomainFlushActionFactory {
- /**
- * Sorts a list of DbEntities.
- */
- void sortDbEntities(List<DbEntity> dbEntities, boolean deleteOrder);
+ @Inject
+ private DbRowOpSorter operationSorter;
- /**
- * Sorts a list of ObjEntities.
- */
- void sortObjEntities(List<ObjEntity> objEntities, boolean deleteOrder);
+ @Inject
+ private JdbcEventLogger jdbcEventLogger;
- /**
- * Sorts a list of objects belonging to the ObjEntity.
- */
- void sortObjectsForEntity(ObjEntity entity, List<?> objects, boolean deleteOrder);
+ @Override
+ public DataDomainFlushAction createFlushAction(DataDomain dataDomain) {
+ return new DefaultDataDomainFlushAction(dataDomain, operationSorter, jdbcEventLogger);
+ }
}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/flush/EffectiveOpId.java b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/EffectiveOpId.java
new file mode 100644
index 0000000..c906c08
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/EffectiveOpId.java
@@ -0,0 +1,63 @@
+/*****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.access.flush;
+
+import java.util.Map;
+
+import org.apache.cayenne.ObjectId;
+
+/**
+ * Helper value-object class that used to compare operations by "effective" id (i.e. by id snapshot,
+ * that will include replacement id if any).
+ * This class is not used directly by Cayenne, it's designed to ease custom implementations.
+ */
+@SuppressWarnings("unused")
+public class EffectiveOpId {
+ private final String entityName;
+ private final Map<String, Object> snapshot;
+
+ public EffectiveOpId(ObjectId id) {
+ this.entityName = id.getEntityName();
+ this.snapshot = id.getIdSnapshot();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ if(snapshot.isEmpty()) {
+ return false;
+ }
+
+ EffectiveOpId that = (EffectiveOpId) o;
+
+ if (!entityName.equals(that.entityName)) return false;
+ return snapshot.equals(that.snapshot);
+
+ }
+
+ @Override
+ public int hashCode() {
+ int result = entityName.hashCode();
+ result = 31 * result + snapshot.hashCode();
+ return result;
+ }
+}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/flush/FlushObserver.java b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/FlushObserver.java
new file mode 100644
index 0000000..ad79935
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/FlushObserver.java
@@ -0,0 +1,152 @@
+/*****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.access.flush;
+
+import java.util.List;
+
+import org.apache.cayenne.CayenneRuntimeException;
+import org.apache.cayenne.DataRow;
+import org.apache.cayenne.ObjectId;
+import org.apache.cayenne.ResultIterator;
+import org.apache.cayenne.access.OperationObserver;
+import org.apache.cayenne.log.JdbcEventLogger;
+import org.apache.cayenne.map.DbAttribute;
+import org.apache.cayenne.query.BatchQuery;
+import org.apache.cayenne.query.InsertBatchQuery;
+import org.apache.cayenne.query.Query;
+import org.apache.cayenne.util.Util;
+
+/**
+ * @since 4.2
+ */
+class FlushObserver implements OperationObserver {
+
+ private JdbcEventLogger logger;
+
+ FlushObserver(JdbcEventLogger logger) {
+ this.logger = logger;
+ }
+
+ @Override
+ public void nextQueryException(Query query, Exception ex) {
+ throw new CayenneRuntimeException("Raising from query exception.", Util.unwindException(ex));
+ }
+
+ @Override
+ public void nextGlobalException(Exception ex) {
+ throw new CayenneRuntimeException("Raising from underlyingQueryEngine exception.", Util.unwindException(ex));
+ }
+
+ /**
+ * Processes generated keys.
+ */
+ @Override
+ @SuppressWarnings("unchecked")
+ public void nextGeneratedRows(Query query, ResultIterator<?> keysIterator, ObjectId idToUpdate) {
+
+ // read and close the iterator before doing anything else
+ List<DataRow> keys;
+ try {
+ keys = (List<DataRow>) keysIterator.allRows();
+ } finally {
+ keysIterator.close();
+ }
+
+ if (!(query instanceof InsertBatchQuery)) {
+ throw new CayenneRuntimeException("Generated keys only supported for InsertBatchQuery, instead got %s", query);
+ }
+
+ if (idToUpdate == null || !idToUpdate.isTemporary()) {
+ // why would this happen?
+ return;
+ }
+
+ if (keys.size() != 1) {
+ throw new CayenneRuntimeException("One and only one PK row is expected, instead got %d", keys.size());
+ }
+
+ DataRow key = keys.get(0);
+
+ // empty key?
+ if (key.size() == 0) {
+ throw new CayenneRuntimeException("Empty key generated.");
+ }
+
+ // determine DbAttribute name...
+
+ // As of now (01/2005) all tested drivers don't provide decent
+ // descriptors of identity result sets, so a data row will contain garbage labels.
+ // Also most DBs only support one autogenerated key per table...
+ // So here we will have to infer the key name and currently will only support a single column...
+ if (key.size() > 1) {
+ throw new CayenneRuntimeException("Only a single column autogenerated PK is supported. "
+ + "Generated key: %s", key);
+ }
+
+ BatchQuery batch = (BatchQuery) query;
+ for (DbAttribute attribute : batch.getDbEntity().getGeneratedAttributes()) {
+
+ // batch can have generated attributes that are not PKs, e.g.
+ // columns with DB DEFAULT values. Ignore those.
+ if (attribute.isPrimaryKey()) {
+ Object value = key.values().iterator().next();
+
+ // Log the generated PK
+ logger.logGeneratedKey(attribute, value);
+
+ // I guess we should override any existing value,
+ // as generated key is the latest thing that exists in the DB.
+ idToUpdate.getReplacementIdMap().put(attribute.getName(), value);
+ break;
+ }
+ }
+ }
+
+ public void setJdbcEventLogger(JdbcEventLogger logger) {
+ this.logger = logger;
+ }
+
+ public JdbcEventLogger getJdbcEventLogger() {
+ return this.logger;
+ }
+
+ @Override
+ public void nextBatchCount(Query query, int[] resultCount) {
+ }
+
+ @Override
+ public void nextCount(Query query, int resultCount) {
+ }
+
+ @Override
+ public void nextRows(Query query, List<?> dataRows) {
+ }
+
+ @Override
+ @SuppressWarnings("rawtypes")
+ public void nextRows(Query q, ResultIterator it) {
+ throw new UnsupportedOperationException("'nextDataRows(Query,ResultIterator)' is unsupported (and unexpected) on commit.");
+ }
+
+ @Override
+ public boolean isIteratedResult() {
+ return false;
+ }
+}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/flush/ObjectIdValueSupplier.java b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/ObjectIdValueSupplier.java
new file mode 100644
index 0000000..6d4fb11
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/ObjectIdValueSupplier.java
@@ -0,0 +1,82 @@
+/*****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.access.flush;
+
+import java.util.Objects;
+import java.util.function.Supplier;
+
+import org.apache.cayenne.ObjectId;
+
+/**
+ * Deferred value extracted from ObjectId
+ *
+ * @since 4.2
+ */
+class ObjectIdValueSupplier implements Supplier<Object> {
+
+ private final ObjectId id;
+ private final String attribute;
+
+ static Object getFor(ObjectId id, String attribute) {
+ // resolve eagerly, if value is already present
+ // TODO: what if this is a meaningful part of an ID and it will change?
+ Object value = id.getIdSnapshot().get(attribute);
+ if(value != null) {
+ return value;
+ }
+ return new ObjectIdValueSupplier(id, attribute);
+ }
+
+ private ObjectIdValueSupplier(ObjectId id, String attribute) {
+ this.id = Objects.requireNonNull(id);
+ this.attribute = Objects.requireNonNull(attribute);
+ }
+
+ @Override
+ public Object get() {
+ return id.getIdSnapshot().get(attribute);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ ObjectIdValueSupplier that = (ObjectIdValueSupplier) o;
+ if (!id.equals(that.id)) {
+ return false;
+ }
+ return attribute.equals(that.attribute);
+ }
+
+ @Override
+ public int hashCode() {
+ return 31 * id.hashCode() + attribute.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return "{id=" + id + ", attr=" + attribute + '}';
+ }
+}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/flush/OptimisticLockQualifierBuilder.java b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/OptimisticLockQualifierBuilder.java
new file mode 100644
index 0000000..1aa7b19
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/OptimisticLockQualifierBuilder.java
@@ -0,0 +1,85 @@
+/*****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.access.flush;
+
+import org.apache.cayenne.ObjectId;
+import org.apache.cayenne.access.ObjectDiff;
+import org.apache.cayenne.access.flush.operation.DbRowOpWithQualifier;
+import org.apache.cayenne.map.DbAttribute;
+import org.apache.cayenne.map.DbJoin;
+import org.apache.cayenne.map.DbRelationship;
+import org.apache.cayenne.map.ObjAttribute;
+import org.apache.cayenne.map.ObjRelationship;
+import org.apache.cayenne.reflect.AttributeProperty;
+import org.apache.cayenne.reflect.PropertyVisitor;
+import org.apache.cayenne.reflect.ToManyProperty;
+import org.apache.cayenne.reflect.ToOneProperty;
+
+/**
+ * {@link PropertyVisitor} that builds optimistic lock qualifier for given db change.
+ *
+ * @since 4.2
+ */
+class OptimisticLockQualifierBuilder implements PropertyVisitor {
+ private final DbRowOpWithQualifier dbRow;
+ private final ObjectDiff diff;
+
+ OptimisticLockQualifierBuilder(DbRowOpWithQualifier dbRow, ObjectDiff diff) {
+ this.dbRow = dbRow;
+ this.diff = diff;
+ }
+
+ @Override
+ public boolean visitAttribute(AttributeProperty property) {
+ ObjAttribute attribute = property.getAttribute();
+ DbAttribute dbAttribute = attribute.getDbAttribute();
+ if (attribute.isUsedForLocking() && dbAttribute.getEntity() == dbRow.getEntity()) {
+ dbRow.getQualifier()
+ .addAdditionalQualifier(dbAttribute, diff.getSnapshotValue(property.getName()), true);
+
+ } else {
+ // unimplemented case, see CAY-2560 for details.
+ // we can't grab sub entity row here as no good accessor for this implemented.
+ }
+ return true;
+ }
+
+ @Override
+ public boolean visitToOne(ToOneProperty property) {
+ ObjRelationship relationship = property.getRelationship();
+ if(relationship.isUsedForLocking()) {
+ ObjectId value = diff.getArcSnapshotValue(property.getName());
+ DbRelationship dbRelationship = relationship.getDbRelationships().get(0);
+ for(DbJoin join : dbRelationship.getJoins()) {
+ DbAttribute source = join.getSource();
+ if(!source.isPrimaryKey()) {
+ dbRow.getQualifier()
+ .addAdditionalQualifier(source, ObjectIdValueSupplier.getFor(value, join.getTargetName()), true);
+ }
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public boolean visitToMany(ToManyProperty property) {
+ return true;
+ }
+}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/flush/PermanentObjectIdVisitor.java b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/PermanentObjectIdVisitor.java
new file mode 100644
index 0000000..a359bee
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/PermanentObjectIdVisitor.java
@@ -0,0 +1,139 @@
+/*****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.access.flush;
+
+import java.util.Map;
+
+import org.apache.cayenne.CayenneRuntimeException;
+import org.apache.cayenne.ObjectId;
+import org.apache.cayenne.access.DataDomain;
+import org.apache.cayenne.access.DataNode;
+import org.apache.cayenne.access.flush.operation.DbRowOpVisitor;
+import org.apache.cayenne.access.flush.operation.InsertDbRowOp;
+import org.apache.cayenne.dba.PkGenerator;
+import org.apache.cayenne.exp.parser.ASTDbPath;
+import org.apache.cayenne.map.DbAttribute;
+import org.apache.cayenne.map.DbEntity;
+import org.apache.cayenne.map.EntityResolver;
+import org.apache.cayenne.map.ObjAttribute;
+import org.apache.cayenne.map.ObjEntity;
+import org.apache.cayenne.reflect.ClassDescriptor;
+
+/**
+ * Visitor that fills replacement map of {@link ObjectId}s of inserted objects.
+ *
+ * @since 4.2
+ */
+class PermanentObjectIdVisitor implements DbRowOpVisitor<Void> {
+
+ private final DataDomain dataDomain;
+ private final EntityResolver resolver;
+
+ private ClassDescriptor lastDescriptor;
+ private ObjEntity lastObjEntity;
+ private DbEntity lastDbEntity;
+ private DataNode lastNode;
+ private String lastEntityName;
+
+ PermanentObjectIdVisitor(DataDomain dataDomain) {
+ this.dataDomain = dataDomain;
+ this.resolver = dataDomain.getEntityResolver();
+ }
+
+ @Override
+ public Void visitInsert(InsertDbRowOp dbRow) {
+ ObjectId id = dbRow.getChangeId();
+ if (id == null || !id.isTemporary()) {
+ return null;
+ }
+
+ if((lastObjEntity == null && lastDbEntity == null) || !id.getEntityName().equals(lastEntityName)) {
+ lastEntityName = id.getEntityName();
+ if(lastEntityName.startsWith(ASTDbPath.DB_PREFIX)) {
+ lastDbEntity = resolver.getDbEntity(lastEntityName.substring(ASTDbPath.DB_PREFIX.length()));
+ lastObjEntity = null;
+ lastDescriptor = null;
+ lastNode = dataDomain.lookupDataNode(lastDbEntity.getDataMap());
+ } else {
+ lastObjEntity = resolver.getObjEntity(id.getEntityName());
+ lastDbEntity = lastObjEntity.getDbEntity();
+ lastDescriptor = resolver.getClassDescriptor(lastObjEntity.getName());
+ lastNode = dataDomain.lookupDataNode(lastObjEntity.getDataMap());
+ }
+ }
+
+ createPermanentId(dbRow);
+ return null;
+ }
+
+ private void createPermanentId(InsertDbRowOp dbRow) {
+ ObjectId id = dbRow.getChangeId();
+ boolean supportsGeneratedKeys = lastNode.getAdapter().supportsGeneratedKeys();
+ PkGenerator pkGenerator = lastNode.getAdapter().getPkGenerator();
+
+ // modify replacement id directly...
+ Map<String, Object> idMap = id.getReplacementIdMap();
+
+ boolean autoPkDone = false;
+
+ for (DbAttribute dbAttr : lastDbEntity.getPrimaryKeys()) {
+ String dbAttrName = dbAttr.getName();
+
+ if (idMap.containsKey(dbAttrName)) {
+ continue;
+ }
+
+ // handle meaningful PK
+ if(lastObjEntity != null) {
+ ObjAttribute objAttr = lastObjEntity.getAttributeForDbAttribute(dbAttr);
+ if (objAttr != null) {
+ Object value = lastDescriptor.getProperty(objAttr.getName()).readPropertyDirectly(dbRow.getObject());
+ if (value != null) {
+ // primitive 0 has to be treated as NULL, or otherwise we can't generate PK for POJO's
+ Class<?> javaClass = objAttr.getJavaClass();
+ if (!javaClass.isPrimitive() || !(value instanceof Number) || ((Number) value).intValue() != 0) {
+ idMap.put(dbAttrName, value);
+ continue;
+ }
+ }
+ }
+ }
+
+ // skip db-generated
+ if (supportsGeneratedKeys && dbAttr.isGenerated()) {
+ continue;
+ }
+
+ // only a single key can be generated from DB... if this is done already in this loop, we must bail out.
+ if (autoPkDone) {
+ throw new CayenneRuntimeException("Primary Key autogeneration only works for a single attribute.");
+ }
+
+ // finally, use database generation mechanism
+ try {
+ Object pkValue = pkGenerator.generatePk(lastNode, dbAttr);
+ idMap.put(dbAttrName, pkValue);
+ autoPkDone = true;
+ } catch (Exception ex) {
+ throw new CayenneRuntimeException("Error generating PK: %s", ex, ex.getMessage());
+ }
+ }
+ }
+}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/flush/PostprocessVisitor.java b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/PostprocessVisitor.java
new file mode 100644
index 0000000..35a55a5
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/PostprocessVisitor.java
@@ -0,0 +1,143 @@
+/*****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.access.flush;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+
+import org.apache.cayenne.DataObject;
+import org.apache.cayenne.DataRow;
+import org.apache.cayenne.ObjectId;
+import org.apache.cayenne.access.DataContext;
+import org.apache.cayenne.access.flush.operation.DbRowOp;
+import org.apache.cayenne.access.flush.operation.DbRowOpVisitor;
+import org.apache.cayenne.access.flush.operation.DeleteDbRowOp;
+import org.apache.cayenne.access.flush.operation.InsertDbRowOp;
+import org.apache.cayenne.access.flush.operation.UpdateDbRowOp;
+import org.apache.cayenne.exp.parser.ASTDbPath;
+import org.apache.cayenne.reflect.ArcProperty;
+import org.apache.cayenne.reflect.ClassDescriptor;
+import org.apache.cayenne.reflect.ToManyMapProperty;
+
+/**
+ * @since 4.2
+ */
+class PostprocessVisitor implements DbRowOpVisitor<Void> {
+
+ private final DataContext context;
+ private Map<ObjectId, DataRow> updatedSnapshots;
+ private Collection<ObjectId> deletedIds;
+
+ PostprocessVisitor(DataContext context) {
+ this.context = context;
+ }
+
+ @Override
+ public Void visitInsert(InsertDbRowOp dbRow) {
+ processObjectChange(dbRow);
+ return null;
+ }
+
+ @Override
+ public Void visitUpdate(UpdateDbRowOp dbRow) {
+ processObjectChange(dbRow);
+ return null;
+ }
+
+ private void processObjectChange(DbRowOp dbRow) {
+ if (dbRow.getChangeId().getEntityName().startsWith(ASTDbPath.DB_PREFIX)) {
+ return;
+ }
+
+ DataRow dataRow = context.currentSnapshot(dbRow.getObject());
+
+ if (dbRow.getObject() instanceof DataObject) {
+ DataObject dataObject = (DataObject) dbRow.getObject();
+ dataRow.setReplacesVersion(dataObject.getSnapshotVersion());
+ dataObject.setSnapshotVersion(dataRow.getVersion());
+ }
+
+ if (updatedSnapshots == null) {
+ updatedSnapshots = new HashMap<>();
+ }
+ updatedSnapshots.put(dbRow.getObject().getObjectId(), dataRow);
+
+ // update Map reverse relationships
+ ClassDescriptor descriptor = context.getEntityResolver().getClassDescriptor(dbRow.getChangeId().getEntityName());
+ for (ArcProperty arc : descriptor.getMapArcProperties()) {
+ ToManyMapProperty reverseArc = (ToManyMapProperty) arc.getComplimentaryReverseArc();
+
+ // must resolve faults... hopefully for to-one this will not cause extra fetches...
+ Object source = arc.readProperty(dbRow.getObject());
+ if (source != null && !reverseArc.isFault(source)) {
+ remapTarget(reverseArc, source, dbRow.getObject());
+ }
+ }
+ }
+
+ @Override
+ public Void visitDelete(DeleteDbRowOp dbRow) {
+ if (dbRow.getChangeId().getEntityName().startsWith(ASTDbPath.DB_PREFIX)) {
+ return null;
+ }
+ if (deletedIds == null) {
+ deletedIds = new HashSet<>();
+ }
+ deletedIds.add(dbRow.getChangeId());
+ return null;
+ }
+
+ Collection<ObjectId> getDeletedIds() {
+ return deletedIds == null ? Collections.emptyList() : deletedIds;
+ }
+
+ Map<ObjectId, DataRow> getUpdatedSnapshots() {
+ return updatedSnapshots == null ? Collections.emptyMap() : updatedSnapshots;
+ }
+
+ private void remapTarget(ToManyMapProperty property, Object source, Object target) {
+ @SuppressWarnings("unchecked")
+ Map<Object, Object> map = (Map<Object, Object>) property.readProperty(source);
+ Object newKey = property.getMapKey(target);
+ Object currentValue = map.get(newKey);
+
+ if (currentValue == target) {
+ // nothing to do
+ return;
+ }
+ // else - do not check for conflicts here (i.e. another object mapped for the same key), as we have no control
+ // of the order in which this method is called, so another object may be remapped later by the caller
+ // must do a slow map scan to ensure the object is not mapped under a different key...
+ Iterator<Map.Entry<Object, Object>> it = map.entrySet().iterator();
+ while (it.hasNext()) {
+ Map.Entry<Object, Object> e = it.next();
+ if (e.getValue() == target) {
+ it.remove();
+ break;
+ }
+ }
+
+ map.put(newKey, target);
+ }
+}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/flush/QueryCreatorVisitor.java b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/QueryCreatorVisitor.java
new file mode 100644
index 0000000..d1e2f15
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/QueryCreatorVisitor.java
@@ -0,0 +1,121 @@
+/*****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.access.flush;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.cayenne.access.flush.operation.DbRowOp;
+import org.apache.cayenne.access.flush.operation.DbRowOpVisitor;
+import org.apache.cayenne.access.flush.operation.DeleteDbRowOp;
+import org.apache.cayenne.access.flush.operation.InsertDbRowOp;
+import org.apache.cayenne.access.flush.operation.UpdateDbRowOp;
+import org.apache.cayenne.query.BatchQuery;
+import org.apache.cayenne.query.DeleteBatchQuery;
+import org.apache.cayenne.query.InsertBatchQuery;
+import org.apache.cayenne.query.UpdateBatchQuery;
+
+/**
+ * Visitor that creates batch queries.
+ * It relies on correct sorting of {@link DbRowOp} to just linearly scan of rows and put them in batches.
+ *
+ * @since 4.2
+ */
+// TODO: pass DbRowOp as argument directly to batch...
+class QueryCreatorVisitor implements DbRowOpVisitor<Void> {
+
+ private final List<BatchQuery> queryList;
+ private final int batchSize;
+ private DbRowOp lastRow = null;
+ private BatchQuery lastBatch = null;
+
+ QueryCreatorVisitor(int size) {
+ // these sizes are pretty much random ...
+ this.queryList = new ArrayList<>(Math.min(4, size / 2));
+ this.batchSize = Math.min(2, size / 3);
+ }
+
+ List<BatchQuery> getQueryList() {
+ return queryList;
+ }
+
+ @Override
+ public Void visitInsert(InsertDbRowOp dbRow) {
+ InsertBatchQuery query;
+ if(lastRow == null || !lastRow.isSameBatch(dbRow)) {
+ query = new InsertBatchQuery(dbRow.getEntity(), batchSize);
+ queryList.add(query);
+ lastBatch = query;
+ } else {
+ query = (InsertBatchQuery)lastBatch;
+ }
+ query.add(dbRow.getValues().getSnapshot(), dbRow.getChangeId());
+ lastRow = dbRow;
+ return null;
+ }
+
+ @Override
+ public Void visitUpdate(UpdateDbRowOp dbRow) {
+ // skip empty update..
+ if(dbRow.getValues().isEmpty()) {
+ return null;
+ }
+
+ UpdateBatchQuery query;
+ if(lastRow == null || !lastRow.isSameBatch(dbRow)) {
+ query = new UpdateBatchQuery(
+ dbRow.getEntity(),
+ dbRow.getQualifier().getQualifierAttributes(),
+ dbRow.getValues().getUpdatedAttributes(),
+ dbRow.getQualifier().getNullQualifierNames(),
+ batchSize
+ );
+ query.setUsingOptimisticLocking(dbRow.getQualifier().isUsingOptimisticLocking());
+ queryList.add(query);
+ lastBatch = query;
+ } else {
+ query = (UpdateBatchQuery)lastBatch;
+ }
+ query.add(dbRow.getQualifier().getSnapshot(), dbRow.getValues().getSnapshot(), dbRow.getChangeId());
+ lastRow = dbRow;
+ return null;
+ }
+
+ @Override
+ public Void visitDelete(DeleteDbRowOp dbRow) {
+ DeleteBatchQuery query;
+ if(lastRow == null || !lastRow.isSameBatch(dbRow)) {
+ query = new DeleteBatchQuery(
+ dbRow.getEntity(),
+ dbRow.getQualifier().getQualifierAttributes(),
+ dbRow.getQualifier().getNullQualifierNames(),
+ batchSize
+ );
+ query.setUsingOptimisticLocking(dbRow.getQualifier().isUsingOptimisticLocking());
+ queryList.add(query);
+ lastBatch = query;
+ } else {
+ query = (DeleteBatchQuery)lastBatch;
+ }
+ query.add(dbRow.getQualifier().getSnapshot());
+ lastRow = dbRow;
+ return null;
+ }
+}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/flush/ReplacementIdVisitor.java b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/ReplacementIdVisitor.java
new file mode 100644
index 0000000..405eec6
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/ReplacementIdVisitor.java
@@ -0,0 +1,106 @@
+/*****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.access.flush;
+
+import java.util.Map;
+import java.util.function.Supplier;
+
+import org.apache.cayenne.CayenneRuntimeException;
+import org.apache.cayenne.ObjectId;
+import org.apache.cayenne.Persistent;
+import org.apache.cayenne.access.ObjectStore;
+import org.apache.cayenne.access.flush.operation.DbRowOp;
+import org.apache.cayenne.access.flush.operation.DbRowOpVisitor;
+import org.apache.cayenne.access.flush.operation.InsertDbRowOp;
+import org.apache.cayenne.access.flush.operation.UpdateDbRowOp;
+import org.apache.cayenne.exp.parser.ASTDbPath;
+import org.apache.cayenne.graph.CompoundDiff;
+import org.apache.cayenne.graph.NodeIdChangeOperation;
+import org.apache.cayenne.map.EntityResolver;
+import org.apache.cayenne.reflect.AttributeProperty;
+
+/**
+ * @since 4.2
+ */
+class ReplacementIdVisitor implements DbRowOpVisitor<Void> {
+
+ private final ObjectStore store;
+ private final EntityResolver resolver;
+ private final CompoundDiff result;
+
+ ReplacementIdVisitor(ObjectStore store, EntityResolver resolver, CompoundDiff result) {
+ this.store = store;
+ this.resolver = resolver;
+ this.result = result;
+ }
+
+ @Override
+ public Void visitInsert(InsertDbRowOp dbRow) {
+ updateId(dbRow);
+ dbRow.getValues().getFlattenedIds().forEach((path, id) -> {
+ if(id.isTemporary() && id.isReplacementIdAttached()) {
+ // resolve lazy suppliers
+ for (Map.Entry<String, Object> next : id.getReplacementIdMap().entrySet()) {
+ if (next.getValue() instanceof Supplier) {
+ next.setValue(((Supplier) next.getValue()).get());
+ }
+ }
+ store.markFlattenedPath(dbRow.getChangeId(), path, id.createReplacementId());
+ } else {
+ throw new CayenneRuntimeException("PK for flattened path '%s' of object %s is not set during insert."
+ , path, dbRow.getObject());
+ }
+ });
+ return null;
+ }
+
+ @Override
+ public Void visitUpdate(UpdateDbRowOp dbRow) {
+ updateId(dbRow);
+ return null;
+ }
+
+ private void updateId(DbRowOp dbRow) {
+ ObjectId id = dbRow.getChangeId();
+ if (!id.isReplacementIdAttached()) {
+ if (id.isTemporary()) {
+ throw new CayenneRuntimeException("PK for the object %s is not set during insert.", dbRow.getObject());
+ }
+ return;
+ }
+
+ Persistent object = dbRow.getObject();
+ Map<String, Object> replacement = id.getReplacementIdMap();
+ ObjectId replacementId = id.createReplacementId();
+ if (object.getObjectId() == id && !replacementId.getEntityName().startsWith(ASTDbPath.DB_PREFIX)) {
+ object.setObjectId(replacementId);
+ // update meaningful PKs
+ for (AttributeProperty property: resolver.getClassDescriptor(replacementId.getEntityName()).getIdProperties()) {
+ if(property.getAttribute() != null) {
+ Object value = replacement.get(property.getAttribute().getDbAttributeName());
+ if (value != null) {
+ property.writePropertyDirectly(object, null, value);
+ }
+ }
+ }
+ result.add(new NodeIdChangeOperation(id, replacementId));
+ }
+ }
+}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/flush/RootRowOpProcessor.java b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/RootRowOpProcessor.java
new file mode 100644
index 0000000..edc919c
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/RootRowOpProcessor.java
@@ -0,0 +1,82 @@
+/*****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.access.flush;
+
+import java.util.Collection;
+
+import org.apache.cayenne.CayenneRuntimeException;
+import org.apache.cayenne.ObjectId;
+import org.apache.cayenne.access.ObjectDiff;
+import org.apache.cayenne.access.flush.operation.DbRowOpType;
+import org.apache.cayenne.access.flush.operation.DbRowOpVisitor;
+import org.apache.cayenne.access.flush.operation.DeleteDbRowOp;
+import org.apache.cayenne.access.flush.operation.InsertDbRowOp;
+import org.apache.cayenne.access.flush.operation.UpdateDbRowOp;
+import org.apache.cayenne.map.ObjEntity;
+
+/**
+ * Visitor that runs all required actions based on operation type.
+ * <p>
+ * E.g. it creates values for insert and update, it fills optimistic lock qualifier for update and delete, etc.
+ *
+ * @since 4.2
+ */
+class RootRowOpProcessor implements DbRowOpVisitor<Void> {
+ private final DbRowOpFactory dbRowOpFactory;
+ private ObjectDiff diff;
+
+ RootRowOpProcessor(DbRowOpFactory dbRowOpFactory) {
+ this.dbRowOpFactory = dbRowOpFactory;
+ }
+
+ void setDiff(ObjectDiff diff) {
+ this.diff = diff;
+ }
+
+ @Override
+ public Void visitInsert(InsertDbRowOp dbRow) {
+ diff.apply(new ValuesCreationHandler(dbRowOpFactory, DbRowOpType.INSERT));
+ return null;
+ }
+
+ @Override
+ public Void visitUpdate(UpdateDbRowOp dbRow) {
+ diff.apply(new ValuesCreationHandler(dbRowOpFactory, DbRowOpType.UPDATE));
+ if (dbRowOpFactory.getDescriptor().getEntity().getDeclaredLockType() == ObjEntity.LOCK_TYPE_OPTIMISTIC) {
+ dbRowOpFactory.getDescriptor().visitAllProperties(new OptimisticLockQualifierBuilder(dbRow, diff));
+ }
+ return null;
+ }
+
+ @Override
+ public Void visitDelete(DeleteDbRowOp dbRow) {
+ if (dbRowOpFactory.getDescriptor().getEntity().isReadOnly()) {
+ throw new CayenneRuntimeException("Attempt to modify object(s) mapped to a read-only entity: '%s'. " +
+ "Can't commit changes.", dbRowOpFactory.getDescriptor().getEntity().getName());
+ }
+ diff.apply(new ArcValuesCreationHandler(dbRowOpFactory, DbRowOpType.DELETE));
+ Collection<ObjectId> flattenedIds = dbRowOpFactory.getStore().getFlattenedIds(dbRow.getChangeId());
+ flattenedIds.forEach(id -> dbRowOpFactory.getOrCreate(dbRowOpFactory.getDbEntity(id), id, DbRowOpType.DELETE));
+ if (dbRowOpFactory.getDescriptor().getEntity().getDeclaredLockType() == ObjEntity.LOCK_TYPE_OPTIMISTIC) {
+ dbRowOpFactory.getDescriptor().visitAllProperties(new OptimisticLockQualifierBuilder(dbRow, diff));
+ }
+ return null;
+ }
+}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/flush/ValuesCreationHandler.java b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/ValuesCreationHandler.java
new file mode 100644
index 0000000..a9adacc
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/ValuesCreationHandler.java
@@ -0,0 +1,77 @@
+/*****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.access.flush;
+
+import org.apache.cayenne.CayenneRuntimeException;
+import org.apache.cayenne.ObjectId;
+import org.apache.cayenne.access.flush.operation.DbRowOpType;
+import org.apache.cayenne.access.flush.operation.DbRowOpWithValues;
+import org.apache.cayenne.map.DbAttribute;
+import org.apache.cayenne.map.DbEntity;
+import org.apache.cayenne.map.ObjAttribute;
+import org.apache.cayenne.map.ObjEntity;
+
+/**
+ * Extension of {@link ArcValuesCreationHandler} that also tracks property changes.
+ *
+ * @since 4.2
+ */
+class ValuesCreationHandler extends ArcValuesCreationHandler {
+
+ ValuesCreationHandler(DbRowOpFactory factory, DbRowOpType defaultType) {
+ super(factory, defaultType);
+ }
+
+ @Override
+ public void nodePropertyChanged(Object nodeId, String property, Object oldValue, Object newValue) {
+ ObjectId id = (ObjectId)nodeId;
+ ObjEntity entity = factory.getDescriptor().getEntity();
+ if(entity.isReadOnly()) {
+ throw new CayenneRuntimeException("Attempt to modify object(s) mapped to a read-only entity: '%s'. " +
+ "Can't commit changes.", entity.getName());
+ }
+ ObjAttribute attribute = entity.getAttribute(property);
+ DbEntity dbEntity = entity.getDbEntity();
+
+ if(attribute.isFlattened()) {
+ // get target row ID
+ id = processFlattenedPath(id, null, dbEntity, attribute.getDbAttributePath(), newValue != null);
+ }
+
+ if(id == null) {
+ // some extra safety, shouldn't happen
+ throw new CayenneRuntimeException("Unable to resolve DB row PK for object's %s update of property '%s'"
+ , nodeId, property);
+ }
+
+ DbAttribute dbAttribute = attribute.getDbAttribute();
+ if(dbAttribute.isPrimaryKey()) {
+ if(!(newValue instanceof Number) || ((Number) newValue).longValue() != 0) {
+ id.getReplacementIdMap().put(dbAttribute.getName(), newValue);
+ }
+ }
+
+ DbRowOpWithValues dbRow = factory.get(id);
+ if(dbRow != null) {
+ dbRow.getValues().addValue(dbAttribute, newValue);
+ }
+ }
+
+}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/flush/operation/BaseDbRowOp.java b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/operation/BaseDbRowOp.java
new file mode 100644
index 0000000..648affb
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/operation/BaseDbRowOp.java
@@ -0,0 +1,77 @@
+/*****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.access.flush.operation;
+
+import java.util.Objects;
+
+import org.apache.cayenne.ObjectId;
+import org.apache.cayenne.Persistent;
+import org.apache.cayenne.map.DbEntity;
+
+/**
+ * @since 4.2
+ */
+public abstract class BaseDbRowOp implements DbRowOp {
+
+ protected final Persistent object;
+ protected final DbEntity entity;
+ // Can be ObjEntity id or a DB row id for flattened rows
+ protected final ObjectId changeId;
+
+ protected BaseDbRowOp(Persistent object, DbEntity entity, ObjectId id) {
+ this.object = Objects.requireNonNull(object);
+ this.entity = Objects.requireNonNull(entity);
+ this.changeId = Objects.requireNonNull(id);
+ }
+
+ @Override
+ public DbEntity getEntity() {
+ return entity;
+ }
+
+ @Override
+ public ObjectId getChangeId() {
+ return changeId;
+ }
+
+ @Override
+ public Persistent getObject() {
+ return object;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof DbRowOp)) return false;
+
+ DbRowOp other = (DbRowOp) o;
+ return changeId.equals(other.getChangeId());
+ }
+
+ @Override
+ public int hashCode() {
+ return changeId.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return entity.getName() + " " + changeId;
+ }
+}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/map/EntitySorter.java b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/operation/DbRowOp.java
similarity index 54%
copy from cayenne-server/src/main/java/org/apache/cayenne/map/EntitySorter.java
copy to cayenne-server/src/main/java/org/apache/cayenne/access/flush/operation/DbRowOp.java
index 5c7981e..94b0f91 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/map/EntitySorter.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/operation/DbRowOp.java
@@ -17,37 +17,31 @@
* under the License.
****************************************************************/
-package org.apache.cayenne.map;
+package org.apache.cayenne.access.flush.operation;
-import java.util.List;
+import org.apache.cayenne.ObjectId;
+import org.apache.cayenne.Persistent;
+import org.apache.cayenne.map.DbEntity;
/**
- * Defines API for sorting of Cayenne entities based on their mutual dependencies.
- *
- * @since 1.1
+ * Object that represents some change on DB level.
+ * Common cases are insert/update/delete of single DB row.
+ *
+ * @since 4.2
*/
-public interface EntitySorter {
+public interface DbRowOp {
- /**
- * Sets EntityResolver for this sorter. All entities present in the resolver will be
- * used to determine sort ordering.
- *
- * @since 3.1
- */
- void setEntityResolver(EntityResolver resolver);
+ <T> T accept(DbRowOpVisitor<T> visitor);
- /**
- * Sorts a list of DbEntities.
- */
- void sortDbEntities(List<DbEntity> dbEntities, boolean deleteOrder);
+ DbEntity getEntity();
- /**
- * Sorts a list of ObjEntities.
- */
- void sortObjEntities(List<ObjEntity> objEntities, boolean deleteOrder);
+ ObjectId getChangeId();
+
+ Persistent getObject();
/**
- * Sorts a list of objects belonging to the ObjEntity.
+ * @param rowOp to check
+ * @return is this and rowOp operations belong to same sql batch
*/
- void sortObjectsForEntity(ObjEntity entity, List<?> objects, boolean deleteOrder);
+ boolean isSameBatch(DbRowOp rowOp);
}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/flush/operation/DbRowOpMerger.java b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/operation/DbRowOpMerger.java
new file mode 100644
index 0000000..34dda5b
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/operation/DbRowOpMerger.java
@@ -0,0 +1,82 @@
+/*****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.access.flush.operation;
+
+import java.util.function.BiFunction;
+
+/**
+ * BiFunction that merges two {@link DbRowOp} changing same object.
+ *
+ * @since 4.2
+ */
+public class DbRowOpMerger implements DbRowOpVisitor<DbRowOp>, BiFunction<DbRowOp, DbRowOp, DbRowOp> {
+
+ public static final DbRowOpMerger INSTANCE = new DbRowOpMerger();
+
+ private DbRowOp dbRow;
+
+ public DbRowOpMerger() {
+ }
+
+ @Override
+ public DbRowOp apply(DbRowOp oldValue, DbRowOp newValue) {
+ this.dbRow = oldValue;
+ return newValue.accept(this);
+ }
+
+ @Override
+ public DbRowOp visitInsert(InsertDbRowOp other) {
+ if(dbRow instanceof DeleteDbRowOp) {
+ return new DeleteInsertDbRowOp((DeleteDbRowOp)dbRow, other);
+ }
+ return mergeValues((DbRowOpWithValues) dbRow, other);
+ }
+
+ @Override
+ public DbRowOp visitUpdate(UpdateDbRowOp other) {
+ // delete beats update ...
+ if(dbRow instanceof DeleteDbRowOp) {
+ return dbRow;
+ }
+ return mergeValues((DbRowOpWithValues) dbRow, other);
+ }
+
+ @Override
+ public DbRowOp visitDelete(DeleteDbRowOp other) {
+ if(dbRow.getChangeId() == other.getChangeId()) {
+ return other;
+ }
+ // clash of Insert/Delete with equal ObjectId
+ if(dbRow instanceof InsertDbRowOp) {
+ return new DeleteInsertDbRowOp(other, (InsertDbRowOp)dbRow);
+ }
+ return other;
+ }
+
+ private DbRowOp mergeValues(DbRowOpWithValues left, DbRowOpWithValues right) {
+ if(right.getChangeId() == right.getObject().getObjectId()) {
+ right.getValues().merge(left.getValues());
+ return right;
+ } else {
+ left.getValues().merge(right.getValues());
+ return left;
+ }
+ }
+}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/map/EntitySorter.java b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/operation/DbRowOpSorter.java
similarity index 55%
copy from cayenne-server/src/main/java/org/apache/cayenne/map/EntitySorter.java
copy to cayenne-server/src/main/java/org/apache/cayenne/access/flush/operation/DbRowOpSorter.java
index 5c7981e..24238d0 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/map/EntitySorter.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/operation/DbRowOpSorter.java
@@ -17,37 +17,17 @@
* under the License.
****************************************************************/
-package org.apache.cayenne.map;
+package org.apache.cayenne.access.flush.operation;
import java.util.List;
/**
- * Defines API for sorting of Cayenne entities based on their mutual dependencies.
- *
- * @since 1.1
+ * Sorter of {@link DbRowOp} operations.
+ * @see DefaultDbRowOpSorter default implementation.
+ *
+ * @since 4.2
*/
-public interface EntitySorter {
-
- /**
- * Sets EntityResolver for this sorter. All entities present in the resolver will be
- * used to determine sort ordering.
- *
- * @since 3.1
- */
- void setEntityResolver(EntityResolver resolver);
-
- /**
- * Sorts a list of DbEntities.
- */
- void sortDbEntities(List<DbEntity> dbEntities, boolean deleteOrder);
-
- /**
- * Sorts a list of ObjEntities.
- */
- void sortObjEntities(List<ObjEntity> objEntities, boolean deleteOrder);
+public interface DbRowOpSorter {
- /**
- * Sorts a list of objects belonging to the ObjEntity.
- */
- void sortObjectsForEntity(ObjEntity entity, List<?> objects, boolean deleteOrder);
+ List<DbRowOp> sort(List<DbRowOp> dbRows);
}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/map/EntitySorter.java b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/operation/DbRowOpType.java
similarity index 53%
copy from cayenne-server/src/main/java/org/apache/cayenne/map/EntitySorter.java
copy to cayenne-server/src/main/java/org/apache/cayenne/access/flush/operation/DbRowOpType.java
index 5c7981e..445c500 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/map/EntitySorter.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/operation/DbRowOpType.java
@@ -17,37 +17,30 @@
* under the License.
****************************************************************/
-package org.apache.cayenne.map;
+package org.apache.cayenne.access.flush.operation;
-import java.util.List;
+import org.apache.cayenne.CayenneRuntimeException;
+import org.apache.cayenne.PersistenceState;
+import org.apache.cayenne.Persistent;
/**
- * Defines API for sorting of Cayenne entities based on their mutual dependencies.
- *
- * @since 1.1
+ * @since 4.2
*/
-public interface EntitySorter {
+public enum DbRowOpType implements Comparable<DbRowOpType> {
+ INSERT,
+ UPDATE,
+ DELETE;
- /**
- * Sets EntityResolver for this sorter. All entities present in the resolver will be
- * used to determine sort ordering.
- *
- * @since 3.1
- */
- void setEntityResolver(EntityResolver resolver);
-
- /**
- * Sorts a list of DbEntities.
- */
- void sortDbEntities(List<DbEntity> dbEntities, boolean deleteOrder);
-
- /**
- * Sorts a list of ObjEntities.
- */
- void sortObjEntities(List<ObjEntity> objEntities, boolean deleteOrder);
-
- /**
- * Sorts a list of objects belonging to the ObjEntity.
- */
- void sortObjectsForEntity(ObjEntity entity, List<?> objects, boolean deleteOrder);
+ public static DbRowOpType forObject(Persistent object) {
+ switch (object.getPersistenceState()) {
+ case PersistenceState.NEW:
+ return INSERT;
+ case PersistenceState.MODIFIED:
+ return UPDATE;
+ case PersistenceState.DELETED:
+ return DELETE;
+ }
+ throw new CayenneRuntimeException("Trying to flush object %s in wrong persistence state %s",
+ object, PersistenceState.persistenceStateName(object.getPersistenceState()));
+ }
}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/map/EntitySorter.java b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/operation/DbRowOpVisitor.java
similarity index 53%
copy from cayenne-server/src/main/java/org/apache/cayenne/map/EntitySorter.java
copy to cayenne-server/src/main/java/org/apache/cayenne/access/flush/operation/DbRowOpVisitor.java
index 5c7981e..d179321 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/map/EntitySorter.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/operation/DbRowOpVisitor.java
@@ -17,37 +17,22 @@
* under the License.
****************************************************************/
-package org.apache.cayenne.map;
-
-import java.util.List;
+package org.apache.cayenne.access.flush.operation;
/**
- * Defines API for sorting of Cayenne entities based on their mutual dependencies.
- *
- * @since 1.1
+ * @since 4.2
*/
-public interface EntitySorter {
-
- /**
- * Sets EntityResolver for this sorter. All entities present in the resolver will be
- * used to determine sort ordering.
- *
- * @since 3.1
- */
- void setEntityResolver(EntityResolver resolver);
+public interface DbRowOpVisitor<T> {
- /**
- * Sorts a list of DbEntities.
- */
- void sortDbEntities(List<DbEntity> dbEntities, boolean deleteOrder);
+ default T visitInsert(InsertDbRowOp dbRow) {
+ return null;
+ }
- /**
- * Sorts a list of ObjEntities.
- */
- void sortObjEntities(List<ObjEntity> objEntities, boolean deleteOrder);
+ default T visitUpdate(UpdateDbRowOp dbRow) {
+ return null;
+ }
- /**
- * Sorts a list of objects belonging to the ObjEntity.
- */
- void sortObjectsForEntity(ObjEntity entity, List<?> objects, boolean deleteOrder);
+ default T visitDelete(DeleteDbRowOp dbRow) {
+ return null;
+ }
}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/map/EntitySorter.java b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/operation/DbRowOpWithQualifier.java
similarity index 53%
copy from cayenne-server/src/main/java/org/apache/cayenne/map/EntitySorter.java
copy to cayenne-server/src/main/java/org/apache/cayenne/access/flush/operation/DbRowOpWithQualifier.java
index 5c7981e..ed64e2f 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/map/EntitySorter.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/operation/DbRowOpWithQualifier.java
@@ -17,37 +17,13 @@
* under the License.
****************************************************************/
-package org.apache.cayenne.map;
-
-import java.util.List;
+package org.apache.cayenne.access.flush.operation;
/**
- * Defines API for sorting of Cayenne entities based on their mutual dependencies.
- *
- * @since 1.1
+ * @since 4.2
*/
-public interface EntitySorter {
-
- /**
- * Sets EntityResolver for this sorter. All entities present in the resolver will be
- * used to determine sort ordering.
- *
- * @since 3.1
- */
- void setEntityResolver(EntityResolver resolver);
-
- /**
- * Sorts a list of DbEntities.
- */
- void sortDbEntities(List<DbEntity> dbEntities, boolean deleteOrder);
+public interface DbRowOpWithQualifier extends DbRowOp {
- /**
- * Sorts a list of ObjEntities.
- */
- void sortObjEntities(List<ObjEntity> objEntities, boolean deleteOrder);
+ Qualifier getQualifier();
- /**
- * Sorts a list of objects belonging to the ObjEntity.
- */
- void sortObjectsForEntity(ObjEntity entity, List<?> objects, boolean deleteOrder);
}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/map/EntitySorter.java b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/operation/DbRowOpWithValues.java
similarity index 53%
copy from cayenne-server/src/main/java/org/apache/cayenne/map/EntitySorter.java
copy to cayenne-server/src/main/java/org/apache/cayenne/access/flush/operation/DbRowOpWithValues.java
index 5c7981e..2b8b67e 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/map/EntitySorter.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/operation/DbRowOpWithValues.java
@@ -17,37 +17,13 @@
* under the License.
****************************************************************/
-package org.apache.cayenne.map;
-
-import java.util.List;
+package org.apache.cayenne.access.flush.operation;
/**
- * Defines API for sorting of Cayenne entities based on their mutual dependencies.
- *
- * @since 1.1
+ * @since 4.2
*/
-public interface EntitySorter {
-
- /**
- * Sets EntityResolver for this sorter. All entities present in the resolver will be
- * used to determine sort ordering.
- *
- * @since 3.1
- */
- void setEntityResolver(EntityResolver resolver);
-
- /**
- * Sorts a list of DbEntities.
- */
- void sortDbEntities(List<DbEntity> dbEntities, boolean deleteOrder);
+public interface DbRowOpWithValues extends DbRowOp {
- /**
- * Sorts a list of ObjEntities.
- */
- void sortObjEntities(List<ObjEntity> objEntities, boolean deleteOrder);
+ Values getValues();
- /**
- * Sorts a list of objects belonging to the ObjEntity.
- */
- void sortObjectsForEntity(ObjEntity entity, List<?> objects, boolean deleteOrder);
}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/flush/operation/DefaultDbRowOpSorter.java b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/operation/DefaultDbRowOpSorter.java
new file mode 100644
index 0000000..8af3cf5
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/operation/DefaultDbRowOpSorter.java
@@ -0,0 +1,150 @@
+/*****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.access.flush.operation;
+
+import java.util.Comparator;
+import java.util.List;
+
+import org.apache.cayenne.access.DataDomain;
+import org.apache.cayenne.di.Inject;
+import org.apache.cayenne.di.Provider;
+import org.apache.cayenne.map.DbEntity;
+import org.apache.cayenne.map.EntityResolver;
+import org.apache.cayenne.map.EntitySorter;
+import org.apache.cayenne.map.ObjEntity;
+
+/**
+ * @since 4.2
+ */
+public class DefaultDbRowOpSorter implements DbRowOpSorter {
+
+ protected final Provider<DataDomain> dataDomainProvider;
+ protected volatile Comparator<DbRowOp> comparator;
+
+ public DefaultDbRowOpSorter(@Inject Provider<DataDomain> dataDomainProvider) {
+ this.dataDomainProvider = dataDomainProvider;
+ }
+
+ @Override
+ public List<DbRowOp> sort(List<DbRowOp> dbRows) {
+ // sort by id, operation type and entity relations
+ dbRows.sort(getComparator());
+ // sort reflexively dependent objects
+ sortReflexive(dbRows);
+
+ return dbRows;
+ }
+
+ protected void sortReflexive(List<DbRowOp> sortedDbRows) {
+ DataDomain dataDomain = dataDomainProvider.get();
+ EntitySorter sorter = dataDomain.getEntitySorter();
+ EntityResolver resolver = dataDomain.getEntityResolver();
+
+ DbEntity lastEntity = null;
+ int start = 0;
+ int idx = 0;
+ DbRowOp lastRow = null;
+ for(DbRowOp row : sortedDbRows) {
+ if (row.getEntity() != lastEntity) {
+ start = idx;
+ if(lastEntity != null && sorter.isReflexive(lastEntity)) {
+ ObjEntity objEntity = resolver.getObjEntity(lastRow.getObject().getObjectId().getEntityName());
+ List<DbRowOp> reflexiveSublist = sortedDbRows.subList(start, idx);
+ sorter.sortObjectsForEntity(objEntity, reflexiveSublist, lastRow instanceof DeleteDbRowOp);
+ }
+ lastEntity = row.getEntity();
+ }
+ lastRow = row;
+ idx++;
+ }
+ // sort last chunk
+ if(lastEntity != null && sorter.isReflexive(lastEntity)) {
+ ObjEntity objEntity = resolver.getObjEntity(lastRow.getObject().getObjectId().getEntityName());
+ List<DbRowOp> reflexiveSublist = sortedDbRows.subList(start, idx);
+ sorter.sortObjectsForEntity(objEntity, reflexiveSublist, lastRow instanceof DeleteDbRowOp);
+ }
+ }
+
+ protected Comparator<DbRowOp> getComparator() {
+ Comparator<DbRowOp> local = comparator;
+ if(local == null) {
+ synchronized (this) {
+ local = comparator;
+ if(local == null) {
+ local = new DbRowComparator(dataDomainProvider.get().getEntitySorter());
+ comparator = local;
+ }
+ }
+ }
+ return local;
+ }
+
+ protected static class DbRowComparator implements Comparator<DbRowOp> {
+
+ private final EntitySorter entitySorter;
+
+ protected DbRowComparator(EntitySorter entitySorter) {
+ this.entitySorter = entitySorter;
+ }
+
+ @Override
+ public int compare(DbRowOp left, DbRowOp right) {
+ DbRowOpType leftType = left.accept(DbRowTypeVisitor.INSTANCE);
+ DbRowOpType rightType = right.accept(DbRowTypeVisitor.INSTANCE);
+ int result = leftType.compareTo(rightType);
+
+ // 1. sort by op type
+ if(result != 0) {
+ return result;
+ }
+
+ // 2. sort by entity relations
+ result = entitySorter.getDbEntityComparator().compare(left.getEntity(), right.getEntity());
+ if(result != 0) {
+ // invert result for delete
+ return leftType == DbRowOpType.DELETE ? -result : result;
+ }
+
+ // TODO: 3. sort updates by changed and null attributes to batch it better,
+ // need to check cost vs benefit though
+ return result;
+ }
+ }
+
+ protected static class DbRowTypeVisitor implements DbRowOpVisitor<DbRowOpType> {
+
+ private static final DbRowTypeVisitor INSTANCE = new DbRowTypeVisitor();
+
+ @Override
+ public DbRowOpType visitInsert(InsertDbRowOp diffSnapshot) {
+ return DbRowOpType.INSERT;
+ }
+
+ @Override
+ public DbRowOpType visitUpdate(UpdateDbRowOp diffSnapshot) {
+ return DbRowOpType.UPDATE;
+ }
+
+ @Override
+ public DbRowOpType visitDelete(DeleteDbRowOp diffSnapshot) {
+ return DbRowOpType.DELETE;
+ }
+ }
+}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/flush/operation/DeleteDbRowOp.java b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/operation/DeleteDbRowOp.java
new file mode 100644
index 0000000..8ed6e90
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/operation/DeleteDbRowOp.java
@@ -0,0 +1,72 @@
+/*****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.access.flush.operation;
+
+import org.apache.cayenne.ObjectId;
+import org.apache.cayenne.Persistent;
+import org.apache.cayenne.map.DbEntity;
+
+/**
+ * @since 4.2
+ */
+public class DeleteDbRowOp extends BaseDbRowOp implements DbRowOpWithQualifier {
+
+ protected final Qualifier qualifier;
+
+ public DeleteDbRowOp(Persistent object, DbEntity entity, ObjectId id) {
+ super(object, entity, id);
+ qualifier = new Qualifier(this);
+ }
+
+ @Override
+ public <T> T accept(DbRowOpVisitor<T> visitor) {
+ return visitor.visitDelete(this);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if(!(o instanceof DbRowOpWithQualifier)) {
+ return false;
+ }
+ return super.equals(o);
+ }
+
+ @Override
+ public boolean isSameBatch(DbRowOp rowOp) {
+ if(!(rowOp instanceof DeleteDbRowOp)) {
+ return false;
+ }
+ if(!rowOp.getEntity().getName().equals(getEntity().getName())) {
+ return false;
+ }
+ DeleteDbRowOp other = (DeleteDbRowOp) rowOp;
+ return qualifier.isSameBatch(other.qualifier);
+ }
+
+ @Override
+ public Qualifier getQualifier() {
+ return qualifier;
+ }
+
+ @Override
+ public String toString() {
+ return "delete " + super.toString();
+ }
+}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/map/EntitySorter.java b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/operation/DeleteInsertDbRowOp.java
similarity index 53%
copy from cayenne-server/src/main/java/org/apache/cayenne/map/EntitySorter.java
copy to cayenne-server/src/main/java/org/apache/cayenne/access/flush/operation/DeleteInsertDbRowOp.java
index 5c7981e..40191f5 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/map/EntitySorter.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/operation/DeleteInsertDbRowOp.java
@@ -17,37 +17,33 @@
* under the License.
****************************************************************/
-package org.apache.cayenne.map;
-
-import java.util.List;
+package org.apache.cayenne.access.flush.operation;
/**
- * Defines API for sorting of Cayenne entities based on their mutual dependencies.
- *
- * @since 1.1
+ * Special case op, that describes delete/insert sequence of different objects
+ * that have same ObjectId (known example: meaningful PK set to same value as used before).
+ *
+ * @since 4.2
*/
-public interface EntitySorter {
+public class DeleteInsertDbRowOp extends BaseDbRowOp {
- /**
- * Sets EntityResolver for this sorter. All entities present in the resolver will be
- * used to determine sort ordering.
- *
- * @since 3.1
- */
- void setEntityResolver(EntityResolver resolver);
+ private final DeleteDbRowOp delete;
+ private final InsertDbRowOp insert;
- /**
- * Sorts a list of DbEntities.
- */
- void sortDbEntities(List<DbEntity> dbEntities, boolean deleteOrder);
+ public DeleteInsertDbRowOp(DeleteDbRowOp delete, InsertDbRowOp insert) {
+ super(delete.getObject(), delete.getEntity(), delete.getChangeId());
+ this.delete = delete;
+ this.insert = insert;
+ }
- /**
- * Sorts a list of ObjEntities.
- */
- void sortObjEntities(List<ObjEntity> objEntities, boolean deleteOrder);
+ @Override
+ public <T> T accept(DbRowOpVisitor<T> visitor) {
+ visitor.visitDelete(delete);
+ return visitor.visitInsert(insert);
+ }
- /**
- * Sorts a list of objects belonging to the ObjEntity.
- */
- void sortObjectsForEntity(ObjEntity entity, List<?> objects, boolean deleteOrder);
+ @Override
+ public boolean isSameBatch(DbRowOp rowOp) {
+ return false;
+ }
}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/flush/operation/InsertDbRowOp.java b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/operation/InsertDbRowOp.java
new file mode 100644
index 0000000..9eafbd6
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/operation/InsertDbRowOp.java
@@ -0,0 +1,71 @@
+/*****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.access.flush.operation;
+
+import org.apache.cayenne.ObjectId;
+import org.apache.cayenne.Persistent;
+import org.apache.cayenne.map.DbEntity;
+
+/**
+ * @since 4.2
+ */
+public class InsertDbRowOp extends BaseDbRowOp implements DbRowOpWithValues {
+
+ protected final Values values;
+
+ public InsertDbRowOp(Persistent object, DbEntity entity, ObjectId id) {
+ super(object, entity, id);
+ values = new Values(this, true);
+ }
+
+ @Override
+ public <T> T accept(DbRowOpVisitor<T> visitor) {
+ return visitor.visitInsert(this);
+ }
+
+ @Override
+ public Values getValues() {
+ return values;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ // TODO: here go troubles with transitivity
+ // insert = update, update = delete, delete != insert
+ // though we need this only to store in a hash map, so it should be ok...
+ if(!(o instanceof DbRowOpWithValues)) {
+ return false;
+ }
+ return super.equals(o);
+ }
+
+ @Override
+ public boolean isSameBatch(DbRowOp rowOp) {
+ if(!(rowOp instanceof InsertDbRowOp)) {
+ return false;
+ }
+ return rowOp.getEntity().getName().equals(getEntity().getName());
+ }
+
+ @Override
+ public String toString() {
+ return "insert " + super.toString();
+ }
+}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/flush/operation/Qualifier.java b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/operation/Qualifier.java
new file mode 100644
index 0000000..f680f3c
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/operation/Qualifier.java
@@ -0,0 +1,147 @@
+/*****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.access.flush.operation;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.apache.cayenne.map.DbAttribute;
+
+/**
+ * Qualifier of DB row. It uses PK and optimistic lock qualifier if any.
+ *
+ * @since 4.2
+ */
+public class Qualifier {
+
+ protected final DbRowOp row;
+ // additional qualifier for optimistic lock
+ protected Map<DbAttribute, Object> additionalQualifier;
+ protected List<String> nullNames;
+ protected boolean optimisticLock;
+
+ protected Qualifier(DbRowOp row) {
+ this.row = row;
+ }
+
+ public Map<String, Object> getSnapshot() {
+ Map<String, Object> idSnapshot = row.getChangeId().getIdSnapshot();
+ if(additionalQualifier == null || additionalQualifier.isEmpty()) {
+ return idSnapshot;
+ }
+
+ Map<String, Object> qualifier = new HashMap<>(additionalQualifier.size() + idSnapshot.size());
+ AtomicBoolean hasPK = new AtomicBoolean(!idSnapshot.isEmpty());
+ idSnapshot.forEach((attr, value) -> {
+ if(value != null) {
+ qualifier.put(attr, value);
+ } else {
+ hasPK.set(false);
+ }
+ });
+
+ if(!hasPK.get() || optimisticLock) {
+ additionalQualifier.forEach((attr, value) ->
+ qualifier.put(attr.getName(), value)
+ );
+ }
+
+ return qualifier;
+ }
+
+ public List<DbAttribute> getQualifierAttributes() {
+ List<DbAttribute> primaryKeys = row.getEntity().getPrimaryKeys();
+ if(additionalQualifier == null || additionalQualifier.isEmpty()) {
+ return primaryKeys;
+ }
+
+ List<DbAttribute> attributes = new ArrayList<>();
+ Map<String, Object> idSnapshot = row.getChangeId().getIdSnapshot();
+ AtomicBoolean hasPK = new AtomicBoolean(!idSnapshot.isEmpty());
+ primaryKeys.forEach(pk -> {
+ if(idSnapshot.get(pk.getName()) != null) {
+ attributes.add(pk);
+ } else {
+ hasPK.set(false);
+ }
+ });
+
+ if(!hasPK.get() || optimisticLock) {
+ attributes.addAll(additionalQualifier.keySet());
+ }
+ return attributes;
+ }
+
+ public Collection<String> getNullQualifierNames() {
+ if(nullNames == null || nullNames.isEmpty()) {
+ return Collections.emptyList();
+ }
+ return nullNames;
+ }
+
+ public void addAdditionalQualifier(DbAttribute dbAttribute, Object value) {
+ addAdditionalQualifier(dbAttribute, value, false);
+ }
+
+ public void addAdditionalQualifier(DbAttribute dbAttribute, Object value, boolean optimisticLock) {
+ if(additionalQualifier == null) {
+ additionalQualifier = new HashMap<>();
+ }
+
+ additionalQualifier.put(dbAttribute, value);
+ if(value == null) {
+ if(nullNames == null) {
+ nullNames = new ArrayList<>();
+ }
+ nullNames.add(dbAttribute.getName());
+ }
+
+ if(optimisticLock) {
+ this.optimisticLock = true;
+ }
+ }
+
+ public boolean isUsingOptimisticLocking() {
+ return optimisticLock;
+ }
+
+ public boolean isSameBatch(Qualifier other) {
+ if(additionalQualifier == null) {
+ return other.additionalQualifier == null;
+ }
+ if(optimisticLock != other.optimisticLock) {
+ return false;
+ }
+ if(other.additionalQualifier == null) {
+ return false;
+ }
+ if(!additionalQualifier.keySet().equals(other.additionalQualifier.keySet())) {
+ return false;
+ }
+ return Objects.equals(nullNames, other.nullNames);
+ }
+
+}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/flush/operation/UpdateDbRowOp.java b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/operation/UpdateDbRowOp.java
new file mode 100644
index 0000000..fccaa80
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/operation/UpdateDbRowOp.java
@@ -0,0 +1,74 @@
+/*****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.access.flush.operation;
+
+import org.apache.cayenne.ObjectId;
+import org.apache.cayenne.Persistent;
+import org.apache.cayenne.map.DbEntity;
+
+/**
+ * @since 4.2
+ */
+public class UpdateDbRowOp extends BaseDbRowOp implements DbRowOpWithValues, DbRowOpWithQualifier {
+
+ protected final Values values;
+ protected final Qualifier qualifier;
+
+ public UpdateDbRowOp(Persistent object, DbEntity entity, ObjectId id) {
+ super(object, entity, id);
+ values = new Values(this, false);
+ qualifier = new Qualifier(this);
+ }
+
+ @Override
+ public <T> T accept(DbRowOpVisitor<T> visitor) {
+ return visitor.visitUpdate(this);
+ }
+
+ @Override
+ public Qualifier getQualifier() {
+ return qualifier;
+ }
+
+ @Override
+ public Values getValues() {
+ return values;
+ }
+
+ @Override
+ public boolean isSameBatch(DbRowOp rowOp) {
+ if(!(rowOp instanceof UpdateDbRowOp)) {
+ return false;
+ }
+ if(!rowOp.getEntity().getName().equals(getEntity().getName())) {
+ return false;
+ }
+ UpdateDbRowOp other = (UpdateDbRowOp) rowOp;
+ if(!values.isSameBatch(other.values)) {
+ return false;
+ }
+ return qualifier.isSameBatch(other.qualifier);
+ }
+
+ @Override
+ public String toString() {
+ return "update " + super.toString();
+ }
+}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/flush/operation/Values.java b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/operation/Values.java
new file mode 100644
index 0000000..248d891
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/operation/Values.java
@@ -0,0 +1,151 @@
+/*****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.access.flush.operation;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.cayenne.ObjectId;
+import org.apache.cayenne.map.DbAttribute;
+
+/**
+ * Collection of values that should be inserted or updated in DB.
+ *
+ * @since 4.2
+ */
+public class Values {
+
+ protected final DbRowOp row;
+ protected final boolean includeId;
+ // new values to store to DB
+ protected Map<String, Object> snapshot;
+ protected List<DbAttribute> updatedAttributes;
+ // generated flattened Ids for this insert
+ protected Map<String, ObjectId> flattenedIds;
+
+ public Values(DbRowOp row, boolean includeId) {
+ this.row = row;
+ this.includeId = includeId;
+ }
+
+ public void addValue(DbAttribute attribute, Object value) {
+ if(snapshot == null) {
+ snapshot = new HashMap<>();
+ updatedAttributes = new ArrayList<>();
+ }
+ computeSnapshotValue(attribute.getName(), value);
+ if(!updatedAttributes.contains(attribute)) {
+ updatedAttributes.add(attribute);
+ }
+ }
+
+ private void computeSnapshotValue(String attribute, Object value) {
+ snapshot.putIfAbsent(attribute, value);
+ }
+
+ public void merge(Values other) {
+ if(this.snapshot == null) {
+ this.snapshot = other.snapshot;
+ this.updatedAttributes = other.updatedAttributes;
+ } else if(other.snapshot != null) {
+ other.snapshot.forEach(this::computeSnapshotValue);
+ other.updatedAttributes.forEach(attr -> {
+ if(!updatedAttributes.contains(attr)) {
+ updatedAttributes.add(attr);
+ }
+ });
+ }
+
+ if(other.flattenedIds != null) {
+ if(flattenedIds == null) {
+ flattenedIds = other.getFlattenedIds();
+ } else {
+ other.flattenedIds.forEach((path, id) -> flattenedIds.compute(path, (p, existing) -> {
+ if(id.getEntityName().equals(row.getChangeId().getEntityName())
+ || (existing != null && existing.getEntityName().equals(row.getChangeId().getEntityName()))) {
+ return row.getChangeId();
+ }
+ if(existing != null) {
+ return existing;
+ }
+ return id;
+ }));
+ }
+ }
+ }
+
+ public void addFlattenedId(String path, ObjectId id) {
+ if(flattenedIds == null) {
+ flattenedIds = new HashMap<>();
+ }
+ flattenedIds.put(path, id);
+ }
+
+ public Map<String, Object> getSnapshot() {
+ if(!includeId) {
+ if(snapshot == null) {
+ return Collections.emptyMap();
+ }
+ return snapshot;
+ } else {
+ if (snapshot == null) {
+ snapshot = new HashMap<>();
+ snapshot.putAll(row.getChangeId().getIdSnapshot());
+ return snapshot;
+ }
+ snapshot.putAll(row.getChangeId().getIdSnapshot());
+ return snapshot;
+ }
+ }
+
+ public List<DbAttribute> getUpdatedAttributes() {
+ if(updatedAttributes == null) {
+ return Collections.emptyList();
+ }
+ return updatedAttributes;
+ }
+
+ public Map<String, ObjectId> getFlattenedIds() {
+ if(flattenedIds == null) {
+ return Collections.emptyMap();
+ }
+ return flattenedIds;
+ }
+
+ public boolean isEmpty() {
+ if(includeId) {
+ return false;
+ }
+ return snapshot == null || snapshot.isEmpty();
+ }
+
+ public boolean isSameBatch(Values other) {
+ if(snapshot == null) {
+ return other.snapshot == null;
+ }
+ if(other.snapshot == null) {
+ return false;
+ }
+ return snapshot.keySet().equals(other.snapshot.keySet());
+ }
+}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/ashwood/AshwoodEntitySorter.java b/cayenne-server/src/main/java/org/apache/cayenne/ashwood/AshwoodEntitySorter.java
index 4c1a947..cc8de25 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/ashwood/AshwoodEntitySorter.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/ashwood/AshwoodEntitySorter.java
@@ -25,6 +25,7 @@ import org.apache.cayenne.ObjectContext;
import org.apache.cayenne.ObjectId;
import org.apache.cayenne.Persistent;
import org.apache.cayenne.QueryResponse;
+import org.apache.cayenne.access.flush.operation.DbRowOp;
import org.apache.cayenne.ashwood.graph.Digraph;
import org.apache.cayenne.ashwood.graph.IndegreeTopologicalSort;
import org.apache.cayenne.ashwood.graph.MapDigraph;
@@ -47,6 +48,7 @@ import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.function.Function;
/**
* Implements dependency sorting algorithms for ObjEntities, DbEntities and
@@ -120,11 +122,8 @@ public class AshwoodEntitySorter implements EntitySorter {
if (targetAttribute.isPrimaryKey()) {
if (newReflexive) {
- List<DbRelationship> reflexiveRels = reflexiveDbEntities.get(destination);
- if (reflexiveRels == null) {
- reflexiveRels = new ArrayList<>(1);
- reflexiveDbEntities.put(destination, reflexiveRels);
- }
+ List<DbRelationship> reflexiveRels = reflexiveDbEntities
+ .computeIfAbsent(destination, k -> new ArrayList<>(1));
reflexiveRels.add(candidate);
newReflexive = false;
}
@@ -140,7 +139,6 @@ public class AshwoodEntitySorter implements EntitySorter {
}
}
}
-
}
StrongConnection<DbEntity, List<DbAttribute>> contractor = new StrongConnection<>(referentialDigraph);
@@ -178,51 +176,58 @@ public class AshwoodEntitySorter implements EntitySorter {
@Override
public void sortDbEntities(List<DbEntity> dbEntities, boolean deleteOrder) {
indexSorter();
- Collections.sort(dbEntities, getDbEntityComparator(deleteOrder));
+ dbEntities.sort(getDbEntityComparator(deleteOrder));
}
@Override
public void sortObjEntities(List<ObjEntity> objEntities, boolean deleteOrder) {
indexSorter();
- Collections.sort(objEntities, getObjEntityComparator(deleteOrder));
+ objEntities.sort(getObjEntityComparator(deleteOrder));
}
+ @SuppressWarnings("unchecked")
@Override
public void sortObjectsForEntity(ObjEntity objEntity, List<?> objects, boolean deleteOrder) {
+ if(objects == null || objects.size() == 0) {
+ return;
+ }
indexSorter();
-
- List<Persistent> persistent = (List<Persistent>) objects;
-
DbEntity dbEntity = objEntity.getDbEntity();
-
// if no sorting is required
if (!isReflexive(dbEntity)) {
return;
}
- int size = persistent.size();
- if (size == 0) {
- return;
+ Object probe = objects.get(0);
+ if (probe instanceof DbRowOp) {
+ sortObjectsForEntity(objEntity, (List<DbRowOp>) objects, deleteOrder, DbRowOp::getObject);
+ } else if(probe instanceof Persistent) {
+ sortObjectsForEntity(objEntity, (List<Persistent>) objects, deleteOrder, Function.identity());
+ } else {
+ throw new IllegalArgumentException("Can sort only Persistent or DbRow objects, got " + probe.getClass().getSimpleName());
}
+ }
- EntityResolver resolver = persistent.get(0).getObjectContext().getEntityResolver();
- ClassDescriptor descriptor = resolver.getClassDescriptor(objEntity.getName());
+ protected <E> void sortObjectsForEntity(ObjEntity objEntity, List<E> objects, boolean deleteOrder, Function<E, Persistent> converter) {
+ Digraph<E, Boolean> objectDependencyGraph = buildDigraph(objEntity, objects, converter);
- List<DbRelationship> reflexiveRels = reflexiveDbEntities.get(dbEntity);
- String[] reflexiveRelNames = new String[reflexiveRels.size()];
- for (int i = 0; i < reflexiveRelNames.length; i++) {
- DbRelationship dbRel = reflexiveRels.get(i);
- ObjRelationship objRel = (dbRel != null ? objEntity.getRelationshipForDbRelationship(dbRel) : null);
- reflexiveRelNames[i] = (objRel != null ? objRel.getName() : null);
+ if(!topologicalSort(objects, objectDependencyGraph, deleteOrder)) {
+ throw new CayenneRuntimeException("Sorting objects for %s failed. Cycles found."
+ , objEntity.getClassName());
}
+ }
- List<Persistent> sorted = new ArrayList<>(size);
+ protected <E> Digraph<E, Boolean> buildDigraph(ObjEntity objEntity, List<E> objects, Function<E, Persistent> converter) {
+ EntityResolver resolver = converter.apply(objects.get(0)).getObjectContext().getEntityResolver();
+ ClassDescriptor descriptor = resolver.getClassDescriptor(objEntity.getName());
+ String[] reflexiveRelNames = getReflexiveRelationshipsNames(objEntity);
- Digraph<Persistent, Boolean> objectDependencyGraph = new MapDigraph<>();
- Object[] masters = new Object[reflexiveRelNames.length];
+ int size = objects.size();
+ Digraph<E, Boolean> objectDependencyGraph = new MapDigraph<>();
+ Persistent[] masters = new Persistent[reflexiveRelNames.length];
for (int i = 0; i < size; i++) {
- Persistent current = (Persistent) objects.get(i);
+ E current = objects.get(i);
objectDependencyGraph.addVertex(current);
int actualMasterCount = 0;
for (int k = 0; k < reflexiveRelNames.length; k++) {
@@ -232,11 +237,12 @@ public class AshwoodEntitySorter implements EntitySorter {
continue;
}
- masters[k] = descriptor.getProperty(reflexiveRelName).readProperty(current);
+ Persistent persistent = converter.apply(current);
+ masters[k] = (Persistent)descriptor.getProperty(reflexiveRelName).readProperty(persistent);
if (masters[k] == null) {
- masters[k] = findReflexiveMaster(current, objEntity.getRelationship(reflexiveRelName), current
- .getObjectId().getEntityName());
+ masters[k] = findReflexiveMaster(persistent, objEntity.getRelationship(reflexiveRelName)
+ , persistent.getObjectId().getEntityName());
}
if (masters[k] != null) {
@@ -251,23 +257,26 @@ public class AshwoodEntitySorter implements EntitySorter {
continue;
}
- Persistent masterCandidate = persistent.get(j);
- for (Object master : masters) {
- if (masterCandidate == master) {
+ E masterCandidate = objects.get(j);
+ for (Persistent master : masters) {
+ if (converter.apply(masterCandidate) == master) {
objectDependencyGraph.putArc(masterCandidate, current, Boolean.TRUE);
mastersFound++;
}
}
}
}
+ return objectDependencyGraph;
+ }
- IndegreeTopologicalSort<Persistent> sorter = new IndegreeTopologicalSort<>(objectDependencyGraph);
+ protected <E> boolean topologicalSort(List<E> data, Digraph<E, Boolean> graph, boolean reverse) {
+ IndegreeTopologicalSort<E> sorter = new IndegreeTopologicalSort<>(graph);
+ List<E> sorted = new ArrayList<>(data.size());
while (sorter.hasNext()) {
- Persistent o = sorter.next();
+ E o = sorter.next();
if (o == null) {
- throw new CayenneRuntimeException("Sorting objects for %s failed. Cycles found."
- , objEntity.getClassName());
+ return false;
}
sorted.add(o);
}
@@ -275,25 +284,35 @@ public class AshwoodEntitySorter implements EntitySorter {
// since API requires sorting within the same array,
// simply replace all objects with objects in the right order...
// may come up with something cleaner later
- persistent.clear();
- persistent.addAll(sorted);
+ data.clear();
+ data.addAll(sorted);
- if (deleteOrder) {
- Collections.reverse(persistent);
+ if (reverse) {
+ Collections.reverse(data);
}
+ return true;
}
- protected Object findReflexiveMaster(Persistent object, ObjRelationship toOneRel, String targetEntityName) {
+ protected String[] getReflexiveRelationshipsNames(ObjEntity objEntity) {
+ List<DbRelationship> reflexiveRels = reflexiveDbEntities.get(objEntity.getDbEntity());
+ String[] reflexiveRelNames = new String[reflexiveRels.size()];
+ for (int i = 0; i < reflexiveRelNames.length; i++) {
+ DbRelationship dbRel = reflexiveRels.get(i);
+ ObjRelationship objRel = (dbRel != null ? objEntity.getRelationshipForDbRelationship(dbRel) : null);
+ reflexiveRelNames[i] = (objRel != null ? objRel.getName() : null);
+ }
+ return reflexiveRelNames;
+ }
+
+ protected Persistent findReflexiveMaster(Persistent object, ObjRelationship toOneRel, String targetEntityName) {
DbRelationship finalRel = toOneRel.getDbRelationships().get(0);
ObjectContext context = object.getObjectContext();
- // find committed snapshot - so we can't fetch from the context as it
- // will return
- // dirty snapshot; must go down the stack instead
+ // find committed snapshot - so we can't fetch from the context as it will return dirty snapshot;
+ // must go down the stack instead
- // how do we handle this for NEW objects correctly? For now bail from
- // the method
+ // how do we handle this for NEW objects correctly? For now bail from the method
if (object.getObjectId().isTemporary()) {
return null;
}
@@ -309,12 +328,21 @@ public class AshwoodEntitySorter implements EntitySorter {
ObjectId id = snapshot.createTargetObjectId(targetEntityName, finalRel);
- // not using 'localObject', looking up in context instead, as within the
- // sorter
- // we only care about objects participating in transaction, so no need
- // to create
- // hollow objects
- return (id != null) ? context.getGraphManager().getNode(id) : null;
+ // not using 'localObject', looking up in context instead, as within the sorter
+ // we only care about objects participating in transaction, so no need to create hollow objects
+ return (id != null) ? (Persistent) context.getGraphManager().getNode(id) : null;
+ }
+
+ @Override
+ public Comparator<DbEntity> getDbEntityComparator() {
+ indexSorter();
+ return dbEntityComparator;
+ }
+
+ @Override
+ public Comparator<ObjEntity> getObjEntityComparator() {
+ indexSorter();
+ return objEntityComparator;
}
protected Comparator<DbEntity> getDbEntityComparator(boolean dependantFirst) {
@@ -333,7 +361,9 @@ public class AshwoodEntitySorter implements EntitySorter {
return c;
}
- protected boolean isReflexive(DbEntity metadata) {
+ @Override
+ public boolean isReflexive(DbEntity metadata) {
+ indexSorter();
return reflexiveDbEntities.containsKey(metadata);
}
@@ -378,7 +408,7 @@ public class AshwoodEntitySorter implements EntitySorter {
int index1 = rec1.index;
int index2 = rec2.index;
- int result = index1 > index2 ? 1 : (index1 < index2 ? -1 : 0);
+ int result = Integer.compare(index1, index2);
// TODO: is this check really needed?
if (result != 0 && rec1.component == rec2.component) {
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/map/DbEntity.java b/cayenne-server/src/main/java/org/apache/cayenne/map/DbEntity.java
index d92a7d4..beb4764 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/map/DbEntity.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/map/DbEntity.java
@@ -25,6 +25,7 @@ import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
+import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.function.Function;
@@ -56,7 +57,7 @@ public class DbEntity extends Entity implements ConfigurationNode, DbEntityListe
protected String catalog;
protected String schema;
- protected Collection<DbAttribute> primaryKey;
+ protected List<DbAttribute> primaryKey;
/**
* @since 1.2
@@ -184,8 +185,8 @@ public class DbEntity extends Entity implements ConfigurationNode, DbEntityListe
*
* @since 3.0
*/
- public Collection<DbAttribute> getPrimaryKeys() {
- return Collections.unmodifiableCollection(primaryKey);
+ public List<DbAttribute> getPrimaryKeys() {
+ return Collections.unmodifiableList(primaryKey);
}
/**
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/map/EntitySorter.java b/cayenne-server/src/main/java/org/apache/cayenne/map/EntitySorter.java
index 5c7981e..331f3bc 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/map/EntitySorter.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/map/EntitySorter.java
@@ -19,6 +19,7 @@
package org.apache.cayenne.map;
+import java.util.Comparator;
import java.util.List;
/**
@@ -50,4 +51,24 @@ public interface EntitySorter {
* Sorts a list of objects belonging to the ObjEntity.
*/
void sortObjectsForEntity(ObjEntity entity, List<?> objects, boolean deleteOrder);
+
+ /**
+ * @return comparator for {@link DbEntity}
+ * @since 4.2
+ */
+ Comparator<DbEntity> getDbEntityComparator();
+
+ /**
+ * @return comparator for {@link ObjEntity}
+ * @since 4.2
+ */
+ Comparator<ObjEntity> getObjEntityComparator();
+
+ /**
+ * @param entity to check
+ * @return is entity has reflexive relationships
+ *
+ * @since 4.2
+ */
+ boolean isReflexive(DbEntity entity);
}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/query/DeleteBatchQuery.java b/cayenne-server/src/main/java/org/apache/cayenne/query/DeleteBatchQuery.java
index b58d11c..0ec1721 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/query/DeleteBatchQuery.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/query/DeleteBatchQuery.java
@@ -22,6 +22,7 @@ package org.apache.cayenne.query;
import java.util.Collection;
import java.util.List;
import java.util.Map;
+import java.util.function.Supplier;
import org.apache.cayenne.map.DbAttribute;
import org.apache.cayenne.map.DbEntity;
@@ -91,7 +92,11 @@ public class DeleteBatchQuery extends BatchQuery {
rows.add(new BatchQueryRow(null, dataObjectId) {
@Override
public Object getValue(int i) {
- return qualifier.get(dbAttributes.get(i).getName());
+ Object value = qualifier.get(dbAttributes.get(i).getName());
+ if(value instanceof Supplier) {
+ return ((Supplier) value).get();
+ }
+ return value;
}
});
}