You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cayenne.apache.org by aa...@apache.org on 2026/10/14 19:18:16 UTC
(cayenne) branch master updated: CAY-3050 Array-based DataRows
This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/cayenne.git
The following commit(s) were added to refs/heads/master by this push:
new 513c8791a CAY-3050 Array-based DataRows
513c8791a is described below
commit 513c8791a6c2c74d6f1260a461422779ca290230
Author: Andrus Adamchik <an...@objectstyle.com>
AuthorDate: Wed Oct 14 13:35:22 2026 -0400
CAY-3050 Array-based DataRows
1. Pure array row, with layout cached in ClassDescriptor
2. Side effect - null FKs are now explicitly recorded in object snapshots
This is arguably the correct way vs how we previously ignored it
---
UPGRADE.md | 8 +-
.../crypto/transformer/DefaultMapTransformer.java | 66 +----
.../main/java/org/apache/cayenne/ArrayDataRow.java | 280 -------------------
.../src/main/java/org/apache/cayenne/DataRow.java | 309 +++++++++++++++++----
.../java/org/apache/cayenne/DataRowLayout.java | 171 ++++++++++++
.../main/java/org/apache/cayenne/MapDataRow.java | 132 ---------
.../main/java/org/apache/cayenne/RowLayout.java | 89 ------
.../apache/cayenne/access/DataContextDelegate.java | 10 +-
.../cayenne/access/DataContextObjectStore.java | 29 +-
.../org/apache/cayenne/access/DataRowStore.java | 122 ++++----
.../org/apache/cayenne/access/DataRowUtils.java | 203 +++-----------
.../cayenne/access/HierarchicalObjectResolver.java | 5 +-
.../access/HierarchicalObjectResolverNode.java | 2 +-
.../java/org/apache/cayenne/access/MergePlan.java | 87 +++---
.../org/apache/cayenne/access/NoopDelegate.java | 5 +-
.../org/apache/cayenne/access/ObjectResolver.java | 78 +++---
.../org/apache/cayenne/access/ObjectSnapshots.java | 94 ++++++-
.../cayenne/access/PrefetchProcessorJointNode.java | 56 +---
.../access/ResultScanParentAttachmentStrategy.java | 17 +-
.../java/org/apache/cayenne/access/RowColumns.java | 61 ----
.../apache/cayenne/access/event/SnapshotEvent.java | 33 ++-
.../cayenne/access/jdbc/reader/FullRowReader.java | 16 +-
.../cayenne/access/jdbc/reader/IndexRowReader.java | 4 +-
.../access/jdbc/reader/OffsetRowReader.java | 4 +-
.../apache/cayenne/reflect/ClassDescriptor.java | 11 +
.../reflect/LazyClassDescriptorDecorator.java | 7 +
.../cayenne/reflect/PersistentDescriptor.java | 55 ++++
.../java/org/apache/cayenne/DataRowLayoutTest.java | 146 ++++++++++
.../test/java/org/apache/cayenne/DataRowTest.java | 158 +++++------
.../java/org/apache/cayenne/RowLayoutTest.java | 67 -----
.../access/DataContextConcurrentResolveIT.java | 5 +-
.../access/DataContextDelegateSharedCacheIT.java | 6 +-
.../apache/cayenne/access/DataContextExtrasIT.java | 15 +-
.../org/apache/cayenne/access/DataContextIT.java | 13 +-
.../cayenne/access/DataContextObjectStoreIT.java | 8 +-
.../access/DataContextObjectTrackingIT.java | 14 +-
.../access/DataContextProcedureQueryIT.java | 15 +-
.../cayenne/access/DataContextQueryCachingIT.java | 7 +-
.../org/apache/cayenne/access/DataRowStoreIT.java | 23 +-
.../org/apache/cayenne/access/DataRowUtilsIT.java | 9 +-
.../apache/cayenne/access/LoggingObserverTest.java | 7 +-
.../cayenne/access/MockDataContextDelegate.java | 5 +-
.../apache/cayenne/access/MockDataRowStore.java | 13 +-
.../cayenne/access/NestedDataContextReadIT.java | 8 +-
.../access/PrefetchProcessorTreeBuilderIT.java | 5 +-
.../SimpleIdIncrementalFaultListDataRowsIT.java | 17 +-
.../apache/cayenne/access/SnapshotVersionTest.java | 4 +-
.../cayenne/access/event/SnapshotEventTest.java | 6 +-
.../org/apache/cayenne/event/EventBridgeTest.java | 3 +
.../org/apache/cayenne/query/ProcedureCallIT.java | 13 +-
.../PersistentDescriptor_SnapshotLayoutIT.java | 117 ++++++++
51 files changed, 1317 insertions(+), 1321 deletions(-)
diff --git a/UPGRADE.md b/UPGRADE.md
index 26538aae4..d413e2351 100644
--- a/UPGRADE.md
+++ b/UPGRADE.md
@@ -445,12 +445,8 @@ List<Artist> withExpensivePaintings = ObjectSelect
(`AttributeProperty`, `ToOneProperty`, `ToManyProperty`) instead. A custom `ClassDescriptor` must implement the
three accessors; the deprecated visit methods are now default methods on top of them.
-* Per [CAY-3050](https://issues.apache.org/jira/browse/CAY-3050) `DataRow` is now immutable: `put`, `putAll`,
- `remove` and `clear` throw `UnsupportedOperationException`, including on rows returned by `dataRows()` queries. To
- build a row, fill a `Map` first and pass it to `DataRow.of(entityName, map, version)`, which replaces the public
- constructor. A row with changed values is derived with `row.merged(changes, version)`. Rows read from the database
- are stored as arrays over a shared `RowLayout`, and a result set column with a repeated name is no longer dropped:
- the last column keeps the name, and the earlier ones get an underscore appended (`ID`, `ID_`, ...).
+* Per [CAY-3050](https://issues.apache.org/jira/browse/CAY-3050) `DataRow` is now immutable: `put`, `putAll`, `remove` and `clear` throw
+ `UnsupportedOperationException`, including on rows returned by `dataRows()` queries.
## Upgrading to 5.0-M3
diff --git a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/DefaultMapTransformer.java b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/DefaultMapTransformer.java
index 1c7f416a8..bf93a4ba3 100644
--- a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/DefaultMapTransformer.java
+++ b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/DefaultMapTransformer.java
@@ -18,18 +18,17 @@
****************************************************************/
package org.apache.cayenne.crypto.transformer;
-import java.util.HashMap;
-import java.util.Map;
-
+import org.apache.cayenne.DataRowLayout;
import org.apache.cayenne.DataRow;
-import org.apache.cayenne.RowLayout;
import org.apache.cayenne.crypto.transformer.bytes.BytesDecryptor;
import org.apache.cayenne.crypto.transformer.value.ValueDecryptor;
+import java.util.HashMap;
+import java.util.Map;
+
/**
- * Decrypts the encrypted columns of a row. A row with a {@link RowLayout} is decrypted by position, with the column
- * positions resolved once per layout, i.e. once per result set, as the transformer is created per row reader. Not
- * thread-safe.
+ * Decrypts the encrypted columns of a row. The columns are read through a {@link DataRowLayout.Reader}, which
+ * resolves their positions once per result set.
*
* @since 4.0
*/
@@ -38,66 +37,21 @@ public class DefaultMapTransformer implements MapTransformer {
private final String[] mapKeys;
private final ValueDecryptor[] transformers;
private final BytesDecryptor bytesDecryptor;
-
- // the positions of mapKeys in the layout of the last row seen, -1 for a key the layout does not have
- private RowLayout layout;
- private int[] indexes;
+ private final DataRowLayout.Reader reader;
public DefaultMapTransformer(String[] mapKeys, ValueDecryptor[] transformers, BytesDecryptor bytesDecryptor) {
this.mapKeys = mapKeys;
this.transformers = transformers;
this.bytesDecryptor = bytesDecryptor;
+ this.reader = DataRowLayout.of(mapKeys).reader();
}
@Override
public DataRow transform(DataRow row) {
- RowLayout rowLayout = row.getLayout();
- return rowLayout != null ? transformByPosition(row, rowLayout) : transformByName(row);
- }
-
- private DataRow transformByPosition(DataRow row, RowLayout rowLayout) {
- int[] indexes = indexes(rowLayout);
-
- // the values are copied on the first encrypted non-null value, so a row with nothing to decrypt is returned
- // as is
- Object[] values = null;
- for (int i = 0; i < indexes.length; i++) {
- int index = indexes[i];
- if (index < 0) {
- continue;
- }
-
- Object value = row.valueAt(index);
- if (value != null) {
- if (values == null) {
- values = new Object[rowLayout.size()];
- for (int j = 0; j < values.length; j++) {
- values[j] = row.valueAt(j);
- }
- }
- values[index] = transformers[i].decrypt(bytesDecryptor, value);
- }
- }
-
- return values != null ? DataRow.of(row.getEntityName(), rowLayout, values, row.getVersion()) : row;
- }
-
- private int[] indexes(RowLayout rowLayout) {
- if (rowLayout != layout) {
- int[] resolved = new int[mapKeys.length];
- for (int i = 0; i < mapKeys.length; i++) {
- resolved[i] = rowLayout.indexOf(mapKeys[i]);
- }
- indexes = resolved;
- layout = rowLayout;
- }
- return indexes;
- }
-
- private DataRow transformByName(DataRow row) {
+ // a row with nothing to decrypt is returned as is
Map<String, Object> decrypted = null;
for (int i = 0; i < mapKeys.length; i++) {
- Object value = row.get(mapKeys[i]);
+ Object value = reader.get(row, i);
if (value != null) {
if (decrypted == null) {
decrypted = new HashMap<>();
diff --git a/cayenne/src/main/java/org/apache/cayenne/ArrayDataRow.java b/cayenne/src/main/java/org/apache/cayenne/ArrayDataRow.java
deleted file mode 100644
index 09324f79c..000000000
--- a/cayenne/src/main/java/org/apache/cayenne/ArrayDataRow.java
+++ /dev/null
@@ -1,280 +0,0 @@
-/*****************************************************************
- * 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
- *
- * https://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;
-
-import java.io.Serial;
-import java.util.AbstractCollection;
-import java.util.AbstractMap;
-import java.util.AbstractSet;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.Map;
-import java.util.NoSuchElementException;
-import java.util.Objects;
-import java.util.Set;
-import java.util.function.BiConsumer;
-
-/**
- * A {@link DataRow} stored as an array of values over a {@link RowLayout}.
- *
- * @since 5.0
- */
-final class ArrayDataRow extends DataRow {
-
- private final RowLayout layout;
- private final Object[] values;
-
- ArrayDataRow(String entityName, RowLayout layout, Object[] values, long version) {
- super(entityName, version);
- if (values.length != layout.size()) {
- throw new IllegalArgumentException(
- "Values length " + values.length + " does not match the layout size " + layout.size());
- }
- this.layout = layout;
- this.values = values;
- }
-
- @Override
- public RowLayout getLayout() {
- return layout;
- }
-
- @Override
- public Object valueAt(int index) {
- return values[index];
- }
-
- @Override
- public DataRow withEntityName(String entityName) {
- return new ArrayDataRow(entityName, layout, values, getVersion());
- }
-
- @Override
- public DataRow merged(Map<String, ?> changes, long version) {
- Object[] merged = values.clone();
- for (Map.Entry<String, ?> entry : changes.entrySet()) {
- int index = layout.indexOf(entry.getKey());
- if (index < 0) {
- // a key outside the layout: the result has to be a map
- HashMap<String, Object> map = new HashMap<>(this);
- map.putAll(changes);
- return new MapDataRow(getEntityName(), map, version);
- }
- merged[index] = entry.getValue();
- }
- return new ArrayDataRow(getEntityName(), layout, merged, version);
- }
-
- @Override
- public int size() {
- return layout.size();
- }
-
- @Override
- public boolean isEmpty() {
- return layout.size() == 0;
- }
-
- @Override
- public boolean containsKey(Object key) {
- return key instanceof String name && layout.indexOf(name) >= 0;
- }
-
- @Override
- public boolean containsValue(Object value) {
- for (Object v : values) {
- if (Objects.equals(v, value)) {
- return true;
- }
- }
- return false;
- }
-
- @Override
- public Object get(Object key) {
- if (!(key instanceof String name)) {
- return null;
- }
- int index = layout.indexOf(name);
- return index < 0 ? null : values[index];
- }
-
- @Override
- public Object getOrDefault(Object key, Object defaultValue) {
- if (!(key instanceof String name)) {
- return defaultValue;
- }
- int index = layout.indexOf(name);
- return index < 0 ? defaultValue : values[index];
- }
-
- @Override
- public Set<String> keySet() {
- return new AbstractSet<>() {
- @Override
- public Iterator<String> iterator() {
- return new PositionIterator<>() {
- @Override
- String at(int index) {
- return layout.nameAt(index);
- }
- };
- }
-
- @Override
- public int size() {
- return layout.size();
- }
-
- @Override
- public boolean contains(Object o) {
- return containsKey(o);
- }
- };
- }
-
- @Override
- public Collection<Object> values() {
- return new AbstractCollection<>() {
- @Override
- public Iterator<Object> iterator() {
- return new PositionIterator<>() {
- @Override
- Object at(int index) {
- return values[index];
- }
- };
- }
-
- @Override
- public int size() {
- return layout.size();
- }
- };
- }
-
- @Override
- public Set<Entry<String, Object>> entrySet() {
- return new AbstractSet<>() {
- @Override
- public Iterator<Entry<String, Object>> iterator() {
- return new PositionIterator<>() {
- @Override
- Entry<String, Object> at(int index) {
- return new AbstractMap.SimpleImmutableEntry<>(layout.nameAt(index), values[index]);
- }
- };
- }
-
- @Override
- public int size() {
- return layout.size();
- }
- };
- }
-
- @Override
- public void forEach(BiConsumer<? super String, ? super Object> action) {
- for (int i = 0; i < values.length; i++) {
- action.accept(layout.nameAt(i), values[i]);
- }
- }
-
- @Override
- public boolean equals(Object o) {
- if (o == this) {
- return true;
- }
-
- if (o instanceof ArrayDataRow other && other.layout == layout) {
- return Arrays.equals(values, other.values);
- }
-
- if (!(o instanceof Map<?, ?> map) || map.size() != layout.size()) {
- return false;
- }
-
- for (int i = 0; i < values.length; i++) {
- String name = layout.nameAt(i);
- Object value = values[i];
- if (value == null) {
- if (map.get(name) != null || !map.containsKey(name)) {
- return false;
- }
- } else if (!value.equals(map.get(name))) {
- return false;
- }
- }
- return true;
- }
-
- @Override
- public int hashCode() {
- // the Map contract: the sum of the entry hashes, each being the key hash xor the value hash
- int h = 0;
- for (int i = 0; i < values.length; i++) {
- h += layout.nameAt(i).hashCode() ^ Objects.hashCode(values[i]);
- }
- return h;
- }
-
- @Override
- public String toString() {
- StringBuilder buffer = new StringBuilder(getClass().getSimpleName()).append("@")
- .append(Integer.toHexString(System.identityHashCode(this))).append("[values={");
- for (int i = 0; i < values.length; i++) {
- if (i > 0) {
- buffer.append(", ");
- }
- buffer.append(layout.nameAt(i)).append('=').append(values[i]);
- }
- return buffer.append("}, version=").append(getVersion()).append(']').toString();
- }
-
- /**
- * Serializes as a map row: the layout is not shared across the wire.
- */
- @Serial
- private Object writeReplace() {
- return new MapDataRow(getEntityName(), new HashMap<>(this), getVersion());
- }
-
- private abstract class PositionIterator<T> implements Iterator<T> {
-
- private int next;
-
- abstract T at(int index);
-
- @Override
- public boolean hasNext() {
- return next < values.length;
- }
-
- @Override
- public T next() {
- if (next >= values.length) {
- throw new NoSuchElementException();
- }
- return at(next++);
- }
- }
-}
diff --git a/cayenne/src/main/java/org/apache/cayenne/DataRow.java b/cayenne/src/main/java/org/apache/cayenne/DataRow.java
index e5c13e545..3b97a7fcd 100644
--- a/cayenne/src/main/java/org/apache/cayenne/DataRow.java
+++ b/cayenne/src/main/java/org/apache/cayenne/DataRow.java
@@ -20,47 +20,41 @@
package org.apache.cayenne;
import java.io.Serializable;
+import java.util.AbstractCollection;
+import java.util.AbstractMap;
+import java.util.AbstractSet;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.BiConsumer;
/**
- * DataRow is a read-only map that holds values retrieved from the database for a given query row. DataRows are used to
- * cache raw database data and as a reference point for tracking PersistentObject changes.
- * <p>
- * Every DataRow carries a {@link org.apache.cayenne.access.SnapshotVersion snapshot version}, the version of the
- * operation that produced the row. All rows read by one SQL statement share a version, as do all snapshots of one
- * commit, and an operation that runs later has a greater version. Two rows of the same ObjectId with the same version
- * therefore hold the same data. A cached row keeps its version though while later statements read the same data.
- * <p>
- * A row is immutable: the mutating Map methods throw {@link UnsupportedOperationException}, and the key, value and
- * entry views of a row stored as a map are the views of that map, which the caller is not to change either. A row
- * with changed values is derived with {@link #merged(Map, long)}. Rows come in two forms. A row read from a result set is stored as an
- * array of values over a {@link RowLayout} shared by all the rows of the result set, and exposes its
- * {@link #getLayout() layout} so that the values can be read by position. Any other row is stored as a map and has no
- * layout.
+ * DataRow is a precursor of a persistent object that holds its database values, but models no relationships. A row is
+ * immutable and the mutating Map methods throw {@link UnsupportedOperationException}. A row is serializable when its
+ * values are. Rows serialized in one stream keep sharing their layout.
*
* @since 1.1
*/
-public abstract sealed class DataRow implements Map<String, Object>, Serializable permits MapDataRow, ArrayDataRow {
+public final class DataRow implements Map<String, Object>, Serializable {
- private final long version;
private final String entityName;
+ private final long version;
+ final DataRowLayout layout;
+ final Object[] values;
- DataRow(String entityName, long version) {
+ private DataRow(String entityName, DataRowLayout layout, Object[] values, long version) {
this.entityName = entityName;
+ this.layout = layout;
+ this.values = values;
this.version = version;
}
- /**
- * Creates a row over the given values, with the given entity name and snapshot version. The map is not copied:
- * the row takes ownership of it, and the caller must not change the map after this call. To create a row from a
- * map that stays in use, pass a copy. The map should be serializable for the row to be.
- *
- * @since 5.0
- */
- public static DataRow of(String entityName, Map<String, Object> values, long version) {
- return new MapDataRow(entityName, values, version);
- }
-
/**
* Creates a row over the given values laid out by the given layout, with the given entity name and snapshot
* version. The array is not copied: the row takes ownership of it, and the caller must not use the array after
@@ -68,8 +62,12 @@ public abstract sealed class DataRow implements Map<String, Object>, Serializabl
*
* @since 5.0
*/
- public static DataRow of(String entityName, RowLayout layout, Object[] values, long version) {
- return new ArrayDataRow(entityName, layout, values, version);
+ public static DataRow of(String entityName, DataRowLayout layout, Object[] values, long version) {
+ if (values.length != layout.size()) {
+ throw new IllegalArgumentException(
+ "Values length " + values.length + " does not match the layout size " + layout.size());
+ }
+ return new DataRow(entityName, layout, values, version);
}
/**
@@ -89,64 +87,263 @@ public abstract sealed class DataRow implements Map<String, Object>, Serializabl
return version;
}
- /**
- * Returns the layout of a row stored as an array, or null if the row is stored as a map. The values of a row with
- * a layout can be read by position with {@link #valueAt(int)}.
- *
- * @since 5.0
- */
- public abstract RowLayout getLayout();
-
- /**
- * Returns the value at the given position of the {@link #getLayout() layout} of this row.
- *
- * @throws UnsupportedOperationException if the row has no layout
- * @since 5.0
- */
- public abstract Object valueAt(int index);
-
/**
* Returns a row with the given entity name and this row's values and version. The values are shared: rows are
* immutable, so the copy is free.
*
* @since 5.0
*/
- public abstract DataRow withEntityName(String entityName);
+ public DataRow withEntityName(String entityName) {
+ return new DataRow(entityName, layout, values, version);
+ }
/**
* Returns a row with this row's entity name and the given version, holding this row's values overlaid with the
- * given ones. A row with a layout keeps it when every changed key is a column of the layout.
+ * given ones. The row keeps its layout when every changed key is a column of it, and gets its own wider layout
+ * otherwise.
*
* @since 5.0
*/
- public abstract DataRow merged(Map<String, ?> changes, long version);
+ public DataRow merged(Map<String, ?> changes, long version) {
+ Object[] merged = values.clone();
+ List<Map.Entry<String, ?>> added = null;
+
+ for (Map.Entry<String, ?> entry : changes.entrySet()) {
+ int index = layout.indexOf(entry.getKey());
+ if (index >= 0) {
+ merged[index] = entry.getValue();
+ } else {
+ if (added == null) {
+ added = new ArrayList<>();
+ }
+ added.add(entry);
+ }
+ }
+
+ if (added == null) {
+ return new DataRow(entityName, layout, merged, version);
+ }
+
+ int size = values.length;
+ String[] names = new String[size + added.size()];
+ Object[] widened = Arrays.copyOf(merged, names.length);
+ for (int i = 0; i < size; i++) {
+ names[i] = layout.nameAt(i);
+ }
+ for (int i = 0; i < added.size(); i++) {
+ names[size + i] = added.get(i).getKey();
+ widened[size + i] = added.get(i).getValue();
+ }
+ return new DataRow(entityName, DataRowLayout.of(names), widened, version);
+ }
+
+ @Override
+ public int size() {
+ return values.length;
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return values.length == 0;
+ }
+
+ @Override
+ public boolean containsKey(Object key) {
+ return key instanceof String name && layout.indexOf(name) >= 0;
+ }
+
+ @Override
+ public boolean containsValue(Object value) {
+ for (Object v : values) {
+ if (Objects.equals(v, value)) {
+ return true;
+ }
+ }
+ return false;
+ }
@Override
- public final Object put(String key, Object value) {
+ public Object get(Object key) {
+ if (!(key instanceof String name)) {
+ return null;
+ }
+ int index = layout.indexOf(name);
+ return index < 0 ? null : values[index];
+ }
+
+ @Override
+ public Object getOrDefault(Object key, Object defaultValue) {
+ if (!(key instanceof String name)) {
+ return defaultValue;
+ }
+ int index = layout.indexOf(name);
+ return index < 0 ? defaultValue : values[index];
+ }
+
+ @Override
+ public Object put(String key, Object value) {
throw new UnsupportedOperationException("DataRow is immutable");
}
@Override
- public final Object remove(Object key) {
+ public Object remove(Object key) {
throw new UnsupportedOperationException("DataRow is immutable");
}
@Override
- public final void putAll(Map<? extends String, ?> m) {
+ public void putAll(Map<? extends String, ?> m) {
throw new UnsupportedOperationException("DataRow is immutable");
}
@Override
- public final void clear() {
+ public void clear() {
throw new UnsupportedOperationException("DataRow is immutable");
}
+ @Override
+ public Set<String> keySet() {
+ return new AbstractSet<>() {
+ @Override
+ public Iterator<String> iterator() {
+ return new PositionIterator<>() {
+ @Override
+ String at(int index) {
+ return layout.nameAt(index);
+ }
+ };
+ }
+
+ @Override
+ public int size() {
+ return values.length;
+ }
+
+ @Override
+ public boolean contains(Object o) {
+ return containsKey(o);
+ }
+ };
+ }
+
+ @Override
+ public Collection<Object> values() {
+ return new AbstractCollection<>() {
+ @Override
+ public Iterator<Object> iterator() {
+ return new PositionIterator<>() {
+ @Override
+ Object at(int index) {
+ return values[index];
+ }
+ };
+ }
+
+ @Override
+ public int size() {
+ return values.length;
+ }
+ };
+ }
+
+ @Override
+ public Set<Entry<String, Object>> entrySet() {
+ return new AbstractSet<>() {
+ @Override
+ public Iterator<Entry<String, Object>> iterator() {
+ return new PositionIterator<>() {
+ @Override
+ Entry<String, Object> at(int index) {
+ return new AbstractMap.SimpleImmutableEntry<>(layout.nameAt(index), values[index]);
+ }
+ };
+ }
+
+ @Override
+ public int size() {
+ return values.length;
+ }
+ };
+ }
+
/**
- * Rows are equal to any Map with the same entries, regardless of the version and the entity name.
+ * Visits the entries in column order. Unlike the entry set, allocates nothing per entry.
*/
@Override
- public abstract boolean equals(Object o);
+ public void forEach(BiConsumer<? super String, ? super Object> action) {
+ for (int i = 0; i < values.length; i++) {
+ action.accept(layout.nameAt(i), values[i]);
+ }
+ }
+ /**
+ * Compares the rows as maps, i.e. by their keys and values and regardless of their entity names, versions or the
+ * order of their columns.
+ */
@Override
- public abstract int hashCode();
+ public boolean equals(Object o) {
+ if (o == this) {
+ return true;
+ }
+ if (o instanceof DataRow other && other.layout == layout) {
+ return Arrays.equals(values, other.values);
+ }
+ if (!(o instanceof Map<?, ?> map) || map.size() != values.length) {
+ return false;
+ }
+ for (int i = 0; i < values.length; i++) {
+ String name = layout.nameAt(i);
+ Object value = values[i];
+ if (value == null) {
+ if (map.get(name) != null || !map.containsKey(name)) {
+ return false;
+ }
+ } else if (!value.equals(map.get(name))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ // the Map contract: the sum of the entry hashes, each being the key hash xor the value hash
+ int h = 0;
+ for (int i = 0; i < values.length; i++) {
+ h += layout.nameAt(i).hashCode() ^ Objects.hashCode(values[i]);
+ }
+ return h;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder buffer = new StringBuilder("DataRow@")
+ .append(Integer.toHexString(System.identityHashCode(this))).append("[values={");
+ for (int i = 0; i < values.length; i++) {
+ if (i > 0) {
+ buffer.append(", ");
+ }
+ buffer.append(layout.nameAt(i)).append('=').append(values[i]);
+ }
+ return buffer.append("}, version=").append(version).append(']').toString();
+ }
+
+ private abstract class PositionIterator<T> implements Iterator<T> {
+
+ private int next;
+
+ abstract T at(int index);
+
+ @Override
+ public boolean hasNext() {
+ return next < values.length;
+ }
+
+ @Override
+ public T next() {
+ if (next >= values.length) {
+ throw new NoSuchElementException();
+ }
+ return at(next++);
+ }
+ }
}
diff --git a/cayenne/src/main/java/org/apache/cayenne/DataRowLayout.java b/cayenne/src/main/java/org/apache/cayenne/DataRowLayout.java
new file mode 100644
index 000000000..505c6448e
--- /dev/null
+++ b/cayenne/src/main/java/org/apache/cayenne/DataRowLayout.java
@@ -0,0 +1,171 @@
+/*****************************************************************
+ * 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
+ *
+ * https://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;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * The layout of a {@link DataRow}: the column names in column order, with a lookup of a name to its position. A row
+ * holds its values by position in its layout, and all the rows of one result set share a layout. Layouts are
+ * immutable and compared by identity, so two rows share a layout only when they came from the same reader. A layout
+ * is serializable, and the rows serialized in one stream keep sharing theirs.
+ * <p>
+ * A consumer that needs a fixed set of columns from rows of any layouts builds a layout of those names and reads
+ * them through a {@link Reader} of it.
+ *
+ * @since 5.0
+ */
+public final class DataRowLayout implements Serializable {
+
+ private final String[] names;
+ private final transient Map<String, Integer> indexes;
+
+ private DataRowLayout(String[] names) {
+ this.indexes = new HashMap<>((int) Math.ceil(names.length / 0.75));
+
+ // the array is owned, so the duplicates are renamed in place
+ for (int i = names.length - 1; i >= 0; i--) {
+ String name = names[i];
+ if (indexes.containsKey(name)) {
+ do {
+ name = name + "_";
+ } while (indexes.containsKey(name));
+ names[i] = name;
+ }
+ indexes.put(name, i);
+ }
+ this.names = names;
+ }
+
+ /**
+ * Returns a layout of the given column names, in the given order. A repeated name is deduplicated: the last
+ * occurrence keeps the name, the way the last put wins in a map, and each earlier one is renamed with an
+ * underscore appended until it is unique.
+ */
+ public static DataRowLayout of(String... names) {
+ return new DataRowLayout(names.clone());
+ }
+
+ /**
+ * Returns the number of columns.
+ */
+ public int size() {
+ return names.length;
+ }
+
+ /**
+ * Returns the position of the named column, or -1 if there is no such column.
+ */
+ public int indexOf(String name) {
+ Integer index = indexes.get(name);
+ return index != null ? index : -1;
+ }
+
+ /**
+ * Returns the name of the column at the given position.
+ */
+ public String nameAt(int index) {
+ return names[index];
+ }
+
+ /**
+ * Returns a reader of the columns of this layout from rows of any layouts.
+ */
+ public Reader reader() {
+ return new Reader(this);
+ }
+
+ @Override
+ public String toString() {
+ return "DataRowLayout[" + String.join(",", names) + "]";
+ }
+
+ /**
+ * Rebuilds the index of the names, which is not serialized.
+ */
+ @Serial
+ private Object readResolve() {
+ return new DataRowLayout(names);
+ }
+
+ /**
+ * Reads the columns of a layout from rows of any layouts. A row of the reader's own layout is read by position.
+ * A row of another layout has the reader's column names resolved to positions in it, which the reader keeps
+ * until it reads a row of yet another layout, so a loop over a result set resolves the positions once.
+ * <p>
+ * A reader is thus a per-execution object: one is created per query execution, or per other loop over the rows
+ * of one result set, and not shared across them. It may still be shared between threads: the resolved positions
+ * are kept as one immutable pair, and the race of two threads resolving them costs a repeated resolution only.
+ */
+ public static final class Reader {
+
+ private final DataRowLayout layout;
+ private Resolved resolved;
+
+ private record Resolved(DataRowLayout rowLayout, int[] indexes) {
+ }
+
+ private Reader(DataRowLayout layout) {
+ this.layout = layout;
+ }
+
+ /**
+ * Returns the value of the i-th column of the reader's layout in the row, or null if the row has no such
+ * column. A null is told from a missing column with {@link #has(DataRow, int)}.
+ */
+ public Object get(DataRow row, int i) {
+ DataRowLayout rowLayout = row.layout;
+ if (rowLayout == layout) {
+ return row.values[i];
+ }
+ int index = indexesIn(rowLayout)[i];
+ return index < 0 ? null : row.values[index];
+ }
+
+ /**
+ * Returns whether the row has the i-th column of the reader's layout.
+ */
+ public boolean has(DataRow row, int i) {
+ DataRowLayout rowLayout = row.layout;
+ return rowLayout == layout || indexesIn(rowLayout)[i] >= 0;
+ }
+
+ private int[] indexesIn(DataRowLayout rowLayout) {
+ Resolved r = resolved;
+ if (r == null || r.rowLayout != rowLayout) {
+ int[] indexes = new int[layout.names.length];
+ for (int j = 0; j < indexes.length; j++) {
+ indexes[j] = rowLayout.indexOf(layout.names[j]);
+ }
+ r = new Resolved(rowLayout, indexes);
+ resolved = r;
+ }
+ return r.indexes;
+ }
+
+ @Override
+ public String toString() {
+ return "Reader" + layout;
+ }
+ }
+}
diff --git a/cayenne/src/main/java/org/apache/cayenne/MapDataRow.java b/cayenne/src/main/java/org/apache/cayenne/MapDataRow.java
deleted file mode 100644
index 25c7ff40c..000000000
--- a/cayenne/src/main/java/org/apache/cayenne/MapDataRow.java
+++ /dev/null
@@ -1,132 +0,0 @@
-/*****************************************************************
- * 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
- *
- * https://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;
-
-import org.apache.cayenne.util.ToStringBuilder;
-
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Set;
-import java.util.function.BiConsumer;
-
-/**
- * A {@link DataRow} stored as a map.
- *
- * @since 5.0
- */
-final class MapDataRow extends DataRow {
-
- private final Map<String, Object> values;
-
- MapDataRow(String entityName, Map<String, Object> values, long version) {
- super(entityName, version);
- this.values = values;
- }
-
- @Override
- public RowLayout getLayout() {
- return null;
- }
-
- @Override
- public Object valueAt(int index) {
- throw new UnsupportedOperationException("The row has no layout");
- }
-
- @Override
- public DataRow withEntityName(String entityName) {
- return new MapDataRow(entityName, values, getVersion());
- }
-
- @Override
- public DataRow merged(Map<String, ?> changes, long version) {
- HashMap<String, Object> merged = new HashMap<>(values);
- merged.putAll(changes);
- return new MapDataRow(getEntityName(), merged, version);
- }
-
- @Override
- public int size() {
- return values.size();
- }
-
- @Override
- public boolean isEmpty() {
- return values.isEmpty();
- }
-
- @Override
- public boolean containsKey(Object key) {
- return values.containsKey(key);
- }
-
- @Override
- public boolean containsValue(Object value) {
- return values.containsValue(value);
- }
-
- @Override
- public Object get(Object key) {
- return values.get(key);
- }
-
- @Override
- public Object getOrDefault(Object key, Object defaultValue) {
- return values.getOrDefault(key, defaultValue);
- }
-
- @Override
- public Set<String> keySet() {
- return values.keySet();
- }
-
- @Override
- public Collection<Object> values() {
- return values.values();
- }
-
- @Override
- public Set<Entry<String, Object>> entrySet() {
- return values.entrySet();
- }
-
- @Override
- public void forEach(BiConsumer<? super String, ? super Object> action) {
- values.forEach(action);
- }
-
- @Override
- public boolean equals(Object o) {
- return values.equals(o);
- }
-
- @Override
- public int hashCode() {
- return values.hashCode();
- }
-
- @Override
- public String toString() {
- return new ToStringBuilder(this)
- .append("values", values)
- .append(" version", getVersion()).toString();
- }
-}
diff --git a/cayenne/src/main/java/org/apache/cayenne/RowLayout.java b/cayenne/src/main/java/org/apache/cayenne/RowLayout.java
deleted file mode 100644
index 1a7430e7a..000000000
--- a/cayenne/src/main/java/org/apache/cayenne/RowLayout.java
+++ /dev/null
@@ -1,89 +0,0 @@
-/*****************************************************************
- * 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
- *
- * https://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;
-
-import java.util.HashMap;
-import java.util.Map;
-
-/**
- * The shape of a {@link DataRow} stored as an array: the column names in column order, with a lookup of a name to its
- * position. One layout is shared by all the rows of one result set. Layouts are compared by identity, so two rows
- * share a layout only when they came from the same reader.
- *
- * @since 5.0
- */
-public final class RowLayout {
-
- private final String[] names;
- private final Map<String, Integer> indexes;
-
- /**
- * Creates a layout over the given column names. The array is not copied unless a name is repeated in it: the
- * layout takes ownership of it, and the caller must not change it after this call. A repeated name is
- * deduplicated: the last occurrence keeps the name, the way the last put wins in a map, and each earlier one is
- * renamed with an underscore appended until it is unique.
- */
- public RowLayout(String[] names) {
- this.indexes = new HashMap<>((int) Math.ceil(names.length / 0.75));
-
- String[] deduplicated = names;
- for (int i = names.length - 1; i >= 0; i--) {
- String name = names[i];
- if (indexes.containsKey(name)) {
- if (deduplicated == names) {
- deduplicated = names.clone();
- }
- do {
- name = name + "_";
- } while (indexes.containsKey(name));
- deduplicated[i] = name;
- }
- indexes.put(name, i);
- }
- this.names = deduplicated;
- }
-
- /**
- * Returns the number of columns.
- */
- public int size() {
- return names.length;
- }
-
- /**
- * Returns the position of the named column, or -1 if there is no such column.
- */
- public int indexOf(String name) {
- Integer index = indexes.get(name);
- return index != null ? index : -1;
- }
-
- /**
- * Returns the name of the column at the given position.
- */
- public String nameAt(int index) {
- return names[index];
- }
-
- @Override
- public String toString() {
- return "RowLayout[" + String.join(",", names) + "]";
- }
-}
diff --git a/cayenne/src/main/java/org/apache/cayenne/access/DataContextDelegate.java b/cayenne/src/main/java/org/apache/cayenne/access/DataContextDelegate.java
index 4f94662d9..7db0846f5 100644
--- a/cayenne/src/main/java/org/apache/cayenne/access/DataContextDelegate.java
+++ b/cayenne/src/main/java/org/apache/cayenne/access/DataContextDelegate.java
@@ -19,10 +19,11 @@
package org.apache.cayenne.access;
-import org.apache.cayenne.DataRow;
import org.apache.cayenne.Persistent;
import org.apache.cayenne.query.Query;
+import java.util.Map;
+
/**
* Defines API for a DataContext "delegate" - an object that is temporarily passed control
* by DataContext at some critical points in the normal flow of execution. A delegate thus
@@ -59,13 +60,16 @@ public interface DataContextDelegate {
* not resulting in a snapshot change. In the later case snapshot argument may be
* null. If a delegate returns <code>true</code>, ObjectStore will attempt to merge
* the changes into an object.
+ *
+ * @param snapshotInStore the snapshot of the object in the cache, or the changed keys of it with their new
+ * values, or null
*/
- boolean shouldMergeChanges(Persistent object, DataRow snapshotInStore);
+ boolean shouldMergeChanges(Persistent object, Map<String, Object> snapshotInStore);
/**
* Called after a successful merging of external changes to an object. If previosly a
* delegate returned <code>false</code> from
- * {@link #shouldMergeChanges(Persistent, DataRow)}, this method is not invoked,
+ * {@link #shouldMergeChanges(Persistent, Map)}, this method is not invoked,
* since changes were not merged.
*/
void finishedMergeChanges(Persistent object);
diff --git a/cayenne/src/main/java/org/apache/cayenne/access/DataContextObjectStore.java b/cayenne/src/main/java/org/apache/cayenne/access/DataContextObjectStore.java
index 974ed80c2..a3a24ad18 100644
--- a/cayenne/src/main/java/org/apache/cayenne/access/DataContextObjectStore.java
+++ b/cayenne/src/main/java/org/apache/cayenne/access/DataContextObjectStore.java
@@ -48,6 +48,7 @@ import org.apache.cayenne.reflect.ToOneProperty;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
+import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
@@ -590,10 +591,12 @@ public class DataContextObjectStore implements ObjectStore {
void processSnapshotEvent(SnapshotEvent event) {
- Map<ObjectId, DataRow> modifiedDiffs = event.getModifiedDiffs();
+ Map<ObjectId, Map<String, Object>> modifiedDiffs = event.getModifiedDiffs();
if (modifiedDiffs != null && !modifiedDiffs.isEmpty()) {
- for (Map.Entry<ObjectId, DataRow> entry : modifiedDiffs.entrySet()) {
- processUpdatedSnapshot(entry.getKey(), entry.getValue());
+ // the merge plans of the refreshed objects, built once per descriptor per event
+ Map<ClassDescriptor, MergePlan> plans = new HashMap<>();
+ for (Map.Entry<ObjectId, Map<String, Object>> entry : modifiedDiffs.entrySet()) {
+ processUpdatedSnapshot(entry.getKey(), entry.getValue(), event.getVersion(), plans);
}
}
@@ -713,7 +716,7 @@ public class DataContextObjectStore implements ObjectStore {
ClassDescriptor descriptor = context
.getEntityResolver()
.getClassDescriptor(oid.getEntityName());
- mergeDirty(object, descriptor, snapshot);
+ mergeDirty(object, descriptor, snapshot, snapshot.getVersion());
delegate.finishedMergeChanges(object);
}
}
@@ -766,9 +769,14 @@ public class DataContextObjectStore implements ObjectStore {
}
/**
+ * @param diff the changed keys of the snapshot of the id, with their new values
+ * @param version the version of the snapshot the diff is of
+ * @param plans the merge plans built so far for this event, by descriptor. A COMMITTED object is refreshed with
+ * the plan of its descriptor, which is built on the first object of that descriptor
* @since 1.1
*/
- void processUpdatedSnapshot(ObjectId id, DataRow diff) {
+ void processUpdatedSnapshot(ObjectId id, Map<String, Object> diff, long version,
+ Map<ClassDescriptor, MergePlan> plans) {
Persistent object = objectMap.get(id);
@@ -796,12 +804,13 @@ public class DataContextObjectStore implements ObjectStore {
// the snapshot may need a fetch, so it is taken before the object lock
DataRow snapshot = getSnapshot(id);
if (snapshot != null) {
+ MergePlan plan = plans.computeIfAbsent(descriptor, MergePlan::of);
withObjectLock(id, () -> {
// the object may have been modified since the state check above. A MODIFIED object merges
// the cached snapshot when its first change is recorded, so there's nothing to do for it
if (object.getPersistenceState() == PersistenceState.COMMITTED
&& snapshot.getVersion() >= object.getSnapshotVersion()) {
- DataRowUtils.refreshObjectWithSnapshot(descriptor, object, snapshot, true);
+ DataRowUtils.refreshObjectWithSnapshot(plan, object, snapshot, true);
object.setSnapshotVersion(snapshot.getVersion());
}
return null;
@@ -818,7 +827,7 @@ public class DataContextObjectStore implements ObjectStore {
ClassDescriptor descriptor = context
.getEntityResolver()
.getClassDescriptor(id.getEntityName());
- mergeDirty(object, descriptor, diff);
+ mergeDirty(object, descriptor, diff, version);
delegate.finishedMergeChanges(object);
}
}
@@ -830,13 +839,13 @@ public class DataContextObjectStore implements ObjectStore {
* record changes to the object, so it takes the changes lock before the object lock. The snapshot is only merged
* if it is not older than the one the object already reflects.
*/
- private void mergeDirty(Persistent object, ClassDescriptor descriptor, DataRow snapshot) {
+ private void mergeDirty(Persistent object, ClassDescriptor descriptor, Map<String, Object> snapshot, long version) {
withChangesLock(() -> withObjectLock(object.getObjectId(), () -> {
int state = object.getPersistenceState();
if ((state == PersistenceState.MODIFIED || state == PersistenceState.DELETED)
- && snapshot.getVersion() >= object.getSnapshotVersion()) {
+ && version >= object.getSnapshotVersion()) {
DataRowUtils.forceMergeWithSnapshot(context, descriptor, object, snapshot);
- object.setSnapshotVersion(snapshot.getVersion());
+ object.setSnapshotVersion(version);
}
return null;
}));
diff --git a/cayenne/src/main/java/org/apache/cayenne/access/DataRowStore.java b/cayenne/src/main/java/org/apache/cayenne/access/DataRowStore.java
index 2d5170d6b..b6744bb95 100644
--- a/cayenne/src/main/java/org/apache/cayenne/access/DataRowStore.java
+++ b/cayenne/src/main/java/org/apache/cayenne/access/DataRowStore.java
@@ -24,7 +24,6 @@ import org.apache.cayenne.DataRow;
import org.apache.cayenne.ObjectId;
import org.apache.cayenne.PersistenceState;
import org.apache.cayenne.Persistent;
-import org.apache.cayenne.RowLayout;
import org.apache.cayenne.access.event.SnapshotEvent;
import org.apache.cayenne.event.EventBridge;
import org.apache.cayenne.event.EventManager;
@@ -39,6 +38,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
+import java.util.function.BiConsumer;
import java.util.concurrent.ConcurrentMap;
/**
@@ -286,7 +286,7 @@ public class DataRowStore {
Collection<ObjectId> deletedSnapshotIds = event.getDeletedIds();
Collection<ObjectId> invalidatedSnapshotIds = event.getInvalidatedIds();
- Map<ObjectId, DataRow> diffs = event.getModifiedDiffs();
+ Map<ObjectId, Map<String, Object>> diffs = event.getModifiedDiffs();
Collection<ObjectId> indirectlyModifiedIds = event.getIndirectlyModifiedIds();
if (deletedSnapshotIds.isEmpty()
@@ -299,9 +299,10 @@ public class DataRowStore {
processDeletedIDs(deletedSnapshotIds);
processInvalidatedIDs(invalidatedSnapshotIds);
- processUpdateDiffs(diffs);
+ long version = processUpdateDiffs(diffs);
sendUpdateNotification(
event.getPostedBy(),
+ version,
diffs,
deletedSnapshotIds,
invalidatedSnapshotIds,
@@ -331,9 +332,10 @@ public class DataRowStore {
processDeletedIDs(deletedSnapshotIds);
processInvalidatedIDs(invalidatedSnapshotIds);
- Map<ObjectId, DataRow> diffs = processUpdatedSnapshots(updatedSnapshots);
+ Map<ObjectId, Map<String, Object>> diffs = processUpdatedSnapshots(updatedSnapshots);
sendUpdateNotification(
postedBy,
+ version(updatedSnapshots),
diffs,
deletedSnapshotIds,
invalidatedSnapshotIds,
@@ -358,8 +360,20 @@ public class DataRowStore {
}
}
- private Map<ObjectId, DataRow> processUpdatedSnapshots(Map<ObjectId, DataRow> updatedSnapshots) {
- Map<ObjectId, DataRow> diffs = null;
+ /**
+ * Returns the version of the snapshots, which are of one operation, or {@link SnapshotVersion#NONE} if there are
+ * none.
+ */
+ private static long version(Map<ObjectId, DataRow> snapshots) {
+ long version = SnapshotVersion.NONE;
+ for (DataRow snapshot : snapshots.values()) {
+ version = Math.max(version, snapshot.getVersion());
+ }
+ return version;
+ }
+
+ private Map<ObjectId, Map<String, Object>> processUpdatedSnapshots(Map<ObjectId, DataRow> updatedSnapshots) {
+ Map<ObjectId, Map<String, Object>> diffs = null;
// MODIFIED: replace/add snapshots, generate diffs for event
if (!updatedSnapshots.isEmpty()) {
@@ -388,7 +402,7 @@ public class DataRowStore {
// generate diff for the updated event, if this not a new snapshot
if (replaced[0] != null) {
- DataRow diff = diff(replaced[0], newSnapshot);
+ Map<String, Object> diff = diff(replaced[0], newSnapshot);
if (diff != null) {
if (diffs == null) {
@@ -405,68 +419,67 @@ public class DataRowStore {
}
/**
- * Returns a row of the keys of {@code oldSnapshot} whose values differ in {@code newSnapshot}, with the values of
+ * Returns the keys of {@code oldSnapshot} whose values differ in {@code newSnapshot}, with the values of
* {@code newSnapshot}, or null if there are no differences. Key sets are assumed compatible, i.e. both rows are
- * snapshots of the same entity. The diff represents the newer row, so it carries that row's version.
+ * snapshots of the same entity.
*/
- private static DataRow diff(DataRow oldSnapshot, DataRow newSnapshot) {
- HashMap<String, Object> diff = null;
-
- RowLayout layout = oldSnapshot.getLayout();
- if (layout != null) {
- // the old row is an array row: walk its columns by position instead of through entrySet(), which would
- // allocate an entry per column. The new row is normally read by a later statement, so it has a layout of
- // its own, and its values are looked up by name. The layouts are the same object only when both rows came
- // from the same reader, and then the new row can be read by position too
- boolean sameLayout = layout == newSnapshot.getLayout();
- int size = layout.size();
- for (int i = 0; i < size; i++) {
- String name = layout.nameAt(i);
- Object newValue = sameLayout ? newSnapshot.valueAt(i) : newSnapshot.get(name);
- if (!Objects.deepEquals(oldSnapshot.valueAt(i), newValue)) {
- if (diff == null) {
- diff = new HashMap<>(size);
- }
- diff.put(name, newValue);
- }
- }
- } else {
- for (Map.Entry<String, Object> entry : oldSnapshot.entrySet()) {
- Object newValue = newSnapshot.get(entry.getKey());
- if (!Objects.deepEquals(entry.getValue(), newValue)) {
- if (diff == null) {
- diff = new HashMap<>(oldSnapshot.size());
- }
- diff.put(entry.getKey(), newValue);
+ private static Map<String, Object> diff(DataRow oldSnapshot, DataRow newSnapshot) {
+ // forEach, unlike the entry set, allocates no entries
+ Diff diff = new Diff(newSnapshot);
+ oldSnapshot.forEach(diff);
+ return diff.changes;
+ }
+
+ private static final class Diff implements BiConsumer<String, Object> {
+
+ private final DataRow newSnapshot;
+ private HashMap<String, Object> changes;
+
+ Diff(DataRow newSnapshot) {
+ this.newSnapshot = newSnapshot;
+ }
+
+ @Override
+ public void accept(String name, Object oldValue) {
+ Object newValue = newSnapshot.get(name);
+ if (!Objects.deepEquals(oldValue, newValue)) {
+ if (changes == null) {
+ changes = new HashMap<>(newSnapshot.size());
}
+ changes.put(name, newValue);
}
}
-
- return diff != null ? DataRow.of(newSnapshot.getEntityName(), diff, newSnapshot.getVersion()) : null;
}
- private void processUpdateDiffs(Map<ObjectId, DataRow> diffs) {
- // apply snapshot diffs
- if (!diffs.isEmpty()) {
- // the diffs come from another JVM, so their versions are not comparable with the local ones: the merged
- // snapshots take the version of this event
- long version = SnapshotVersion.next();
- for (Map.Entry<ObjectId, DataRow> entry : diffs.entrySet()) {
- ObjectId key = entry.getKey();
- DataRow oldSnapshot = snapshots.remove(key);
+ /**
+ * Applies the diffs of a remote event to the cached snapshots and returns the version the merged snapshots got,
+ * or {@link SnapshotVersion#NONE} if there were no diffs.
+ */
+ private long processUpdateDiffs(Map<ObjectId, Map<String, Object>> diffs) {
+ if (diffs.isEmpty()) {
+ return SnapshotVersion.NONE;
+ }
- if (oldSnapshot == null) {
- continue;
- }
+ // the diffs come from another JVM, so their version is not comparable with the local ones: the merged
+ // snapshots take a version of this JVM
+ long version = SnapshotVersion.next();
+ for (Map.Entry<ObjectId, Map<String, Object>> entry : diffs.entrySet()) {
+ ObjectId key = entry.getKey();
+ DataRow oldSnapshot = snapshots.remove(key);
- snapshots.put(key, oldSnapshot.merged(entry.getValue(), version));
+ if (oldSnapshot == null) {
+ continue;
}
+
+ snapshots.put(key, oldSnapshot.merged(entry.getValue(), version));
}
+ return version;
}
private void sendUpdateNotification(
Object postedBy,
- Map<ObjectId, DataRow> diffs,
+ long version,
+ Map<ObjectId, Map<String, Object>> diffs,
Collection<ObjectId> deletedSnapshotIDs,
Collection<ObjectId> invalidatedSnapshotIDs,
Collection<ObjectId> indirectlyModifiedIds) {
@@ -480,6 +493,7 @@ public class DataRowStore {
SnapshotEvent event = new SnapshotEvent(
this,
postedBy,
+ version,
diffs,
deletedSnapshotIDs,
invalidatedSnapshotIDs,
diff --git a/cayenne/src/main/java/org/apache/cayenne/access/DataRowUtils.java b/cayenne/src/main/java/org/apache/cayenne/access/DataRowUtils.java
index dd6b5d34a..d58f8f59b 100644
--- a/cayenne/src/main/java/org/apache/cayenne/access/DataRowUtils.java
+++ b/cayenne/src/main/java/org/apache/cayenne/access/DataRowUtils.java
@@ -16,16 +16,15 @@
* specific language governing permissions and limitations
* under the License.
****************************************************************/
-
package org.apache.cayenne.access;
+import org.apache.cayenne.DataRowLayout;
import org.apache.cayenne.DataRow;
import org.apache.cayenne.ObjectId;
import org.apache.cayenne.PersistenceState;
import org.apache.cayenne.Persistent;
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.ArcProperty;
import org.apache.cayenne.reflect.AttributeProperty;
@@ -35,7 +34,6 @@ import org.apache.cayenne.reflect.ToManyProperty;
import org.apache.cayenne.reflect.ToOneProperty;
import org.apache.cayenne.util.Util;
-import java.util.List;
import java.util.Map;
/**
@@ -46,72 +44,58 @@ import java.util.Map;
*/
class DataRowUtils {
- /**
- * Merges changes reflected in snapshot map to the object. Changes made to attributes
- * and to-one relationships will be merged. In case an object is already modified,
- * modified properties will not be overwritten.
- */
- static void mergeObjectWithSnapshot(
- DataContext context,
- ClassDescriptor descriptor,
- Persistent object,
- DataRow snapshot) {
- mergeObjectWithSnapshot(context, descriptor, object, snapshot, null);
- }
-
/**
* Merges changes reflected in snapshot map to the object. Changes made to attributes
* and to-one relationships will be merged. In case an object is already modified,
* modified properties will not be overwritten.
*
- * @param plan the positions of the descriptor's columns in the snapshot, or null to look them up by name. A plan
- * of another layout than the snapshot's is ignored
+ * @param plan the plan of the object's descriptor
*/
static void mergeObjectWithSnapshot(
DataContext context,
- ClassDescriptor descriptor,
Persistent object,
DataRow snapshot,
MergePlan plan) {
int state = object.getPersistenceState();
- if (plan != null && plan.layout != snapshot.getLayout()) {
- plan = null;
- }
-
if (state == PersistenceState.HOLLOW) {
- refreshObjectWithSnapshot(descriptor, object, snapshot, true, plan);
- } else if (state != PersistenceState.COMMITTED && !descriptor.getEntity().isReadOnly()) {
- forceMergeWithSnapshot(context, descriptor, object, snapshot);
+ refreshObjectWithSnapshot(plan, object, snapshot, true);
+ } else if (state != PersistenceState.COMMITTED && !plan.descriptor.getEntity().isReadOnly()) {
+ forceMergeWithSnapshot(context, plan.descriptor, object, snapshot);
} else {
// do not invalidate relationships: to-many lists might have just been prefetched, and a to-one target
// whose FK did not change is still valid
- refreshObjectWithSnapshot(descriptor, object, snapshot, false, plan);
+ refreshObjectWithSnapshot(plan, object, snapshot, false);
}
}
- private static void refreshObjectWithSnapshot(
- ClassDescriptor descriptor,
+ /**
+ * Replaces all object attribute values with snapshot values. Sets object state to
+ * COMMITTED, unless the snapshot is partial in which case the state is set to HOLLOW.
+ * With 'invalidateRelationships' all relationships are turned into faults. Otherwise to-many relationships
+ * are left alone, and a resolved to-one target is kept when the snapshot FK still points to it.
+ *
+ * @param plan the plan of the object's descriptor
+ */
+ static void refreshObjectWithSnapshot(
+ MergePlan plan,
Persistent object,
DataRow snapshot,
- boolean invalidateRelationships,
- MergePlan plan) {
-
- if (plan == null) {
- refreshObjectWithSnapshot(descriptor, object, snapshot, invalidateRelationships);
- return;
- }
+ boolean invalidateRelationships) {
boolean isPartialSnapshot = false;
+ DataRowLayout.Reader reader = plan.reader;
AttributeProperty[] attributes = plan.attributes;
- int[] attributeIndexes = plan.attributeIndexes;
for (int i = 0; i < attributes.length; i++) {
AttributeProperty property = attributes[i];
- int index = attributeIndexes[i];
- if (index >= 0) {
- property.writePropertyDirectly(object, null, snapshot.valueAt(index));
+ int column = plan.attributeColumns[i];
+ Object value = reader.get(snapshot, column);
+
+ // a null value is valid, so the column is only checked for when the value is null
+ if (value != null || reader.has(snapshot, column)) {
+ property.writePropertyDirectly(object, null, value);
} else if (property.getAttribute().isLazy()) {
property.writePropertyDirectly(object, null, new AttributeFault(property));
} else {
@@ -131,64 +115,7 @@ class DataRowUtils {
} else {
ToOneProperty[] toOnes = plan.toOnes;
for (int i = 0; i < toOnes.length; i++) {
- refreshToOne(toOnes[i], object, snapshot, plan.toOneFkIndexes[i], plan.toOneTargetNames[i]);
- }
- }
-
- object.setPersistenceState(isPartialSnapshot
- ? PersistenceState.HOLLOW
- : PersistenceState.COMMITTED);
- }
-
- /**
- * Replaces all object attribute values with snapshot values. Sets object state to
- * COMMITTED, unless the snapshot is partial in which case the state is set to HOLLOW.
- * With 'invalidateRelationships' all relationships are turned into faults. Otherwise to-many relationships
- * are left alone, and a resolved to-one target is kept when the snapshot FK still points to it.
- */
- static void refreshObjectWithSnapshot(
- ClassDescriptor descriptor,
- Persistent object,
- DataRow snapshot,
- boolean invalidateRelationships) {
-
- boolean isPartialSnapshot = false;
-
- for (PropertyDescriptor property : descriptor.getProperties()) {
- switch (property) {
- case AttributeProperty attribute -> {
- ObjAttribute attr = attribute.getAttribute();
- String dbAttrPath = attr.getDbAttributePath().value();
-
- Object value = snapshot.get(dbAttrPath);
- attribute.writePropertyDirectly(object, null, value);
-
- // note that a check "snaphsot.get(..) == null" would be incorrect in this
- // case, as NULL value is entirely valid; still save a map lookup by
- // checking for the null value first
- if (value == null && !snapshot.containsKey(dbAttrPath)) {
- if (attr.isLazy()) {
- attribute.writePropertyDirectly(object, null, new AttributeFault(attribute));
- } else {
- isPartialSnapshot = true;
- }
- }
- }
- case ToOneProperty toOne -> {
- if (invalidateRelationships) {
- toOne.invalidate(object);
- } else {
- refreshToOne(toOne, object, snapshot);
- }
- }
- case ToManyProperty toMany -> {
- // "to many" relationships have no information to collect from snapshot
- if (invalidateRelationships) {
- toMany.invalidate(object);
- }
- }
- default -> {
- }
+ refreshToOne(toOnes[i], object, snapshot, reader, plan.toOneFkColumns[i], plan.toOneTargetNames[i]);
}
}
@@ -202,91 +129,43 @@ class DataRowUtils {
* Replacing an unchanged target with a fault would be pointless: the fault resolves from the cached snapshot to
* the very same object. It is also harmful on a shared DataContext, where other threads would observe the
* transient fault and have to resolve it, each contending for the ObjectStore lock.
+ *
+ * @param fkColumns the FK columns, join by join, or null when the FK does not define the arc. A flattened or a
+ * to-dependent-PK relationship is defined by the target rows, so it is invalidated as before
+ * @param targetNames the target PK column of each join
*/
- private static void refreshToOne(ToOneProperty property, Persistent object, DataRow snapshot) {
+ private static void refreshToOne(ToOneProperty property, Persistent object, DataRow snapshot,
+ DataRowLayout.Reader reader, int[] fkColumns, String[] targetNames) {
Object current = property.readPropertyDirectly(object);
if (property.isFaultValue(current)) {
return;
}
- ObjRelationship relationship = property.getRelationship();
-
- // only an FK to the target PK lets the source snapshot tell whether the arc changed. A flattened or a
- // to-dependent-PK relationship is defined by the target rows, so it is invalidated as before
- if (relationship.isSourceIndependentFromTargetChange()) {
+ if (fkColumns == null) {
property.invalidate(object);
return;
}
// the FK is compared with the current target id join by join, without building an intermediate id
Map<String, Object> currentPk = current != null ? ((Persistent) current).getObjectId().getIdSnapshot() : null;
- List<DbJoin> joins = relationship.getDbRelationships().getFirst().getJoins();
int nullFks = 0;
- for (DbJoin join : joins) {
- String fkName = join.getSourceName();
- Object fk = snapshot.get(fkName);
+ for (int i = 0; i < fkColumns.length; i++) {
+ Object fk = reader.get(snapshot, fkColumns[i]);
if (fk == null) {
- if (!snapshot.containsKey(fkName)) {
+ if (!reader.has(snapshot, fkColumns[i])) {
// partial snapshot
property.invalidate(object);
return;
}
nullFks++;
- } else if (currentPk == null || !Util.idValueEquals(fk, currentPk.get(join.getTargetName()))) {
- property.invalidate(object);
- return;
- }
- }
-
- refreshToOne(property, object, current, nullFks, joins.size());
- }
-
- /**
- * The same as {@link #refreshToOne(ToOneProperty, Persistent, DataRow)}, with the FK columns read by position.
- *
- * @param fkIndexes the positions of the FK columns, join by join, or null when the FK does not define the arc
- * @param targetNames the target PK column of each join
- */
- private static void refreshToOne(ToOneProperty property, Persistent object, DataRow snapshot, int[] fkIndexes,
- String[] targetNames) {
- Object current = property.readPropertyDirectly(object);
- if (property.isFaultValue(current)) {
- return;
- }
-
- if (fkIndexes == null) {
- property.invalidate(object);
- return;
- }
-
- Map<String, Object> currentPk = current != null ? ((Persistent) current).getObjectId().getIdSnapshot() : null;
- int nullFks = 0;
-
- for (int i = 0; i < fkIndexes.length; i++) {
- if (fkIndexes[i] < 0) {
- // partial snapshot
- property.invalidate(object);
- return;
- }
- Object fk = snapshot.valueAt(fkIndexes[i]);
- if (fk == null) {
- nullFks++;
} else if (currentPk == null || !Util.idValueEquals(fk, currentPk.get(targetNames[i]))) {
property.invalidate(object);
return;
}
}
- refreshToOne(property, object, current, nullFks, fkIndexes.length);
- }
-
- /**
- * Settles a to-one whose non-null FK columns all point to the current target.
- */
- private static void refreshToOne(ToOneProperty property, Persistent object, Object current, int nullFks,
- int joins) {
- if (nullFks == joins) {
+ if (nullFks == fkColumns.length) {
if (current != null) {
property.writePropertyDirectly(object, current, null);
}
@@ -296,11 +175,15 @@ class DataRowUtils {
}
}
+ /**
+ * Merges a snapshot, or a diff of one, into a MODIFIED or DELETED object: the properties the object has not
+ * changed take the snapshot values.
+ */
static void forceMergeWithSnapshot(
DataContext context,
ClassDescriptor descriptor,
Persistent object,
- DataRow snapshot) {
+ Map<String, Object> snapshot) {
ObjectDiff diff = context
.getObjectStore()
@@ -321,7 +204,7 @@ class DataRowUtils {
private static void mergeAttribute(
AttributeProperty property,
Persistent object,
- DataRow snapshot,
+ Map<String, Object> snapshot,
ObjectDiff diff) {
String dbAttrPath = property.getAttribute().getDbAttributePath().value();
@@ -346,7 +229,7 @@ class DataRowUtils {
DataContext context,
ToOneProperty property,
Persistent object,
- DataRow snapshot,
+ Map<String, Object> snapshot,
ObjectDiff diff) {
ObjRelationship relationship = property.getRelationship();
diff --git a/cayenne/src/main/java/org/apache/cayenne/access/HierarchicalObjectResolver.java b/cayenne/src/main/java/org/apache/cayenne/access/HierarchicalObjectResolver.java
index ea5cf38b7..a5a6c1305 100644
--- a/cayenne/src/main/java/org/apache/cayenne/access/HierarchicalObjectResolver.java
+++ b/cayenne/src/main/java/org/apache/cayenne/access/HierarchicalObjectResolver.java
@@ -20,6 +20,7 @@
package org.apache.cayenne.access;
import org.apache.cayenne.CayenneRuntimeException;
+import org.apache.cayenne.DataRowLayout;
import org.apache.cayenne.DataRow;
import org.apache.cayenne.Persistent;
import org.apache.cayenne.exp.Expression;
@@ -156,7 +157,7 @@ class HierarchicalObjectResolver {
int maxIdQualifierSize = context.getChannel().getDataDomain().getMaxIdQualifierSize();
List<DbJoin> joins = getDbJoins(relationship);
- RowColumns keyColumns = new RowColumns(dataRowKeys(joins, pathPrefix));
+ DataRowLayout.Reader keyReader = DataRowLayout.of(dataRowKeys(joins, pathPrefix)).reader();
List<PrefetchSelectQuery<DataRow>> queries = new ArrayList<>();
PrefetchSelectQuery<DataRow> currentQuery = null;
@@ -179,7 +180,7 @@ class HierarchicalObjectResolver {
List<Object> joinValues = new ArrayList<>(joins.size());
for (int i = 0; i < joins.size(); i++) {
- joinValues.add(keyColumns.get(dataRow, i));
+ joinValues.add(keyReader.get(dataRow, i));
}
if(values.add(joinValues)) {
diff --git a/cayenne/src/main/java/org/apache/cayenne/access/HierarchicalObjectResolverNode.java b/cayenne/src/main/java/org/apache/cayenne/access/HierarchicalObjectResolverNode.java
index 3d8e9dbb9..fca548e99 100644
--- a/cayenne/src/main/java/org/apache/cayenne/access/HierarchicalObjectResolverNode.java
+++ b/cayenne/src/main/java/org/apache/cayenne/access/HierarchicalObjectResolverNode.java
@@ -52,7 +52,7 @@ class HierarchicalObjectResolverNode extends PrefetchObjectResolver {
// determine entity to use
ClassDescriptor classDescriptor = descriptorResolutionStrategy.descriptorForRow(row);
- MergePlan plan = planFor(row, classDescriptor);
+ MergePlan plan = planFor(classDescriptor);
ObjectId anId = createObjectId(row, classDescriptor, plan);
Persistent object = objectFromDataRow(row, anId, classDescriptor, plan);
diff --git a/cayenne/src/main/java/org/apache/cayenne/access/MergePlan.java b/cayenne/src/main/java/org/apache/cayenne/access/MergePlan.java
index 47cb75d1f..baea724b0 100644
--- a/cayenne/src/main/java/org/apache/cayenne/access/MergePlan.java
+++ b/cayenne/src/main/java/org/apache/cayenne/access/MergePlan.java
@@ -16,10 +16,11 @@
* specific language governing permissions and limitations
* under the License.
****************************************************************/
-
package org.apache.cayenne.access;
-import org.apache.cayenne.RowLayout;
+import org.apache.cayenne.CayenneRuntimeException;
+import org.apache.cayenne.DataRowLayout;
+import org.apache.cayenne.DataRow;
import org.apache.cayenne.map.DbAttribute;
import org.apache.cayenne.map.DbJoin;
import org.apache.cayenne.map.ObjRelationship;
@@ -34,56 +35,62 @@ import java.util.Collection;
import java.util.List;
/**
- * The positions of the columns that turn rows of one {@link RowLayout} into objects of one {@link ClassDescriptor}:
- * the primary key, the attributes and the to-one foreign keys. A resolver builds a plan for the rows it converts,
- * so the column names are looked up once per batch of rows instead of once per row. A position of -1 marks a column
- * missing from the layout, i.e. a partial snapshot.
+ * The columns that turn rows into objects of one {@link ClassDescriptor}: the primary key, the attributes and the
+ * to-one foreign keys, by their positions in the descriptor's {@link ClassDescriptor#getSnapshotLayout() snapshot
+ * layout}, and a {@link DataRowLayout.Reader} of that layout, which reads a snapshot built from an object by position
+ * and looks the positions up once per result set otherwise. A resolver builds a plan per descriptor for the rows it
+ * converts.
*/
final class MergePlan {
- final RowLayout layout;
final ClassDescriptor descriptor;
- // the primary key of the descriptor's DbEntity
+ final DataRowLayout layout;
+ final DataRowLayout.Reader reader;
+
+ // the primary key of the descriptor's DbEntity, PK i being column pkColumns[i]
final String[] pkNames;
- final int[] pkIndexes;
+ final int[] pkColumns;
+ // attribute i being column attributeColumns[i]
final AttributeProperty[] attributes;
- final int[] attributeIndexes;
+ final int[] attributeColumns;
final ToOneProperty[] toOnes;
- // per to-one, the positions of the FK columns join by join, or null when the FK does not define the arc
- final int[][] toOneFkIndexes;
- // per to-one, the target PK column of each join
+ // per to-one, the columns of the FK join by join, or null when the FK does not define the arc
+ final int[][] toOneFkColumns;
+ // per to-one, the target PK column name of each join
final String[][] toOneTargetNames;
final ToManyProperty[] toManys;
- private MergePlan(RowLayout layout, ClassDescriptor descriptor, String[] pkNames, int[] pkIndexes,
- AttributeProperty[] attributes, int[] attributeIndexes, ToOneProperty[] toOnes,
- int[][] toOneFkIndexes, String[][] toOneTargetNames, ToManyProperty[] toManys) {
- this.layout = layout;
+ private MergePlan(ClassDescriptor descriptor, DataRowLayout layout, String[] pkNames, int[] pkColumns,
+ AttributeProperty[] attributes, int[] attributeColumns, ToOneProperty[] toOnes,
+ int[][] toOneFkColumns, String[][] toOneTargetNames, ToManyProperty[] toManys) {
this.descriptor = descriptor;
+ this.layout = layout;
+ this.reader = layout.reader();
this.pkNames = pkNames;
- this.pkIndexes = pkIndexes;
+ this.pkColumns = pkColumns;
this.attributes = attributes;
- this.attributeIndexes = attributeIndexes;
+ this.attributeColumns = attributeColumns;
this.toOnes = toOnes;
- this.toOneFkIndexes = toOneFkIndexes;
+ this.toOneFkColumns = toOneFkColumns;
this.toOneTargetNames = toOneTargetNames;
this.toManys = toManys;
}
- static MergePlan of(RowLayout layout, ClassDescriptor descriptor) {
+ static MergePlan of(ClassDescriptor descriptor) {
+
+ DataRowLayout layout = descriptor.getSnapshotLayout();
Collection<DbAttribute> pk = descriptor.getEntity().getDbEntity().getPrimaryKeys();
String[] pkNames = new String[pk.size()];
- int[] pkIndexes = new int[pk.size()];
+ int[] pkColumns = new int[pk.size()];
int i = 0;
for (DbAttribute attribute : pk) {
pkNames[i] = attribute.getName();
- pkIndexes[i] = layout.indexOf(attribute.getName());
- i++;
+ pkColumns[i++] = column(layout, attribute.getName());
}
List<AttributeProperty> attributes = new ArrayList<>();
@@ -99,33 +106,41 @@ final class MergePlan {
}
}
- int[] attributeIndexes = new int[attributes.size()];
- for (i = 0; i < attributeIndexes.length; i++) {
- attributeIndexes[i] = layout.indexOf(attributes.get(i).getAttribute().getDbAttributePath().value());
+ int[] attributeColumns = new int[attributes.size()];
+ for (i = 0; i < attributeColumns.length; i++) {
+ attributeColumns[i] = column(layout, attributes.get(i).getAttribute().getDbAttributePath().value());
}
- int[][] toOneFkIndexes = new int[toOnes.size()][];
+ int[][] toOneFkColumns = new int[toOnes.size()][];
String[][] toOneTargetNames = new String[toOnes.size()][];
- for (i = 0; i < toOneFkIndexes.length; i++) {
+ for (i = 0; i < toOneFkColumns.length; i++) {
ObjRelationship relationship = toOnes.get(i).getRelationship();
// only an FK to the target PK lets the source snapshot tell whether the arc changed
if (relationship.isSourceIndependentFromTargetChange()) {
continue;
}
List<DbJoin> joins = relationship.getDbRelationships().getFirst().getJoins();
- int[] fkIndexes = new int[joins.size()];
+ int[] fkColumns = new int[joins.size()];
String[] targetNames = new String[joins.size()];
- for (int j = 0; j < fkIndexes.length; j++) {
- fkIndexes[j] = layout.indexOf(joins.get(j).getSourceName());
+ for (int j = 0; j < fkColumns.length; j++) {
+ fkColumns[j] = column(layout, joins.get(j).getSourceName());
targetNames[j] = joins.get(j).getTargetName();
}
- toOneFkIndexes[i] = fkIndexes;
+ toOneFkColumns[i] = fkColumns;
toOneTargetNames[i] = targetNames;
}
- return new MergePlan(layout, descriptor, pkNames, pkIndexes,
- attributes.toArray(new AttributeProperty[0]), attributeIndexes,
- toOnes.toArray(new ToOneProperty[0]), toOneFkIndexes, toOneTargetNames,
+ return new MergePlan(descriptor, layout, pkNames, pkColumns,
+ attributes.toArray(new AttributeProperty[0]), attributeColumns,
+ toOnes.toArray(new ToOneProperty[0]), toOneFkColumns, toOneTargetNames,
toManys.toArray(new ToManyProperty[0]));
}
+
+ private static int column(DataRowLayout layout, String name) {
+ int column = layout.indexOf(name);
+ if (column < 0) {
+ throw new CayenneRuntimeException("Column '%s' is not in the snapshot layout %s", name, layout);
+ }
+ return column;
+ }
}
diff --git a/cayenne/src/main/java/org/apache/cayenne/access/NoopDelegate.java b/cayenne/src/main/java/org/apache/cayenne/access/NoopDelegate.java
index 606cb214a..4bfffde48 100644
--- a/cayenne/src/main/java/org/apache/cayenne/access/NoopDelegate.java
+++ b/cayenne/src/main/java/org/apache/cayenne/access/NoopDelegate.java
@@ -19,10 +19,11 @@
package org.apache.cayenne.access;
-import org.apache.cayenne.DataRow;
import org.apache.cayenne.Persistent;
import org.apache.cayenne.query.Query;
+import java.util.Map;
+
/**
* A noop DataContextDelegate.
*
@@ -43,7 +44,7 @@ final class NoopDelegate implements DataContextDelegate {
}
@Override
- public boolean shouldMergeChanges(Persistent object, DataRow snapshotInStore) {
+ public boolean shouldMergeChanges(Persistent object, Map<String, Object> snapshotInStore) {
return true;
}
diff --git a/cayenne/src/main/java/org/apache/cayenne/access/ObjectResolver.java b/cayenne/src/main/java/org/apache/cayenne/access/ObjectResolver.java
index d70bda101..320ca1e77 100644
--- a/cayenne/src/main/java/org/apache/cayenne/access/ObjectResolver.java
+++ b/cayenne/src/main/java/org/apache/cayenne/access/ObjectResolver.java
@@ -20,12 +20,12 @@
package org.apache.cayenne.access;
import org.apache.cayenne.CayenneRuntimeException;
+import org.apache.cayenne.DataRowLayout;
import org.apache.cayenne.DataRow;
import org.apache.cayenne.ObjectContext;
import org.apache.cayenne.ObjectId;
import org.apache.cayenne.PersistenceState;
import org.apache.cayenne.Persistent;
-import org.apache.cayenne.RowLayout;
import org.apache.cayenne.exp.path.CayennePath;
import org.apache.cayenne.map.DbAttribute;
import org.apache.cayenne.map.DbEntity;
@@ -60,7 +60,7 @@ class ObjectResolver {
DescriptorResolutionStrategy descriptorResolutionStrategy;
// the plan of the rows being converted, for the root descriptor, and per subclass descriptor when there is
- // inheritance. Rebuilt when rows of another layout come along
+ // inheritance. A plan follows the columns of the rows it is applied to, so it is built once per resolver
private MergePlan plan;
private Map<ClassDescriptor, MergePlan> subclassPlans;
@@ -121,24 +121,18 @@ class ObjectResolver {
Persistent objectFromDataRow(DataRow row) {
// determine entity to use
ClassDescriptor classDescriptor = descriptorResolutionStrategy.descriptorForRow(row);
- MergePlan plan = planFor(row, classDescriptor);
+ MergePlan plan = planFor(classDescriptor);
ObjectId anId = createObjectId(row, classDescriptor, plan);
return objectFromDataRow(row, anId, classDescriptor, plan);
}
/**
- * Returns the plan of the columns of the given descriptor in rows of the given row's layout, or null if the row
- * has no layout, in which case the columns are looked up by name.
+ * Returns the plan of the columns of the given descriptor, built on the first call for the descriptor.
*/
- MergePlan planFor(DataRow row, ClassDescriptor classDescriptor) {
- RowLayout layout = row.getLayout();
- if (layout == null) {
- return null;
- }
-
+ MergePlan planFor(ClassDescriptor classDescriptor) {
if (classDescriptor == descriptor) {
- if (plan == null || plan.layout != layout) {
- plan = MergePlan.of(layout, classDescriptor);
+ if (plan == null) {
+ plan = MergePlan.of(classDescriptor);
}
return plan;
}
@@ -146,56 +140,50 @@ class ObjectResolver {
if (subclassPlans == null) {
subclassPlans = new HashMap<>();
}
- MergePlan subclassPlan = subclassPlans.get(classDescriptor);
- if (subclassPlan == null || subclassPlan.layout != layout) {
- subclassPlan = MergePlan.of(layout, classDescriptor);
- subclassPlans.put(classDescriptor, subclassPlan);
- }
- return subclassPlan;
+ return subclassPlans.computeIfAbsent(classDescriptor, MergePlan::of);
}
/**
* Returns the id of the row's object, or null if the row has a null PK, which is possible when processing left
* outer joint prefetches.
*
- * @param plan the plan of the descriptor's columns in the row, as returned by {@link #planFor(DataRow,
- * ClassDescriptor)}, or null to look the PK columns up by name
+ * @param plan the plan of the descriptor, as returned by {@link #planFor(ClassDescriptor)}
*/
ObjectId createObjectId(DataRow row, ClassDescriptor classDescriptor, MergePlan plan) {
- if (plan == null) {
- // not using DataRow.createObjectId for performance reasons -
- // ObjectResolver has all needed metadata already cached.
- return createObjectId(row, classDescriptor.getEntity(), CayennePath.EMPTY_PATH);
- }
-
String name = classDescriptor.getEntity().getName();
- int[] pkIndexes = plan.pkIndexes;
+ String[] pkNames = plan.pkNames;
+ int[] pkColumns = plan.pkColumns;
+ DataRowLayout.Reader reader = plan.reader;
- if (pkIndexes.length == 1) {
- if (pkIndexes[0] < 0) {
- throw new CayenneRuntimeException("No PK column '%s' found in data row.", plan.pkNames[0]);
+ // PK i is column i of the plan
+
+ if (pkNames.length == 1) {
+ Object val = reader.get(row, pkColumns[0]);
+ if (val == null) {
+ if (!reader.has(row, pkColumns[0])) {
+ throw new CayenneRuntimeException("No PK column '%s' found in data row.", pkNames[0]);
+ }
+ return null;
}
- Object val = row.valueAt(pkIndexes[0]);
- return val != null ? ObjectId.of(name, plan.pkNames[0], val) : null;
+ return ObjectId.of(name, pkNames[0], val);
}
- Map<String, Object> idMap = new HashMap<>(pkIndexes.length * 2);
- for (int i = 0; i < pkIndexes.length; i++) {
- if (pkIndexes[i] < 0) {
- throw new CayenneRuntimeException("No PK column '%s' found in data row.", plan.pkNames[i]);
- }
- Object val = row.valueAt(pkIndexes[i]);
+ Map<String, Object> idMap = new HashMap<>(pkNames.length * 2);
+ for (int i = 0; i < pkNames.length; i++) {
+ Object val = reader.get(row, pkColumns[i]);
if (val == null) {
+ if (!reader.has(row, pkColumns[i])) {
+ throw new CayenneRuntimeException("No PK column '%s' found in data row.", pkNames[i]);
+ }
return null;
}
- idMap.put(plan.pkNames[i], val);
+ idMap.put(pkNames[i], val);
}
return ObjectId.of(name, idMap);
}
/**
- * @param plan the plan of the descriptor's columns in the row, as returned by {@link #planFor(DataRow,
- * ClassDescriptor)}, or null to look the columns up by name
+ * @param plan the plan of the descriptor, as returned by {@link #planFor(ClassDescriptor)}
*/
Persistent objectFromDataRow(DataRow row, ObjectId anId, ClassDescriptor classDescriptor, MergePlan plan) {
@@ -251,7 +239,7 @@ class ObjectResolver {
case PersistenceState.DELETED:
// process the above only if refresh is requested...
if (refreshObjects && row.getVersion() >= object.getSnapshotVersion()) {
- DataRowUtils.mergeObjectWithSnapshot(context, classDescriptor, object, row, plan);
+ DataRowUtils.mergeObjectWithSnapshot(context, object, row, plan);
object.setSnapshotVersion(row.getVersion());
}
break;
@@ -260,11 +248,11 @@ class ObjectResolver {
if (!refreshObjects) {
DataRow cachedRow = cache.getCachedSnapshot(object.getObjectId());
if (cachedRow != null) {
- // the cached row may be of another layout, which the merge detects and looks the columns up by name
+ // the cached row may be of other columns, which the plan's columns resolve again
row = cachedRow;
}
}
- DataRowUtils.mergeObjectWithSnapshot(context, classDescriptor, object, row, plan);
+ DataRowUtils.mergeObjectWithSnapshot(context, object, row, plan);
object.setSnapshotVersion(row.getVersion());
break;
default:
diff --git a/cayenne/src/main/java/org/apache/cayenne/access/ObjectSnapshots.java b/cayenne/src/main/java/org/apache/cayenne/access/ObjectSnapshots.java
index bec44da55..38df06852 100644
--- a/cayenne/src/main/java/org/apache/cayenne/access/ObjectSnapshots.java
+++ b/cayenne/src/main/java/org/apache/cayenne/access/ObjectSnapshots.java
@@ -21,6 +21,7 @@ package org.apache.cayenne.access;
import org.apache.cayenne.CayenneRuntimeException;
import org.apache.cayenne.DataRow;
+import org.apache.cayenne.DataRowLayout;
import org.apache.cayenne.Fault;
import org.apache.cayenne.ObjectId;
import org.apache.cayenne.PersistenceState;
@@ -37,7 +38,8 @@ import org.apache.cayenne.reflect.ClassDescriptor;
import org.apache.cayenne.reflect.PropertyDescriptor;
import org.apache.cayenne.reflect.ToOneProperty;
-import java.util.HashMap;
+import java.util.ArrayList;
+import java.util.List;
import java.util.Map;
/**
@@ -71,7 +73,14 @@ public final class ObjectSnapshots {
private final DataContextObjectStore objectStore;
private final Persistent object;
private final long version;
- private final HashMap<String, Object> values = new HashMap<>(10);
+
+ private DataRowLayout layout;
+ private Object[] values;
+ private boolean[] present;
+ private int presentCount;
+
+ // the FK columns of the to-ones that are null, filled last
+ private List<String> nullFks;
Builder(EntityResolver resolver, DataContextObjectStore objectStore, Persistent object, long version) {
this.resolver = resolver;
@@ -89,6 +98,10 @@ public final class ObjectSnapshots {
ObjEntity entity = resolver.getObjEntity(object);
ClassDescriptor descriptor = resolver.getClassDescriptor(entity.getName());
+ this.layout = descriptor.getSnapshotLayout();
+ this.values = new Object[layout.size()];
+ this.present = new boolean[layout.size()];
+
// to-many relationships contribute nothing to the snapshot
for (PropertyDescriptor property : descriptor.getProperties()) {
switch (property) {
@@ -105,13 +118,8 @@ public final class ObjectSnapshots {
// set above when db relationships where processed.
Map<String, Object> thisIdParts = object.getObjectId().getIdSnapshot();
if (thisIdParts != null) {
-
- // put only those that do not exist in the map
for (Map.Entry<String, Object> entry : thisIdParts.entrySet()) {
- String nextKey = entry.getKey();
- if (!values.containsKey(nextKey)) {
- values.put(nextKey, entry.getValue());
- }
+ setIfAbsent(entry.getKey(), entry.getValue());
}
}
@@ -120,18 +128,70 @@ public final class ObjectSnapshots {
ObjectId flattenedId = objectStore.getFlattenedId(object.getObjectId(), path);
if (flattenedId != null) {
for (Map.Entry<String, Object> idEntry : flattenedId.getIdSnapshot().entrySet()) {
- values.putIfAbsent(path.dot(idEntry.getKey()).value(), idEntry.getValue());
+ setIfAbsent(path.dot(idEntry.getKey()).value(), idEntry.getValue());
}
}
}
- return DataRow.of(entity.getName(), values, version);
+ // a null to-one is a null FK, unless the column is also a part of the id, which was set above
+ if (nullFks != null) {
+ for (String name : nullFks) {
+ setIfAbsent(name, null);
+ }
+ }
+
+ return presentCount == values.length
+ ? DataRow.of(entity.getName(), layout, values, version)
+ : partialRow(entity.getName());
+ }
+
+ /**
+ * Returns a row of the columns that got a value, over a layout of its own: the snapshot is partial, see the
+ * warning in {@link ObjectSnapshots#snapshot(DataContext, Persistent, long)}.
+ */
+ private DataRow partialRow(String entityName) {
+ String[] names = new String[presentCount];
+ Object[] partial = new Object[presentCount];
+ int j = 0;
+ for (int i = 0; i < values.length; i++) {
+ if (present[i]) {
+ names[j] = layout.nameAt(i);
+ partial[j++] = values[i];
+ }
+ }
+ return DataRow.of(entityName, DataRowLayout.of(names), partial, version);
+ }
+
+ private void set(String name, Object value) {
+ int i = index(name);
+ values[i] = value;
+ if (!present[i]) {
+ present[i] = true;
+ presentCount++;
+ }
+ }
+
+ private void setIfAbsent(String name, Object value) {
+ int i = index(name);
+ if (!present[i]) {
+ values[i] = value;
+ present[i] = true;
+ presentCount++;
+ }
+ }
+
+ private int index(String name) {
+ int i = layout.indexOf(name);
+ if (i < 0) {
+ throw new CayenneRuntimeException("Column '%s' is not in the snapshot layout %s", name, layout);
+ }
+ return i;
}
private void addAttribute(AttributeProperty property) {
ObjAttribute objAttr = property.getAttribute();
// processing compound attributes correctly
- values.put(objAttr.getDbAttributePath().value(), property.readPropertyDirectly(object));
+ set(objAttr.getDbAttributePath().value(), property.readPropertyDirectly(object));
}
private void addToOne(ToOneProperty property) {
@@ -144,6 +204,12 @@ public final class ObjectSnapshots {
Object targetObject = property.readPropertyDirectly(object);
if (targetObject == null) {
+ if (nullFks == null) {
+ nullFks = new ArrayList<>();
+ }
+ for (DbJoin join : rel.getDbRelationships().getFirst().getJoins()) {
+ nullFks.add(join.getSourceName());
+ }
return;
}
@@ -158,7 +224,7 @@ public final class ObjectSnapshots {
DbRelationship dbRel = rel.getDbRelationships().getFirst();
for (DbJoin join : dbRel.getJoins()) {
String key = join.getSourceName();
- values.put(key, storedSnapshot.get(key));
+ set(key, storedSnapshot.get(key));
}
return;
@@ -175,7 +241,9 @@ public final class ObjectSnapshots {
DbRelationship dbRel = rel.getDbRelationships().getFirst();
Map<String, Object> fk = dbRel.srcFkSnapshotWithTargetSnapshot(idParts);
- values.putAll(fk);
+ for (Map.Entry<String, Object> entry : fk.entrySet()) {
+ set(entry.getKey(), entry.getValue());
+ }
}
}
}
diff --git a/cayenne/src/main/java/org/apache/cayenne/access/PrefetchProcessorJointNode.java b/cayenne/src/main/java/org/apache/cayenne/access/PrefetchProcessorJointNode.java
index e360da4d7..d1e2ac9bb 100644
--- a/cayenne/src/main/java/org/apache/cayenne/access/PrefetchProcessorJointNode.java
+++ b/cayenne/src/main/java/org/apache/cayenne/access/PrefetchProcessorJointNode.java
@@ -20,9 +20,9 @@
package org.apache.cayenne.access;
import org.apache.cayenne.CayenneRuntimeException;
+import org.apache.cayenne.DataRowLayout;
import org.apache.cayenne.DataRow;
import org.apache.cayenne.Persistent;
-import org.apache.cayenne.RowLayout;
import org.apache.cayenne.access.jdbc.RSColumn;
import org.apache.cayenne.exp.Expression;
import org.apache.cayenne.exp.ExpressionFactory;
@@ -61,12 +61,11 @@ class PrefetchProcessorJointNode extends PrefetchProcessorNode {
Map<Object, Persistent> resolved;
List<DataRow> resolvedRows;
- // the layout of this node's rows, keyed by the columns' result set names
- private RowLayout rowLayout;
+ // the layout of this node's rows, by the columns' result set names
+ private DataRowLayout rowLayout;
- // the positions of this node's columns in the flat rows, resolved for the layout of the last flat row seen
- private RowLayout flatLayout;
- private int[] flatIndexes;
+ // reads this node's columns from the flat rows
+ private DataRowLayout.Reader flatReader;
PrefetchProcessorJointNode(PrefetchProcessorNode parent, String segmentPath) {
super(parent, segmentPath);
@@ -112,44 +111,19 @@ class PrefetchProcessorJointNode extends PrefetchProcessorNode {
// likely be an indicator of an outer join ... and considering SQLTemplate,
// this is reasonable to expect...
- int[] indexes = flatRow.getLayout() != null ? flatIndexes(flatRow) : null;
if (idIndices.length == 1) {
- return idValue(flatRow, indexes, idIndices[0]);
+ return flatReader.get(flatRow, idIndices[0]);
}
Object[] id = new Object[idIndices.length];
for (int i = 0; i < id.length; i++) {
- id[i] = idValue(flatRow, indexes, idIndices[i]);
+ id[i] = flatReader.get(flatRow, idIndices[i]);
}
// Arrays.asList: a value-based key, unlike List.of it allows nulls, which an outer join produces
return Arrays.asList(id);
}
- private Object idValue(DataRow flatRow, int[] indexes, int idIndex) {
- return indexes != null ? valueAt(flatRow, indexes[idIndex]) : flatRow.get(columns[idIndex].dataRowName());
- }
-
- /**
- * Returns the positions of this node's columns in the given flat row, which has a layout.
- */
- private int[] flatIndexes(DataRow flatRow) {
- RowLayout layout = flatRow.getLayout();
- if (layout != flatLayout) {
- int[] indexes = new int[columns.length];
- for (int i = 0; i < columns.length; i++) {
- indexes[i] = layout.indexOf(columns[i].dataRowName());
- }
- flatIndexes = indexes;
- flatLayout = layout;
- }
- return flatIndexes;
- }
-
- private static Object valueAt(DataRow row, int index) {
- return index < 0 ? null : row.valueAt(index);
- }
-
/**
* Looks up a previously resolved object by the key returned by {@link #idFromFlatRow(DataRow)}. Returns null if
* no matching object exists.
@@ -173,15 +147,8 @@ class PrefetchProcessorJointNode extends PrefetchProcessorNode {
DataRow rowFromFlatRow(DataRow flatRow) {
// extract subset of flat row columns, recasting to the target keys
Object[] values = new Object[columns.length];
- if (flatRow.getLayout() != null) {
- int[] indexes = flatIndexes(flatRow);
- for (int i = 0; i < columns.length; i++) {
- values[i] = valueAt(flatRow, indexes[i]);
- }
- } else {
- for (int i = 0; i < columns.length; i++) {
- values[i] = flatRow.get(columns[i].dataRowName());
- }
+ for (int i = 0; i < columns.length; i++) {
+ values[i] = flatReader.get(flatRow, i);
}
DataRow row = DataRow.of(resolver.getEntity().getName(), rowLayout, values, flatRow.getVersion());
@@ -284,10 +251,13 @@ class PrefetchProcessorJointNode extends PrefetchProcessorNode {
targetSource.values().toArray(columns);
String[] names = new String[size];
+ String[] flatNames = new String[size];
for (int i = 0; i < size; i++) {
names[i] = columns[i].rsName();
+ flatNames[i] = columns[i].dataRowName();
}
- this.rowLayout = new RowLayout(names);
+ this.rowLayout = DataRowLayout.of(names);
+ this.flatReader = DataRowLayout.of(flatNames).reader();
}
private RSColumn appendColumn(
diff --git a/cayenne/src/main/java/org/apache/cayenne/access/ResultScanParentAttachmentStrategy.java b/cayenne/src/main/java/org/apache/cayenne/access/ResultScanParentAttachmentStrategy.java
index 0dab73ce9..2affe238f 100644
--- a/cayenne/src/main/java/org/apache/cayenne/access/ResultScanParentAttachmentStrategy.java
+++ b/cayenne/src/main/java/org/apache/cayenne/access/ResultScanParentAttachmentStrategy.java
@@ -18,6 +18,7 @@
****************************************************************/
package org.apache.cayenne.access;
+import org.apache.cayenne.DataRowLayout;
import org.apache.cayenne.DataRow;
import org.apache.cayenne.Persistent;
import org.apache.cayenne.map.DbJoin;
@@ -39,8 +40,8 @@ class ResultScanParentAttachmentStrategy implements ParentAttachmentStrategy {
private final PrefetchProcessorNode node;
// the join columns in the child rows and in the parent rows
- private final RowColumns targetColumns;
- private final RowColumns sourceColumns;
+ private final DataRowLayout.Reader targetReader;
+ private final DataRowLayout.Reader sourceReader;
// TODO: the ivar below makes this strategy STATEFUL and non-reusable. If we need a
// stateless version down the line, will need to move this to the
@@ -73,8 +74,8 @@ class ResultScanParentAttachmentStrategy implements ParentAttachmentStrategy {
targetNames[i] = joins[i].getTargetName();
sourceNames[i] = joins[i].getSourceName();
}
- targetColumns = new RowColumns(targetNames);
- sourceColumns = new RowColumns(sourceNames);
+ targetReader = DataRowLayout.of(targetNames).reader();
+ sourceReader = DataRowLayout.of(sourceNames).reader();
}
public void linkToParent(DataRow row, Persistent object) {
@@ -87,11 +88,11 @@ class ResultScanParentAttachmentStrategy implements ParentAttachmentStrategy {
if (joins.length > 1) {
List<Object> values = new ArrayList<>(joins.length);
for (int i = 0; i < joins.length; i++) {
- values.add(targetColumns.get(row, i));
+ values.add(targetReader.get(row, i));
}
key = values;
} else {
- key = targetColumns.get(row, 0);
+ key = targetReader.get(row, 0);
}
List<Persistent> parents = partitionByChild.get(key);
@@ -133,11 +134,11 @@ class ResultScanParentAttachmentStrategy implements ParentAttachmentStrategy {
if (joins.length > 1) {
List<Object> values = new ArrayList<>(joins.length);
for (int j = 0; j < joins.length; j++) {
- values.add(sourceColumns.get(row, j));
+ values.add(sourceReader.get(row, j));
}
key = values;
} else {
- key = sourceColumns.get(row, 0);
+ key = sourceReader.get(row, 0);
}
List<Persistent> parents = partitionByChild.computeIfAbsent(key, k -> new ArrayList<>());
diff --git a/cayenne/src/main/java/org/apache/cayenne/access/RowColumns.java b/cayenne/src/main/java/org/apache/cayenne/access/RowColumns.java
deleted file mode 100644
index bc183682e..000000000
--- a/cayenne/src/main/java/org/apache/cayenne/access/RowColumns.java
+++ /dev/null
@@ -1,61 +0,0 @@
-/*****************************************************************
- * 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
- *
- * https://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;
-
-import org.apache.cayenne.DataRow;
-import org.apache.cayenne.RowLayout;
-
-/**
- * Reads a fixed set of named columns from rows, by position when a row has a {@link RowLayout}. The positions are
- * resolved when a row of a new layout comes along, so a loop over the rows of one result set looks the names up
- * once. Not thread-safe.
- */
-final class RowColumns {
-
- private final String[] names;
- private RowLayout layout;
- private int[] indexes;
-
- RowColumns(String[] names) {
- this.names = names;
- }
-
- /**
- * Returns the value of the i-th column of the row, or null if the row has no such column.
- */
- Object get(DataRow row, int i) {
- RowLayout rowLayout = row.getLayout();
- if (rowLayout == null) {
- return row.get(names[i]);
- }
-
- if (rowLayout != layout) {
- int[] resolved = new int[names.length];
- for (int j = 0; j < names.length; j++) {
- resolved[j] = rowLayout.indexOf(names[j]);
- }
- indexes = resolved;
- layout = rowLayout;
- }
-
- int index = indexes[i];
- return index < 0 ? null : row.valueAt(index);
- }
-}
diff --git a/cayenne/src/main/java/org/apache/cayenne/access/event/SnapshotEvent.java b/cayenne/src/main/java/org/apache/cayenne/access/event/SnapshotEvent.java
index 8aafbf928..785e818df 100644
--- a/cayenne/src/main/java/org/apache/cayenne/access/event/SnapshotEvent.java
+++ b/cayenne/src/main/java/org/apache/cayenne/access/event/SnapshotEvent.java
@@ -23,8 +23,8 @@ import java.util.Collection;
import java.util.Collections;
import java.util.Map;
-import org.apache.cayenne.DataRow;
import org.apache.cayenne.ObjectId;
+import org.apache.cayenne.access.SnapshotVersion;
import org.apache.cayenne.event.CayenneEvent;
/**
@@ -35,18 +35,26 @@ import org.apache.cayenne.event.CayenneEvent;
public class SnapshotEvent extends CayenneEvent {
protected long timestamp;
+ protected long version;
protected Collection<ObjectId> deletedIds;
protected Collection<ObjectId> invalidatedIds;
- protected Map<ObjectId, DataRow> modifiedDiffs;
+ protected Map<ObjectId, Map<String, Object>> modifiedDiffs;
protected Collection<ObjectId> indirectlyModifiedIds;
- public SnapshotEvent(Object source, Object postedBy, Map<ObjectId, DataRow> modifiedDiffs,
+ /**
+ * @param version the {@link SnapshotVersion snapshot version} of the modified snapshots, which are of one
+ * operation, or {@link SnapshotVersion#NONE} if none were modified
+ * @param modifiedDiffs the changed keys of each modified snapshot, with their new values
+ * @since 5.0
+ */
+ public SnapshotEvent(Object source, Object postedBy, long version, Map<ObjectId, Map<String, Object>> modifiedDiffs,
Collection<ObjectId> deletedIds, Collection<ObjectId> invalidatedIds,
Collection<ObjectId> indirectlyModifiedIds) {
super(source, postedBy);
this.timestamp = System.currentTimeMillis();
+ this.version = version;
this.modifiedDiffs = modifiedDiffs;
this.deletedIds = deletedIds;
this.invalidatedIds = invalidatedIds;
@@ -57,8 +65,21 @@ public class SnapshotEvent extends CayenneEvent {
return timestamp;
}
- public Map<ObjectId, DataRow> getModifiedDiffs() {
- return (modifiedDiffs != null) ? modifiedDiffs : Collections.<ObjectId, DataRow>emptyMap();
+ /**
+ * Returns the {@link SnapshotVersion snapshot version} of the modified snapshots, or {@link SnapshotVersion#NONE}
+ * if none were modified.
+ *
+ * @since 5.0
+ */
+ public long getVersion() {
+ return version;
+ }
+
+ /**
+ * Returns the changed keys of each modified snapshot, with their new values.
+ */
+ public Map<ObjectId, Map<String, Object>> getModifiedDiffs() {
+ return (modifiedDiffs != null) ? modifiedDiffs : Collections.emptyMap();
}
public Collection<ObjectId> getDeletedIds() {
@@ -80,7 +101,7 @@ public class SnapshotEvent extends CayenneEvent {
StringBuilder buffer = new StringBuilder();
buffer.append("[SnapshotEvent] source: ").append(getSource());
- Map<ObjectId, DataRow> modified = getModifiedDiffs();
+ Map<ObjectId, Map<String, Object>> modified = getModifiedDiffs();
if (!modified.isEmpty()) {
buffer.append(", modified ").append(modified.size()).append(" id(s)");
}
diff --git a/cayenne/src/main/java/org/apache/cayenne/access/jdbc/reader/FullRowReader.java b/cayenne/src/main/java/org/apache/cayenne/access/jdbc/reader/FullRowReader.java
index e57addf7e..86ecc51a0 100644
--- a/cayenne/src/main/java/org/apache/cayenne/access/jdbc/reader/FullRowReader.java
+++ b/cayenne/src/main/java/org/apache/cayenne/access/jdbc/reader/FullRowReader.java
@@ -21,7 +21,7 @@ package org.apache.cayenne.access.jdbc.reader;
import org.apache.cayenne.CayenneRuntimeException;
import org.apache.cayenne.DataRow;
-import org.apache.cayenne.RowLayout;
+import org.apache.cayenne.DataRowLayout;
import org.apache.cayenne.access.jdbc.RSColumn;
import org.apache.cayenne.map.EntityInheritanceTree;
import org.apache.cayenne.map.ObjEntity;
@@ -37,7 +37,7 @@ import java.util.Objects;
class FullRowReader implements RowReader<DataRow> {
private final RSColumn[] columns;
- private final RowLayout layout;
+ private final DataRowLayout layout;
private final long version;
protected final String entityName;
@@ -56,14 +56,14 @@ class FullRowReader implements RowReader<DataRow> {
}
/**
- * Returns a layout of the columns' DataRow names, in column order.
+ * Returns the layout of the columns' DataRow names, in column order.
*/
- static RowLayout layout(RSColumn[] columns) {
- String[] names = new String[columns.length];
- for (int i = 0; i < columns.length; i++) {
- names[i] = columns[i].dataRowName();
+ static DataRowLayout layout(RSColumn[] rsColumns) {
+ String[] names = new String[rsColumns.length];
+ for (int i = 0; i < rsColumns.length; i++) {
+ names[i] = rsColumns[i].dataRowName();
}
- return new RowLayout(names);
+ return DataRowLayout.of(names);
}
private FullRowReader(RSColumn[] columns, String entityName, long version) {
diff --git a/cayenne/src/main/java/org/apache/cayenne/access/jdbc/reader/IndexRowReader.java b/cayenne/src/main/java/org/apache/cayenne/access/jdbc/reader/IndexRowReader.java
index 4f2ffa0b2..01300bf89 100644
--- a/cayenne/src/main/java/org/apache/cayenne/access/jdbc/reader/IndexRowReader.java
+++ b/cayenne/src/main/java/org/apache/cayenne/access/jdbc/reader/IndexRowReader.java
@@ -21,7 +21,7 @@ package org.apache.cayenne.access.jdbc.reader;
import org.apache.cayenne.CayenneRuntimeException;
import org.apache.cayenne.DataRow;
-import org.apache.cayenne.RowLayout;
+import org.apache.cayenne.DataRowLayout;
import org.apache.cayenne.access.jdbc.RSColumn;
import java.sql.ResultSet;
@@ -34,7 +34,7 @@ import java.sql.ResultSet;
class IndexRowReader implements RowReader<DataRow> {
private final RSColumn[] columns;
- private final RowLayout layout;
+ private final DataRowLayout layout;
private final int[] jdbcIndexes;
private final String entityName;
private final long version;
diff --git a/cayenne/src/main/java/org/apache/cayenne/access/jdbc/reader/OffsetRowReader.java b/cayenne/src/main/java/org/apache/cayenne/access/jdbc/reader/OffsetRowReader.java
index 8bf79c569..dab2d2266 100644
--- a/cayenne/src/main/java/org/apache/cayenne/access/jdbc/reader/OffsetRowReader.java
+++ b/cayenne/src/main/java/org/apache/cayenne/access/jdbc/reader/OffsetRowReader.java
@@ -21,7 +21,7 @@ package org.apache.cayenne.access.jdbc.reader;
import org.apache.cayenne.CayenneRuntimeException;
import org.apache.cayenne.DataRow;
-import org.apache.cayenne.RowLayout;
+import org.apache.cayenne.DataRowLayout;
import org.apache.cayenne.access.jdbc.RSColumn;
import org.apache.cayenne.map.EntityInheritanceTree;
import org.apache.cayenne.reflect.ClassDescriptor;
@@ -36,7 +36,7 @@ class OffsetRowReader implements RowReader<DataRow> {
protected final String entityName;
private final long version;
private final RSColumn[] columns;
- private final RowLayout layout;
+ private final DataRowLayout layout;
private final int offset;
// possible inheritance
diff --git a/cayenne/src/main/java/org/apache/cayenne/reflect/ClassDescriptor.java b/cayenne/src/main/java/org/apache/cayenne/reflect/ClassDescriptor.java
index 9a7fcac1d..317f0dbe8 100644
--- a/cayenne/src/main/java/org/apache/cayenne/reflect/ClassDescriptor.java
+++ b/cayenne/src/main/java/org/apache/cayenne/reflect/ClassDescriptor.java
@@ -22,6 +22,7 @@ package org.apache.cayenne.reflect;
import java.util.Collection;
import java.util.Map;
+import org.apache.cayenne.DataRowLayout;
import org.apache.cayenne.exp.Expression;
import org.apache.cayenne.exp.path.CayennePath;
import org.apache.cayenne.map.DbEntity;
@@ -215,6 +216,16 @@ public interface ClassDescriptor {
*/
Collection<PropertyDescriptor> getAllProperties();
+ /**
+ * Returns the layout of a snapshot of an object of the described class: the columns of the attributes, the
+ * foreign keys of the to-one relationships whose targets propagate their keys to this entity, the primary key,
+ * and the primary keys of the additional DB entities, each column listed once. Snapshots of objects are built
+ * over this layout, and rows of any layout are merged into objects through it.
+ *
+ * @since 5.0
+ */
+ DataRowLayout getSnapshotLayout();
+
/**
* Passes the visitor to the properties "visit" method for all properties declared in
* this descriptor and all its super-decsriptors. Properties that are overridden in
diff --git a/cayenne/src/main/java/org/apache/cayenne/reflect/LazyClassDescriptorDecorator.java b/cayenne/src/main/java/org/apache/cayenne/reflect/LazyClassDescriptorDecorator.java
index 1121875e8..95f6e5c16 100644
--- a/cayenne/src/main/java/org/apache/cayenne/reflect/LazyClassDescriptorDecorator.java
+++ b/cayenne/src/main/java/org/apache/cayenne/reflect/LazyClassDescriptorDecorator.java
@@ -21,6 +21,7 @@ package org.apache.cayenne.reflect;
import java.util.Collection;
import java.util.Map;
+import org.apache.cayenne.DataRowLayout;
import org.apache.cayenne.exp.Expression;
import org.apache.cayenne.exp.path.CayennePath;
import org.apache.cayenne.map.DbEntity;
@@ -195,4 +196,10 @@ public class LazyClassDescriptorDecorator implements ClassDescriptor {
checkDescriptorInitialized();
return descriptor.getAllProperties();
}
+
+ @Override
+ public DataRowLayout getSnapshotLayout() {
+ checkDescriptorInitialized();
+ return descriptor.getSnapshotLayout();
+ }
}
diff --git a/cayenne/src/main/java/org/apache/cayenne/reflect/PersistentDescriptor.java b/cayenne/src/main/java/org/apache/cayenne/reflect/PersistentDescriptor.java
index db2195c2b..f1f003b3b 100644
--- a/cayenne/src/main/java/org/apache/cayenne/reflect/PersistentDescriptor.java
+++ b/cayenne/src/main/java/org/apache/cayenne/reflect/PersistentDescriptor.java
@@ -18,11 +18,14 @@
****************************************************************/
package org.apache.cayenne.reflect;
+import org.apache.cayenne.DataRowLayout;
import org.apache.cayenne.PersistenceState;
import org.apache.cayenne.Persistent;
import org.apache.cayenne.exp.Expression;
import org.apache.cayenne.exp.path.CayennePath;
+import org.apache.cayenne.map.DbAttribute;
import org.apache.cayenne.map.DbEntity;
+import org.apache.cayenne.map.DbJoin;
import org.apache.cayenne.map.EntityInheritanceTree;
import org.apache.cayenne.map.ObjAttribute;
import org.apache.cayenne.map.ObjEntity;
@@ -34,9 +37,11 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
+import java.util.Set;
/**
* A default ClassDescriptor implementation for persistent objects.
@@ -74,6 +79,9 @@ public class PersistentDescriptor implements ClassDescriptor {
// the values that this entity's own qualifier pins its attributes to, compiled at creation
protected Map<AttributeProperty, Object> discriminatorValues;
+ // built on first use. The layout is immutable and the build deterministic, so a racing rebuild is harmless
+ private DataRowLayout snapshotLayout;
+
/**
* Creates a PersistentDescriptor.
*/
@@ -406,6 +414,53 @@ public class PersistentDescriptor implements ClassDescriptor {
return all;
}
+ @Override
+ public DataRowLayout getSnapshotLayout() {
+ DataRowLayout layout = snapshotLayout;
+ if (layout == null) {
+ layout = buildSnapshotLayout();
+ snapshotLayout = layout;
+ }
+ return layout;
+ }
+
+ private DataRowLayout buildSnapshotLayout() {
+ // a column is listed once however many roles it has: a PK column is often an attribute or an FK as well
+ Set<String> names = new LinkedHashSet<>();
+
+ for (PropertyDescriptor property : getProperties()) {
+ switch (property) {
+ case AttributeProperty attribute -> names.add(attribute.getAttribute().getDbAttributePath().value());
+ case ToOneProperty toOne -> {
+ // a to-one whose target does not propagate its key to this entity has no FK in the snapshot
+ ObjRelationship relationship = toOne.getRelationship();
+ if (!relationship.isSourceIndependentFromTargetChange()) {
+ for (DbJoin join : relationship.getDbRelationships().getFirst().getJoins()) {
+ names.add(join.getSourceName());
+ }
+ }
+ }
+ default -> {
+ }
+ }
+ }
+
+ DbEntity dbEntity = entity.getDbEntity();
+ if (dbEntity != null) {
+ for (DbAttribute pk : dbEntity.getPrimaryKeys()) {
+ names.add(pk.getName());
+ }
+ }
+
+ for (AdditionalDbEntityDescriptor additional : getAdditionalDbEntities().values()) {
+ for (DbAttribute pk : additional.getDbEntity().getPrimaryKeys()) {
+ names.add(additional.getPath().dot(pk.getName()).value());
+ }
+ }
+
+ return DataRowLayout.of(names.toArray(new String[0]));
+ }
+
public void setEntity(ObjEntity entity) {
this.entity = entity;
}
diff --git a/cayenne/src/test/java/org/apache/cayenne/DataRowLayoutTest.java b/cayenne/src/test/java/org/apache/cayenne/DataRowLayoutTest.java
new file mode 100644
index 000000000..0f9034e8f
--- /dev/null
+++ b/cayenne/src/test/java/org/apache/cayenne/DataRowLayoutTest.java
@@ -0,0 +1,146 @@
+/*****************************************************************
+ * 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
+ *
+ * https://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;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class DataRowLayoutTest {
+
+ private static final DataRowLayout ABC = DataRowLayout.of("a", "b", "c");
+ private static final DataRowLayout CBA = DataRowLayout.of("c", "b", "a");
+
+ @Test
+ public void indexOf() {
+ DataRowLayout layout = DataRowLayout.of("a", "b", "c");
+ assertEquals(3, layout.size());
+ assertEquals(0, layout.indexOf("a"));
+ assertEquals(2, layout.indexOf("c"));
+ assertEquals(-1, layout.indexOf("d"));
+ assertEquals("b", layout.nameAt(1));
+ }
+
+ @Test
+ public void duplicates_LastWins() {
+ String[] names = {"a", "b", "a", "a"};
+ DataRowLayout layout = DataRowLayout.of(names);
+
+ assertEquals(4, layout.size());
+ assertEquals(3, layout.indexOf("a"));
+ assertEquals(2, layout.indexOf("a_"));
+ assertEquals(0, layout.indexOf("a__"));
+ assertEquals(1, layout.indexOf("b"));
+
+ assertArrayEquals(new String[]{"a", "b", "a", "a"}, names, "the argument must not be changed");
+ }
+
+ @Test
+ public void duplicates_ExistingSuffixedName() {
+ DataRowLayout layout = DataRowLayout.of("a", "a_", "a");
+ assertEquals(2, layout.indexOf("a"));
+ assertEquals(1, layout.indexOf("a_"));
+ assertEquals(0, layout.indexOf("a__"));
+ }
+
+ @Test
+ public void reader_OwnLayout() {
+ DataRowLayout.Reader reader = ABC.reader();
+ DataRow row = DataRow.of("E", ABC, new Object[]{1, null, "z"}, 1);
+
+ assertEquals(1, reader.get(row, 0));
+ assertNull(reader.get(row, 1));
+ assertEquals("z", reader.get(row, 2));
+ assertTrue(reader.has(row, 1), "a null value is not a missing column");
+ }
+
+ @Test
+ public void reader_OtherLayout() {
+ DataRowLayout.Reader reader = DataRowLayout.of("c", "a", "x").reader();
+ DataRow row = DataRow.of("E", ABC, new Object[]{1, null, "z"}, 1);
+
+ assertEquals("z", reader.get(row, 0));
+ assertEquals(1, reader.get(row, 1));
+ assertNull(reader.get(row, 2), "a column the row does not have");
+ assertTrue(reader.has(row, 0));
+ assertFalse(reader.has(row, 2));
+ }
+
+ @Test
+ public void reader_NullValue() {
+ DataRowLayout.Reader reader = DataRowLayout.of("b").reader();
+ DataRow row = DataRow.of("E", ABC, new Object[]{1, null, "z"}, 1);
+
+ assertNull(reader.get(row, 0));
+ assertTrue(reader.has(row, 0), "a null value is not a missing column");
+ }
+
+ @Test
+ public void reader_NullAndMissingColumns() {
+ DataRowLayout.Reader reader = DataRowLayout.of("c", "a", "x", "n").reader();
+ DataRow row = DataRow.of("E", DataRowLayout.of("a", "c", "n"), new Object[]{1, "z", null}, 1);
+
+ assertEquals("z", reader.get(row, 0));
+ assertEquals(1, reader.get(row, 1));
+ assertNull(reader.get(row, 2));
+ assertFalse(reader.has(row, 2));
+ assertNull(reader.get(row, 3));
+ assertTrue(reader.has(row, 3));
+ }
+
+ @Test
+ public void reader_RowsOfSeveralLayouts() {
+ DataRowLayout.Reader reader = DataRowLayout.of("a", "c").reader();
+ DataRow abc = DataRow.of("E", ABC, new Object[]{1, 2, 3}, 1);
+ DataRow cba = DataRow.of("E", CBA, new Object[]{1, 2, 3}, 1);
+ DataRow ac = DataRow.of("E", DataRowLayout.of("a", "c"), new Object[]{10, 30}, 1);
+
+ // the positions are resolved per layout, whatever the order the rows come in
+ for (int i = 0; i < 3; i++) {
+ assertEquals(1, reader.get(abc, 0));
+ assertEquals(3, reader.get(abc, 1));
+ assertEquals(3, reader.get(cba, 0));
+ assertEquals(1, reader.get(cba, 1));
+ assertEquals(10, reader.get(ac, 0));
+ assertEquals(30, reader.get(ac, 1));
+ }
+ }
+
+ @Test
+ public void reader_MergedRow() {
+ DataRowLayout.Reader reader = DataRowLayout.of("b", "d").reader();
+ DataRow row = DataRow.of("E", ABC, new Object[]{1, 2, 3}, 1);
+
+ DataRow sameLayout = row.merged(Map.of("b", 20), 2);
+ assertEquals(20, reader.get(sameLayout, 0));
+ assertFalse(reader.has(sameLayout, 1));
+
+ DataRow widened = row.merged(Map.of("d", 4), 3);
+ assertEquals(2, reader.get(widened, 0));
+ assertEquals(4, reader.get(widened, 1));
+ assertTrue(reader.has(widened, 1));
+ }
+}
diff --git a/cayenne/src/test/java/org/apache/cayenne/DataRowTest.java b/cayenne/src/test/java/org/apache/cayenne/DataRowTest.java
index f6a55ac96..6b8afc58b 100644
--- a/cayenne/src/test/java/org/apache/cayenne/DataRowTest.java
+++ b/cayenne/src/test/java/org/apache/cayenne/DataRowTest.java
@@ -41,17 +41,21 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
public class DataRowTest {
- private static final RowLayout LAYOUT = new RowLayout(new String[]{"a", "b", "c"});
+ private static final DataRowLayout ABC = DataRowLayout.of("a", "b", "c");
- private static DataRow arrayRow(Object... values) {
- return DataRow.of("E", LAYOUT, values, SnapshotVersion.next());
+ private static DataRow rowOf(Object... values) {
+ return DataRow.of("E", ABC, values, SnapshotVersion.next());
+ }
+
+ private static DataRow rowOf(DataRowLayout layout, Object... values) {
+ return DataRow.of("E", layout, values, 1);
}
@Test
- public void version() throws Exception {
- DataRow s1 = DataRow.of("E", new HashMap<>(10), SnapshotVersion.next());
- DataRow s2 = DataRow.of("E", new HashMap<>(10), SnapshotVersion.next());
- DataRow s3 = DataRow.of("E", new HashMap<>(10), SnapshotVersion.next());
+ public void version() {
+ DataRow s1 = rowOf(1, 2, 3);
+ DataRow s2 = rowOf(1, 2, 3);
+ DataRow s3 = rowOf(1, 2, 3);
assertFalse(s1.getVersion() == s2.getVersion());
assertFalse(s2.getVersion() == s3.getVersion());
@@ -61,8 +65,8 @@ public class DataRowTest {
@Test
public void explicitVersion() {
long version = SnapshotVersion.next();
- DataRow s1 = DataRow.of("E", Map.of("a", 1), version);
- DataRow s2 = DataRow.of("E2", new HashMap<>(s1), version);
+ DataRow s1 = DataRow.of("E", ABC, new Object[]{1, 2, 3}, version);
+ DataRow s2 = DataRow.of("E2", ABC, new Object[]{1, 2, 3}, version);
assertEquals(version, s1.getVersion());
assertEquals(version, s2.getVersion());
@@ -73,26 +77,22 @@ public class DataRowTest {
@Test
public void immutable() {
- DataRow map = DataRow.of("E", new HashMap<>(Map.of("a", 1)), 1);
- DataRow array = arrayRow(1, 2, 3);
-
- for (DataRow row : List.of(map, array)) {
- assertThrows(UnsupportedOperationException.class, () -> row.put("a", 2));
- assertThrows(UnsupportedOperationException.class, () -> row.putAll(Map.of("a", 2)));
- assertThrows(UnsupportedOperationException.class, () -> row.remove("a"));
- assertThrows(UnsupportedOperationException.class, row::clear);
- }
+ DataRow row = rowOf(1, 2, 3);
- // the views of an array row are read-only. The views of a map row are those of the map, not wrapped
- assertThrows(UnsupportedOperationException.class, () -> array.keySet().remove("a"));
- assertThrows(UnsupportedOperationException.class, () -> array.entrySet().iterator().next().setValue(5));
+ assertThrows(UnsupportedOperationException.class, () -> row.put("a", 2));
+ assertThrows(UnsupportedOperationException.class, () -> row.putAll(Map.of("a", 2)));
+ assertThrows(UnsupportedOperationException.class, () -> row.remove("a"));
+ assertThrows(UnsupportedOperationException.class, row::clear);
+
+ // the views are read-only too
+ assertThrows(UnsupportedOperationException.class, () -> row.keySet().remove("a"));
+ assertThrows(UnsupportedOperationException.class, () -> row.entrySet().iterator().next().setValue(5));
}
@Test
- public void arrayRow_MapView() {
- DataRow row = arrayRow(1, null, "x");
+ public void mapView() {
+ DataRow row = rowOf(1, null, "x");
- assertSame(LAYOUT, row.getLayout());
assertEquals(3, row.size());
assertFalse(row.isEmpty());
assertEquals(1, row.get("a"));
@@ -104,7 +104,6 @@ public class DataRowTest {
assertTrue(row.containsValue(null));
assertTrue(row.containsValue("x"));
assertFalse(row.containsValue("y"));
- assertEquals("x", row.valueAt(2));
assertEquals(7, row.getOrDefault("d", 7));
assertNull(row.getOrDefault("b", 7));
@@ -118,100 +117,91 @@ public class DataRowTest {
assertTrue(copy.containsKey("b"));
}
- @Test
- public void mapRow_NoLayout() {
- DataRow row = DataRow.of("E", Map.of("a", 1), 1);
- assertNull(row.getLayout());
- assertThrows(UnsupportedOperationException.class, () -> row.valueAt(0));
- }
-
@Test
public void equalsAndHashCode() {
- DataRow array = arrayRow(1, null, "x");
- DataRow sameLayout = arrayRow(1, null, "x");
- DataRow otherLayout = DataRow.of("E", new RowLayout(new String[]{"c", "a", "b"}), new Object[]{"x", 1, null}, 1);
- DataRow map = DataRow.of("E", new HashMap<>(Map.of("a", 1, "c", "x")), 1);
+ DataRow row = rowOf(1, null, "x");
+ DataRow sameLayout = rowOf(1, null, "x");
+ DataRow otherLayout = rowOf(DataRowLayout.of("c", "a", "b"), "x", 1, null);
+ DataRow narrower = rowOf(DataRowLayout.of("a", "c"), 1, "x");
HashMap<String, Object> plainMap = new HashMap<>(Map.of("a", 1, "c", "x"));
plainMap.put("b", null);
- assertEquals(array, sameLayout);
- assertEquals(array, otherLayout);
- assertEquals(otherLayout, array);
- assertEquals(array, plainMap);
- assertEquals(plainMap, array);
- assertEquals(array.hashCode(), plainMap.hashCode());
- assertEquals(array.hashCode(), otherLayout.hashCode());
-
- // "b" is absent in the map row, null in the array row
- assertNotEquals(array, map);
- assertNotEquals(map, array);
-
- assertNotEquals(array, arrayRow(1, null, "y"));
- assertNotEquals(array, arrayRow(1, 2, "x"));
- assertNotEquals(array, DataRow.of("E", Map.of("a", 1), 1));
+ assertEquals(row, sameLayout);
+ assertEquals(row, otherLayout);
+ assertEquals(otherLayout, row);
+ assertEquals(row, plainMap);
+ assertEquals(plainMap, row);
+ assertEquals(row.hashCode(), plainMap.hashCode());
+ assertEquals(row.hashCode(), otherLayout.hashCode());
+
+ // "b" is absent in the narrower row, null in the other
+ assertNotEquals(row, narrower);
+ assertNotEquals(narrower, row);
+
+ assertNotEquals(row, rowOf(1, null, "y"));
+ assertNotEquals(row, rowOf(1, 2, "x"));
+ assertNotEquals(row, rowOf(DataRowLayout.of("a"), 1));
}
@Test
public void merged() {
- DataRow array = arrayRow(1, 2, 3);
- DataRow merged = array.merged(Map.of("b", 20), 7);
+ DataRow row = rowOf(1, 2, 3);
+ DataRow merged = row.merged(Map.of("b", 20), 7);
- assertSame(LAYOUT, merged.getLayout());
assertEquals(7, merged.getVersion());
assertEquals("E", merged.getEntityName());
- assertEquals(20, merged.valueAt(1));
- assertEquals(1, merged.valueAt(0));
- assertEquals(2, array.valueAt(1), "the original must not change");
+ assertEquals(Map.of("a", 1, "b", 20, "c", 3), merged);
+ assertSame(row.layout, merged.layout);
+ assertEquals(2, row.get("b"), "the original must not change");
- DataRow widened = array.merged(Map.of("d", 4), 8);
- assertNull(widened.getLayout());
+ DataRow widened = row.merged(Map.of("d", 4), 8);
assertEquals(4, widened.size());
assertEquals(4, widened.get("d"));
assertEquals(3, widened.get("c"));
-
- DataRow map = DataRow.of("E", new HashMap<>(Map.of("a", 1)), 1).merged(Map.of("b", 2), 9);
- assertNull(map.getLayout());
- assertEquals(9, map.getVersion());
- assertEquals(Map.of("a", 1, "b", 2), map);
+ assertEquals(List.of("a", "b", "c", "d"), List.copyOf(widened.keySet()));
}
@Test
public void withEntityName() {
- DataRow array = arrayRow(1, 2, 3);
- DataRow renamed = array.withEntityName("E2");
+ DataRow row = rowOf(1, 2, 3);
+ DataRow renamed = row.withEntityName("E2");
assertEquals("E2", renamed.getEntityName());
- assertEquals(array.getVersion(), renamed.getVersion());
- assertSame(LAYOUT, renamed.getLayout());
- assertEquals(array, renamed);
-
- DataRow map = DataRow.of("E", Map.of("a", 1), 1).withEntityName("E2");
- assertEquals("E2", map.getEntityName());
- assertEquals(1, map.get("a"));
+ assertEquals(row.getVersion(), renamed.getVersion());
+ assertEquals(row, renamed);
+ assertSame(row.layout, renamed.layout);
}
@Test
public void serialization() throws Exception {
- DataRow array = arrayRow(1, null, "x");
+ DataRow row1 = rowOf(1, null, "x");
+ DataRow row2 = rowOf(2, null, "y");
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (ObjectOutputStream out = new ObjectOutputStream(bytes)) {
- out.writeObject(array);
+ out.writeObject(List.of(row1, row2));
}
- DataRow read;
+ List<?> read;
try (ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bytes.toByteArray()))) {
- read = (DataRow) in.readObject();
+ read = (List<?>) in.readObject();
}
- assertNotNull(read);
- assertNull(read.getLayout(), "the layout is not serialized");
- assertEquals(array, read);
- assertEquals(array.getVersion(), read.getVersion());
- assertEquals("E", read.getEntityName());
+ DataRow read1 = (DataRow) read.get(0);
+ DataRow read2 = (DataRow) read.get(1);
+ assertEquals(row1, read1);
+ assertEquals(row2, read2);
+ assertEquals(row1.getVersion(), read1.getVersion());
+ assertEquals("E", read1.getEntityName());
+
+ // the layout is read back once, with its index rebuilt
+ assertSame(read1.layout, read2.layout);
+ assertEquals(List.of("a", "b", "c"), List.copyOf(read1.keySet()));
+ assertEquals("x", read1.get("c"));
+ assertEquals("y", read1.layout.reader().get(read2, 2));
}
@Test
- public void arrayRow_LengthMismatch() {
- assertThrows(IllegalArgumentException.class, () -> DataRow.of("E", LAYOUT, new Object[]{1}, 1));
+ public void lengthMismatch() {
+ assertThrows(IllegalArgumentException.class, () -> DataRow.of("E", ABC, new Object[]{1}, 1));
}
}
diff --git a/cayenne/src/test/java/org/apache/cayenne/RowLayoutTest.java b/cayenne/src/test/java/org/apache/cayenne/RowLayoutTest.java
deleted file mode 100644
index e80de4b60..000000000
--- a/cayenne/src/test/java/org/apache/cayenne/RowLayoutTest.java
+++ /dev/null
@@ -1,67 +0,0 @@
-/*****************************************************************
- * 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
- *
- * https://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;
-
-import org.junit.jupiter.api.Test;
-
-import static org.junit.jupiter.api.Assertions.assertArrayEquals;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertSame;
-
-public class RowLayoutTest {
-
- @Test
- public void indexOf() {
- RowLayout layout = new RowLayout(new String[]{"a", "b", "c"});
- assertEquals(3, layout.size());
- assertEquals(0, layout.indexOf("a"));
- assertEquals(2, layout.indexOf("c"));
- assertEquals(-1, layout.indexOf("d"));
- assertEquals("b", layout.nameAt(1));
- }
-
- @Test
- public void duplicates_LastWins() {
- String[] names = {"a", "b", "a", "a"};
- RowLayout layout = new RowLayout(names);
-
- assertEquals(4, layout.size());
- assertEquals(3, layout.indexOf("a"));
- assertEquals(2, layout.indexOf("a_"));
- assertEquals(0, layout.indexOf("a__"));
- assertEquals(1, layout.indexOf("b"));
-
- assertArrayEquals(new String[]{"a", "b", "a", "a"}, names, "the argument must not be changed");
- }
-
- @Test
- public void duplicates_ExistingSuffixedName() {
- RowLayout layout = new RowLayout(new String[]{"a", "a_", "a"});
- assertEquals(2, layout.indexOf("a"));
- assertEquals(1, layout.indexOf("a_"));
- assertEquals(0, layout.indexOf("a__"));
- }
-
- @Test
- public void noDuplicates_NoCopy() {
- RowLayout layout = new RowLayout(new String[]{"a", "b"});
- assertSame("a", layout.nameAt(0));
- }
-}
diff --git a/cayenne/src/test/java/org/apache/cayenne/access/DataContextConcurrentResolveIT.java b/cayenne/src/test/java/org/apache/cayenne/access/DataContextConcurrentResolveIT.java
index 2d5d1a2e5..66ea1b736 100644
--- a/cayenne/src/test/java/org/apache/cayenne/access/DataContextConcurrentResolveIT.java
+++ b/cayenne/src/test/java/org/apache/cayenne/access/DataContextConcurrentResolveIT.java
@@ -39,7 +39,6 @@ import org.junit.jupiter.api.extension.RegisterExtension;
import java.math.BigDecimal;
import java.sql.Types;
import java.util.ArrayList;
-import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Queue;
@@ -241,9 +240,7 @@ public class DataContextConcurrentResolveIT {
}
private static DataRow copy(DataRow row, String title, long version) {
- HashMap<String, Object> values = new HashMap<>(row);
- values.put("PAINTING_TITLE", title);
- return DataRow.of(row.getEntityName(), values, version);
+ return row.merged(Map.of("PAINTING_TITLE", title), version);
}
@Test
diff --git a/cayenne/src/test/java/org/apache/cayenne/access/DataContextDelegateSharedCacheIT.java b/cayenne/src/test/java/org/apache/cayenne/access/DataContextDelegateSharedCacheIT.java
index 89293fdd1..25993d09a 100644
--- a/cayenne/src/test/java/org/apache/cayenne/access/DataContextDelegateSharedCacheIT.java
+++ b/cayenne/src/test/java/org/apache/cayenne/access/DataContextDelegateSharedCacheIT.java
@@ -20,8 +20,8 @@
package org.apache.cayenne.access;
import java.util.Date;
+import java.util.Map;
-import org.apache.cayenne.DataRow;
import org.apache.cayenne.PersistenceState;
import org.apache.cayenne.Persistent;
import org.apache.cayenne.CoreModule;
@@ -72,7 +72,7 @@ public class DataContextDelegateSharedCacheIT {
DataContextDelegate delegate = new MockDataContextDelegate() {
@Override
- public boolean shouldMergeChanges(Persistent object, DataRow snapshotInStore) {
+ public boolean shouldMergeChanges(Persistent object, Map<String, Object> snapshotInStore) {
methodInvoked[0] = true;
return true;
}
@@ -116,7 +116,7 @@ public class DataContextDelegateSharedCacheIT {
DataContextDelegate delegate = new MockDataContextDelegate() {
@Override
- public boolean shouldMergeChanges(Persistent object, DataRow snapshotInStore) {
+ public boolean shouldMergeChanges(Persistent object, Map<String, Object> snapshotInStore) {
return false;
}
};
diff --git a/cayenne/src/test/java/org/apache/cayenne/access/DataContextExtrasIT.java b/cayenne/src/test/java/org/apache/cayenne/access/DataContextExtrasIT.java
index f7623645e..14a57eb1b 100644
--- a/cayenne/src/test/java/org/apache/cayenne/access/DataContextExtrasIT.java
+++ b/cayenne/src/test/java/org/apache/cayenne/access/DataContextExtrasIT.java
@@ -22,6 +22,7 @@ package org.apache.cayenne.access;
import org.apache.cayenne.Cayenne;
import org.apache.cayenne.CayenneRuntimeException;
import org.apache.cayenne.DataRow;
+import org.apache.cayenne.DataRowLayout;
import org.apache.cayenne.ObjectId;
import org.apache.cayenne.PersistenceState;
import org.apache.cayenne.Persistent;
@@ -204,7 +205,8 @@ public class DataContextExtrasIT {
@Test
public void idObjectFromDataRow() {
- DataRow row = DataRow.of("Artist", Map.of("ARTIST_ID", 100000), SnapshotVersion.next());
+ DataRow row = DataRow.of("Artist", DataRowLayout.of("ARTIST_ID"),
+ new Object[]{100000}, SnapshotVersion.next());
Persistent obj = context.objectFromDataRow(Artist.class, row);
assertNotNull(obj);
assertTrue(context.getObjectStore().registeredObjects().contains(obj));
@@ -216,9 +218,8 @@ public class DataContextExtrasIT {
@Test
public void partialObjectFromDataRow() {
- DataRow row = DataRow.of("Artist", Map.of(
- "ARTIST_ID", 100001,
- "ARTIST_NAME", "ArtistXYZ"), SnapshotVersion.next());
+ DataRow row = DataRow.of("Artist", DataRowLayout.of("ARTIST_ID", "ARTIST_NAME"),
+ new Object[]{100001, "ArtistXYZ"}, SnapshotVersion.next());
Persistent obj = context.objectFromDataRow(Artist.class, row);
assertNotNull(obj);
assertTrue(context.getObjectStore().registeredObjects().contains(obj));
@@ -229,10 +230,8 @@ public class DataContextExtrasIT {
@Test
public void fullObjectFromDataRow() {
- DataRow row = DataRow.of("Artist", Map.of(
- "ARTIST_ID", 123456,
- "ARTIST_NAME", "ArtistXYZ",
- "DATE_OF_BIRTH", new Date()), SnapshotVersion.next());
+ DataRow row = DataRow.of("Artist", DataRowLayout.of("ARTIST_ID", "ARTIST_NAME", "DATE_OF_BIRTH"),
+ new Object[]{123456, "ArtistXYZ", new Date()}, SnapshotVersion.next());
Artist obj = context.objectFromDataRow(Artist.class, row);
assertTrue(context.getObjectStore().registeredObjects().contains(obj));
diff --git a/cayenne/src/test/java/org/apache/cayenne/access/DataContextIT.java b/cayenne/src/test/java/org/apache/cayenne/access/DataContextIT.java
index f2ccc8db4..1bc42f528 100644
--- a/cayenne/src/test/java/org/apache/cayenne/access/DataContextIT.java
+++ b/cayenne/src/test/java/org/apache/cayenne/access/DataContextIT.java
@@ -21,6 +21,7 @@ package org.apache.cayenne.access;
import org.apache.cayenne.Cayenne;
import org.apache.cayenne.DataRow;
+import org.apache.cayenne.DataRowLayout;
import org.apache.cayenne.Fault;
import org.apache.cayenne.ObjectId;
import org.apache.cayenne.PersistenceState;
@@ -551,10 +552,8 @@ public class DataContextIT {
@Test
public void invalidateObjects_Vararg() throws Exception {
- DataRow row = DataRow.of("Artist", Map.of(
- "ARTIST_ID", 1,
- "ARTIST_NAME", "ArtistXYZ",
- "DATE_OF_BIRTH", new Date()), SnapshotVersion.next());
+ DataRow row = DataRow.of("Artist", DataRowLayout.of("ARTIST_ID", "ARTIST_NAME", "DATE_OF_BIRTH"),
+ new Object[]{1, "ArtistXYZ", new Date()}, SnapshotVersion.next());
Persistent object = context.objectFromDataRow(Artist.class, row);
ObjectId oid = object.getObjectId();
@@ -574,10 +573,8 @@ public class DataContextIT {
@Test
public void invalidateObjects() throws Exception {
- DataRow row = DataRow.of("Artist", Map.of(
- "ARTIST_ID", 1,
- "ARTIST_NAME", "ArtistXYZ",
- "DATE_OF_BIRTH", new Date()), SnapshotVersion.next());
+ DataRow row = DataRow.of("Artist", DataRowLayout.of("ARTIST_ID", "ARTIST_NAME", "DATE_OF_BIRTH"),
+ new Object[]{1, "ArtistXYZ", new Date()}, SnapshotVersion.next());
Persistent object = context.objectFromDataRow(Artist.class, row);
ObjectId oid = object.getObjectId();
diff --git a/cayenne/src/test/java/org/apache/cayenne/access/DataContextObjectStoreIT.java b/cayenne/src/test/java/org/apache/cayenne/access/DataContextObjectStoreIT.java
index e9dba8ef2..b8ee165f2 100644
--- a/cayenne/src/test/java/org/apache/cayenne/access/DataContextObjectStoreIT.java
+++ b/cayenne/src/test/java/org/apache/cayenne/access/DataContextObjectStoreIT.java
@@ -21,6 +21,7 @@ package org.apache.cayenne.access;
import org.apache.cayenne.Cayenne;
import org.apache.cayenne.DataRow;
+import org.apache.cayenne.DataRowLayout;
import org.apache.cayenne.MockPersistentObject;
import org.apache.cayenne.ObjectId;
import org.apache.cayenne.Persistent;
@@ -32,7 +33,6 @@ import org.apache.cayenne.unit.CayenneProjects;
import org.apache.cayenne.unit.CayenneTestsEnv;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.junit.jupiter.api.Test;
-import java.util.Map;
import java.util.Collections;
import java.util.Date;
@@ -72,10 +72,8 @@ public class DataContextObjectStoreIT {
@Test
public void objectsUnregistered() throws Exception {
- DataRow row = DataRow.of("Artist", Map.of(
- "ARTIST_ID", 1,
- "ARTIST_NAME", "ArtistXYZ",
- "DATE_OF_BIRTH", new Date()), SnapshotVersion.next());
+ DataRow row = DataRow.of("Artist", DataRowLayout.of("ARTIST_ID", "ARTIST_NAME", "DATE_OF_BIRTH"),
+ new Object[]{1, "ArtistXYZ", new Date()}, SnapshotVersion.next());
Persistent object = env.context().objectFromDataRow(Artist.class, row);
ObjectId oid = object.getObjectId();
diff --git a/cayenne/src/test/java/org/apache/cayenne/access/DataContextObjectTrackingIT.java b/cayenne/src/test/java/org/apache/cayenne/access/DataContextObjectTrackingIT.java
index e184d9b14..b6a3562ea 100644
--- a/cayenne/src/test/java/org/apache/cayenne/access/DataContextObjectTrackingIT.java
+++ b/cayenne/src/test/java/org/apache/cayenne/access/DataContextObjectTrackingIT.java
@@ -20,6 +20,7 @@
package org.apache.cayenne.access;
import org.apache.cayenne.DataRow;
+import org.apache.cayenne.DataRowLayout;
import org.apache.cayenne.ObjectId;
import org.apache.cayenne.PersistenceState;
import org.apache.cayenne.Persistent;
@@ -33,7 +34,6 @@ import org.junit.jupiter.api.extension.RegisterExtension;
import org.junit.jupiter.api.Test;
import java.sql.Types;
-import java.util.Map;
import java.util.Collections;
import java.util.Date;
@@ -89,10 +89,8 @@ public class DataContextObjectTrackingIT {
@Test
public void unregisterObject() {
- DataRow row = DataRow.of("Artist", Map.of(
- "ARTIST_ID", 1,
- "ARTIST_NAME", "ArtistXYZ",
- "DATE_OF_BIRTH", new Date()), SnapshotVersion.next());
+ DataRow row = DataRow.of("Artist", DataRowLayout.of("ARTIST_ID", "ARTIST_NAME", "DATE_OF_BIRTH"),
+ new Object[]{1, "ArtistXYZ", new Date()}, SnapshotVersion.next());
Persistent obj = context.objectFromDataRow(Artist.class, row);
ObjectId oid = obj.getObjectId();
@@ -112,10 +110,8 @@ public class DataContextObjectTrackingIT {
@Test
public void invalidateObjects_Vararg() {
- DataRow row = DataRow.of("Artist", Map.of(
- "ARTIST_ID", 1,
- "ARTIST_NAME", "ArtistXYZ",
- "DATE_OF_BIRTH", new Date()), SnapshotVersion.next());
+ DataRow row = DataRow.of("Artist", DataRowLayout.of("ARTIST_ID", "ARTIST_NAME", "DATE_OF_BIRTH"),
+ new Object[]{1, "ArtistXYZ", new Date()}, SnapshotVersion.next());
Persistent obj = context.objectFromDataRow(Artist.class, row);
ObjectId oid = obj.getObjectId();
diff --git a/cayenne/src/test/java/org/apache/cayenne/access/DataContextProcedureQueryIT.java b/cayenne/src/test/java/org/apache/cayenne/access/DataContextProcedureQueryIT.java
index ba0af6980..9f0554528 100644
--- a/cayenne/src/test/java/org/apache/cayenne/access/DataContextProcedureQueryIT.java
+++ b/cayenne/src/test/java/org/apache/cayenne/access/DataContextProcedureQueryIT.java
@@ -20,6 +20,7 @@
package org.apache.cayenne.access;
import org.apache.cayenne.DataRow;
+import org.apache.cayenne.DataRowLayout;
import org.apache.cayenne.OutParametersResult;
import org.apache.cayenne.QueryResult;
import org.apache.cayenne.dba.TypesMapping;
@@ -41,7 +42,6 @@ import org.junit.jupiter.api.extension.RegisterExtension;
import java.math.BigDecimal;
import java.sql.Types;
-import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
@@ -427,12 +427,13 @@ public class DataContextProcedureQueryIT {
* An ugly hack - converting row keys to uppercase ... Tracked via CAY-148.
*/
protected DataRow uppercaseConverter(DataRow row) {
- DataRow converted = DataRow.of(row.getEntityName(), new HashMap<>(row.size()), SnapshotVersion.next());
-
- for (Entry<String, Object> entry : row.entrySet()) {
- converted.put(entry.getKey().toString().toUpperCase(), entry.getValue());
+ String[] names = new String[row.size()];
+ Object[] values = new Object[names.length];
+ int i = 0;
+ for (Map.Entry<String, Object> entry : row.entrySet()) {
+ names[i] = entry.getKey().toUpperCase();
+ values[i++] = entry.getValue();
}
-
- return converted;
+ return DataRow.of(row.getEntityName(), DataRowLayout.of(names), values, SnapshotVersion.next());
}
}
diff --git a/cayenne/src/test/java/org/apache/cayenne/access/DataContextQueryCachingIT.java b/cayenne/src/test/java/org/apache/cayenne/access/DataContextQueryCachingIT.java
index ddb2c642f..bf3512269 100644
--- a/cayenne/src/test/java/org/apache/cayenne/access/DataContextQueryCachingIT.java
+++ b/cayenne/src/test/java/org/apache/cayenne/access/DataContextQueryCachingIT.java
@@ -20,6 +20,7 @@
package org.apache.cayenne.access;
import org.apache.cayenne.DataRow;
+import org.apache.cayenne.DataRowLayout;
import org.apache.cayenne.Persistent;
import org.apache.cayenne.query.ObjectSelect;
import org.apache.cayenne.query.QueryCacheStrategy;
@@ -32,7 +33,6 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
-import java.util.Map;
import java.util.ArrayList;
import java.util.List;
@@ -221,9 +221,8 @@ public class DataContextQueryCachingIT {
List<Object> rows = new ArrayList<>(len);
for (int i = 0; i < len; i++) {
- DataRow a = DataRow.of("Artist", Map.of(
- "ARTIST_ID", i + 1,
- "ARTIST_NAME", "A-" + (i + 1)), SnapshotVersion.next());
+ DataRow a = DataRow.of("Artist", DataRowLayout.of("ARTIST_ID", "ARTIST_NAME"),
+ new Object[]{i + 1, "A-" + (i + 1)}, SnapshotVersion.next());
rows.add(a);
}
diff --git a/cayenne/src/test/java/org/apache/cayenne/access/DataRowStoreIT.java b/cayenne/src/test/java/org/apache/cayenne/access/DataRowStoreIT.java
index 5c3878ac0..a0d21e622 100644
--- a/cayenne/src/test/java/org/apache/cayenne/access/DataRowStoreIT.java
+++ b/cayenne/src/test/java/org/apache/cayenne/access/DataRowStoreIT.java
@@ -20,6 +20,7 @@
package org.apache.cayenne.access;
import org.apache.cayenne.DataRow;
+import org.apache.cayenne.DataRowLayout;
import org.apache.cayenne.ObjectId;
import org.apache.cayenne.access.event.SnapshotEvent;
import org.apache.cayenne.event.DefaultEventManager;
@@ -80,15 +81,15 @@ public class DataRowStoreIT {
ObjectId key1 = ObjectId.of("Artist", Artist.ARTIST_ID_PK_COLUMN, 1);
Map<ObjectId, DataRow> diff1 = new HashMap<>();
- diff1.put(key1, DataRow.of("Artist", new HashMap<>(1), SnapshotVersion.next()));
+ diff1.put(key1, DataRow.of("Artist", DataRowLayout.of(), new Object[0], SnapshotVersion.next()));
ObjectId key2 = ObjectId.of("Artist", Artist.ARTIST_ID_PK_COLUMN, 2);
Map<ObjectId, DataRow> diff2 = new HashMap<>();
- diff2.put(key2, DataRow.of("Artist", new HashMap<>(1), SnapshotVersion.next()));
+ diff2.put(key2, DataRow.of("Artist", DataRowLayout.of(), new Object[0], SnapshotVersion.next()));
ObjectId key3 = ObjectId.of("Artist", Artist.ARTIST_ID_PK_COLUMN, 3);
Map<ObjectId, DataRow> diff3 = new HashMap<>();
- diff3.put(key3, DataRow.of("Artist", new HashMap<>(1), SnapshotVersion.next()));
+ diff3.put(key3, DataRow.of("Artist", DataRowLayout.of(), new Object[0], SnapshotVersion.next()));
cache.processSnapshotChanges(
this,
@@ -137,28 +138,24 @@ public class DataRowStoreIT {
eventManager.addListener(events, SnapshotEvent.class, List::add, cache.getSnapshotEventSubject());
ObjectId key = ObjectId.of("Artist", Artist.ARTIST_ID_PK_COLUMN, 1);
- DataRow old = DataRow.of("Artist",
- new HashMap<>(Map.of("ARTIST_ID", 1, "ARTIST_NAME", "a", "DATE_OF_BIRTH", "d")),
- SnapshotVersion.next());
+ DataRowLayout layout = DataRowLayout.of("ARTIST_ID", "ARTIST_NAME", "DATE_OF_BIRTH");
+ DataRow old = DataRow.of("Artist", layout, new Object[]{1, "a", "d"}, SnapshotVersion.next());
cache.processSnapshotChanges(this, Map.of(key, old), Collections.emptyList(), Collections.emptyList(),
Collections.emptyList());
assertTrue(events.isEmpty(), "a new snapshot is not an update");
- DataRow same = DataRow.of("Artist", new HashMap<>(old), SnapshotVersion.next());
+ DataRow same = DataRow.of("Artist", layout, new Object[]{1, "a", "d"}, SnapshotVersion.next());
cache.processSnapshotChanges(this, Map.of(key, same), Collections.emptyList(), Collections.emptyList(),
Collections.emptyList());
assertTrue(events.isEmpty(), "an unchanged snapshot is not broadcast");
- DataRow changed = DataRow.of("Artist",
- new HashMap<>(Map.of("ARTIST_ID", 1, "ARTIST_NAME", "b", "DATE_OF_BIRTH", "d")),
- SnapshotVersion.next());
+ DataRow changed = DataRow.of("Artist", layout, new Object[]{1, "b", "d"}, SnapshotVersion.next());
cache.processSnapshotChanges(this, Map.of(key, changed), Collections.emptyList(),
Collections.emptyList(), Collections.emptyList());
assertEquals(1, events.size());
- DataRow diff = events.getFirst().getModifiedDiffs().get(key);
- assertEquals(Map.of("ARTIST_NAME", "b"), diff);
- assertEquals(changed.getVersion(), diff.getVersion());
+ assertEquals(Map.of("ARTIST_NAME", "b"), events.getFirst().getModifiedDiffs().get(key));
+ assertEquals(changed.getVersion(), events.getFirst().getVersion());
} finally {
eventManager.shutdown();
}
diff --git a/cayenne/src/test/java/org/apache/cayenne/access/DataRowUtilsIT.java b/cayenne/src/test/java/org/apache/cayenne/access/DataRowUtilsIT.java
index 1cc09bac7..3f4786b4a 100644
--- a/cayenne/src/test/java/org/apache/cayenne/access/DataRowUtilsIT.java
+++ b/cayenne/src/test/java/org/apache/cayenne/access/DataRowUtilsIT.java
@@ -19,10 +19,10 @@
package org.apache.cayenne.access;
-import java.util.Map;
import java.util.List;
import org.apache.cayenne.DataRow;
+import org.apache.cayenne.DataRowLayout;
import org.apache.cayenne.query.ObjectSelect;
import org.apache.cayenne.reflect.ArcProperty;
import org.apache.cayenne.reflect.ClassDescriptor;
@@ -84,12 +84,11 @@ public class DataRowUtilsIT {
.get(0);
a1.setArtistName(n1);
- DataRow s2 = DataRow.of("Artist", Map.of(
- "ARTIST_NAME", n2,
- "DATE_OF_BIRTH", new java.util.Date()), SnapshotVersion.next());
+ DataRow s2 = DataRow.of("Artist", DataRowLayout.of("ARTIST_NAME", "DATE_OF_BIRTH"),
+ new Object[]{n2, new java.util.Date()}, SnapshotVersion.next());
ClassDescriptor d = context.getEntityResolver().getClassDescriptor("Artist");
- DataRowUtils.mergeObjectWithSnapshot(context, d, a1, s2);
+ DataRowUtils.mergeObjectWithSnapshot(context, a1, s2, MergePlan.of(d));
// name was modified, so it should not change during merge
assertEquals(n1, a1.getArtistName());
diff --git a/cayenne/src/test/java/org/apache/cayenne/access/LoggingObserverTest.java b/cayenne/src/test/java/org/apache/cayenne/access/LoggingObserverTest.java
index 91f4cce55..bb2eb5242 100644
--- a/cayenne/src/test/java/org/apache/cayenne/access/LoggingObserverTest.java
+++ b/cayenne/src/test/java/org/apache/cayenne/access/LoggingObserverTest.java
@@ -20,6 +20,7 @@
package org.apache.cayenne.access;
import org.apache.cayenne.DataRow;
+import org.apache.cayenne.DataRowLayout;
import org.apache.cayenne.access.jdbc.CSParameter;
import org.apache.cayenne.access.jdbc.PSBatchParameter;
import org.apache.cayenne.access.jdbc.PSParameter;
@@ -194,8 +195,10 @@ public class LoggingObserverTest {
CapturingLogger logger = new CapturingLogger();
LoggingObserver observer = observer(logger);
- DataRow row1 = DataRow.of("Artist", Map.of("ARTIST_ID", 1L), SnapshotVersion.next());
- DataRow row2 = DataRow.of("Artist", Map.of("ARTIST_ID", 2L), SnapshotVersion.next());
+ DataRow row1 = DataRow.of("Artist", DataRowLayout.of("ARTIST_ID"),
+ new Object[]{1L}, SnapshotVersion.next());
+ DataRow row2 = DataRow.of("Artist", DataRowLayout.of("ARTIST_ID"),
+ new Object[]{2L}, SnapshotVersion.next());
observer.nextStatement(null, batch());
observer.nextGeneratedRows(null, asList(row1, row2), List.of());
diff --git a/cayenne/src/test/java/org/apache/cayenne/access/MockDataContextDelegate.java b/cayenne/src/test/java/org/apache/cayenne/access/MockDataContextDelegate.java
index 74b49c327..b199f62d8 100644
--- a/cayenne/src/test/java/org/apache/cayenne/access/MockDataContextDelegate.java
+++ b/cayenne/src/test/java/org/apache/cayenne/access/MockDataContextDelegate.java
@@ -19,10 +19,11 @@
package org.apache.cayenne.access;
-import org.apache.cayenne.DataRow;
import org.apache.cayenne.Persistent;
import org.apache.cayenne.query.Query;
+import java.util.Map;
+
/**
* Default implementation of DataContextDelegate that serves as a superclass for mockup
* test delegates.
@@ -41,7 +42,7 @@ public class MockDataContextDelegate implements DataContextDelegate {
}
@Override
- public boolean shouldMergeChanges(Persistent object, DataRow snapshotInStore) {
+ public boolean shouldMergeChanges(Persistent object, Map<String, Object> snapshotInStore) {
return true;
}
diff --git a/cayenne/src/test/java/org/apache/cayenne/access/MockDataRowStore.java b/cayenne/src/test/java/org/apache/cayenne/access/MockDataRowStore.java
index 261874f84..7a904cb68 100644
--- a/cayenne/src/test/java/org/apache/cayenne/access/MockDataRowStore.java
+++ b/cayenne/src/test/java/org/apache/cayenne/access/MockDataRowStore.java
@@ -20,10 +20,10 @@
package org.apache.cayenne.access;
import org.apache.cayenne.DataRow;
+import org.apache.cayenne.DataRowLayout;
import org.apache.cayenne.ObjectId;
import org.apache.cayenne.event.MockEventManager;
-import java.util.HashMap;
import java.util.Map;
/**
@@ -46,11 +46,18 @@ public class MockDataRowStore extends DataRowStore {
}
public void putSnapshot(ObjectId id, Map<String, ?> snapshot) {
- snapshots.put(id, DataRow.of(id.getEntityName(), new HashMap<>(snapshot), SnapshotVersion.next()));
+ String[] names = new String[snapshot.size()];
+ Object[] values = new Object[names.length];
+ int i = 0;
+ for (Map.Entry<String, ?> entry : snapshot.entrySet()) {
+ names[i] = entry.getKey();
+ values[i++] = entry.getValue();
+ }
+ snapshots.put(id, DataRow.of(id.getEntityName(), DataRowLayout.of(names), values, SnapshotVersion.next()));
}
public void putEmptySnapshot(ObjectId id) {
- snapshots.put(id, DataRow.of(id.getEntityName(), new HashMap<>(2), SnapshotVersion.next()));
+ snapshots.put(id, DataRow.of(id.getEntityName(), DataRowLayout.of(), new Object[0], SnapshotVersion.next()));
}
}
diff --git a/cayenne/src/test/java/org/apache/cayenne/access/NestedDataContextReadIT.java b/cayenne/src/test/java/org/apache/cayenne/access/NestedDataContextReadIT.java
index 629ddcbd6..b411b4e7e 100644
--- a/cayenne/src/test/java/org/apache/cayenne/access/NestedDataContextReadIT.java
+++ b/cayenne/src/test/java/org/apache/cayenne/access/NestedDataContextReadIT.java
@@ -21,6 +21,7 @@ package org.apache.cayenne.access;
import org.apache.cayenne.Cayenne;
import org.apache.cayenne.DataRow;
+import org.apache.cayenne.DataRowLayout;
import org.apache.cayenne.ObjectContext;
import org.apache.cayenne.ObjectId;
import org.apache.cayenne.PersistenceState;
@@ -37,7 +38,6 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.sql.Types;
-import java.util.Map;
import java.util.Date;
import java.util.List;
@@ -339,10 +339,8 @@ public class NestedDataContextReadIT {
DataContext childContext = (DataContext) runtime.newContext(context);
- DataRow row = DataRow.of("Artist", Map.of(
- "ARTIST_ID", 5L,
- "ARTIST_NAME", "A",
- "DATE_OF_BIRTH", new Date()), SnapshotVersion.next());
+ DataRow row = DataRow.of("Artist", DataRowLayout.of("ARTIST_ID", "ARTIST_NAME", "DATE_OF_BIRTH"),
+ new Object[]{5L, "A", new Date()}, SnapshotVersion.next());
Artist artist = childContext.objectFromDataRow(Artist.class, row);
assertNotNull(artist);
diff --git a/cayenne/src/test/java/org/apache/cayenne/access/PrefetchProcessorTreeBuilderIT.java b/cayenne/src/test/java/org/apache/cayenne/access/PrefetchProcessorTreeBuilderIT.java
index 6b6d5d296..842652271 100644
--- a/cayenne/src/test/java/org/apache/cayenne/access/PrefetchProcessorTreeBuilderIT.java
+++ b/cayenne/src/test/java/org/apache/cayenne/access/PrefetchProcessorTreeBuilderIT.java
@@ -20,6 +20,7 @@
package org.apache.cayenne.access;
import org.apache.cayenne.DataRow;
+import org.apache.cayenne.DataRowLayout;
import org.apache.cayenne.exp.path.CayennePath;
import org.apache.cayenne.map.DataMap;
import org.apache.cayenne.map.DbEntity;
@@ -69,8 +70,8 @@ public class PrefetchProcessorTreeBuilderIT {
final ClassDescriptor descriptor = resolver.getClassDescriptor("Artist");
List<DataRow> dataRows = new ArrayList<>();
- dataRows.add(DataRow.of("Artist", new HashMap<>(4), SnapshotVersion.next()));
- dataRows.add(DataRow.of("Artist", new HashMap<>(4), SnapshotVersion.next()));
+ dataRows.add(DataRow.of("Artist", DataRowLayout.of(), new Object[0], SnapshotVersion.next()));
+ dataRows.add(DataRow.of("Artist", DataRowLayout.of(), new Object[0], SnapshotVersion.next()));
QueryMetadata metadata = new MockQueryMetadata() {
diff --git a/cayenne/src/test/java/org/apache/cayenne/access/SimpleIdIncrementalFaultListDataRowsIT.java b/cayenne/src/test/java/org/apache/cayenne/access/SimpleIdIncrementalFaultListDataRowsIT.java
index 117f9b0f5..77e7c6819 100644
--- a/cayenne/src/test/java/org/apache/cayenne/access/SimpleIdIncrementalFaultListDataRowsIT.java
+++ b/cayenne/src/test/java/org/apache/cayenne/access/SimpleIdIncrementalFaultListDataRowsIT.java
@@ -20,6 +20,7 @@
package org.apache.cayenne.access;
import org.apache.cayenne.DataRow;
+import org.apache.cayenne.DataRowLayout;
import org.apache.cayenne.query.ObjectSelect;
import org.apache.cayenne.test.jdbc.TableHelper;
import org.apache.cayenne.testdo.testmap.Artist;
@@ -29,9 +30,10 @@ import org.junit.jupiter.api.extension.RegisterExtension;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
-import java.util.HashMap;
+import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
+import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@@ -117,7 +119,7 @@ public class SimpleIdIncrementalFaultListDataRowsIT {
DataRow row = artists.getFirst();
assertEquals(19, list.indexOf(row));
- DataRow clone = DataRow.of(row.getEntityName(), new HashMap<>(row), SnapshotVersion.next());
+ DataRow clone = row.merged(Map.of(), SnapshotVersion.next());
assertEquals(19, list.indexOf(clone));
assertEquals(-1, list.indexOf(without(row, "ARTIST_ID")));
@@ -199,8 +201,13 @@ public class SimpleIdIncrementalFaultListDataRowsIT {
}
}
private static DataRow without(DataRow row, String key) {
- HashMap<String, Object> values = new HashMap<>(row);
- values.remove(key);
- return DataRow.of(row.getEntityName(), values, row.getVersion());
+ List<String> names = new ArrayList<>(row.keySet());
+ names.remove(key);
+ Object[] values = new Object[names.size()];
+ for (int i = 0; i < values.length; i++) {
+ values[i] = row.get(names.get(i));
+ }
+ DataRowLayout layout = DataRowLayout.of(names.toArray(new String[0]));
+ return DataRow.of(row.getEntityName(), layout, values, row.getVersion());
}
}
diff --git a/cayenne/src/test/java/org/apache/cayenne/access/SnapshotVersionTest.java b/cayenne/src/test/java/org/apache/cayenne/access/SnapshotVersionTest.java
index 2fbc1136d..9d6da29d3 100644
--- a/cayenne/src/test/java/org/apache/cayenne/access/SnapshotVersionTest.java
+++ b/cayenne/src/test/java/org/apache/cayenne/access/SnapshotVersionTest.java
@@ -20,6 +20,7 @@
package org.apache.cayenne.access;
import org.apache.cayenne.DataRow;
+import org.apache.cayenne.DataRowLayout;
import org.apache.cayenne.Persistent;
import org.junit.jupiter.api.Test;
@@ -27,7 +28,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
-import java.util.HashMap;
public class SnapshotVersionTest {
@@ -37,7 +37,7 @@ public class SnapshotVersionTest {
long v2 = SnapshotVersion.next();
assertNotEquals(SnapshotVersion.NONE, v1);
assertTrue(v2 > v1);
- assertTrue(DataRow.of("E", new HashMap<>(10), SnapshotVersion.next()).getVersion() > v2);
+ assertTrue(DataRow.of("E", DataRowLayout.of(), new Object[0], SnapshotVersion.next()).getVersion() > v2);
}
@Test
diff --git a/cayenne/src/test/java/org/apache/cayenne/access/event/SnapshotEventTest.java b/cayenne/src/test/java/org/apache/cayenne/access/event/SnapshotEventTest.java
index e3bf0aec6..d429161dc 100644
--- a/cayenne/src/test/java/org/apache/cayenne/access/event/SnapshotEventTest.java
+++ b/cayenne/src/test/java/org/apache/cayenne/access/event/SnapshotEventTest.java
@@ -19,7 +19,6 @@
package org.apache.cayenne.access.event;
-import org.apache.cayenne.DataRow;
import org.apache.cayenne.ObjectId;
import org.junit.jupiter.api.Test;
@@ -28,6 +27,7 @@ import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
+import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertSame;
public class SnapshotEventTest {
@@ -37,12 +37,13 @@ public class SnapshotEventTest {
Object source = new Object();
Collection<ObjectId> deleted = new ArrayList<>();
Collection<ObjectId> invalidated = new ArrayList<>();
- Map<ObjectId, DataRow> modified = new HashMap<>();
+ Map<ObjectId, Map<String, Object>> modified = new HashMap<>();
Collection<ObjectId> related = new ArrayList<>();
SnapshotEvent event = new SnapshotEvent(
source,
source,
+ 7,
modified,
deleted,
invalidated,
@@ -52,6 +53,7 @@ public class SnapshotEventTest {
assertSame(deleted, event.getDeletedIds());
assertSame(invalidated, event.getInvalidatedIds());
assertSame(modified, event.getModifiedDiffs());
+ assertEquals(7, event.getVersion());
assertSame(related, event.getIndirectlyModifiedIds());
}
}
diff --git a/cayenne/src/test/java/org/apache/cayenne/event/EventBridgeTest.java b/cayenne/src/test/java/org/apache/cayenne/event/EventBridgeTest.java
index a641acd56..246bd369d 100644
--- a/cayenne/src/test/java/org/apache/cayenne/event/EventBridgeTest.java
+++ b/cayenne/src/test/java/org/apache/cayenne/event/EventBridgeTest.java
@@ -19,6 +19,7 @@
package org.apache.cayenne.event;
+import org.apache.cayenne.access.SnapshotVersion;
import org.apache.cayenne.access.event.SnapshotEvent;
import org.apache.cayenne.test.parallel.ParallelTestContainer;
import org.junit.jupiter.api.AfterEach;
@@ -116,6 +117,7 @@ public class EventBridgeTest {
final SnapshotEvent eventWithNoSubject = new SnapshotEvent(
this,
this,
+ SnapshotVersion.NONE,
null,
null,
null,
@@ -141,6 +143,7 @@ public class EventBridgeTest {
final SnapshotEvent eventWithSubject = new SnapshotEvent(
this,
this,
+ SnapshotVersion.NONE,
null,
null,
null,
diff --git a/cayenne/src/test/java/org/apache/cayenne/query/ProcedureCallIT.java b/cayenne/src/test/java/org/apache/cayenne/query/ProcedureCallIT.java
index aedab8a23..24c8e46d8 100644
--- a/cayenne/src/test/java/org/apache/cayenne/query/ProcedureCallIT.java
+++ b/cayenne/src/test/java/org/apache/cayenne/query/ProcedureCallIT.java
@@ -19,6 +19,7 @@
package org.apache.cayenne.query;
import org.apache.cayenne.DataRow;
+import org.apache.cayenne.DataRowLayout;
import org.apache.cayenne.OutParametersResult;
import org.apache.cayenne.QueryResult;
import org.apache.cayenne.access.SnapshotVersion;
@@ -35,7 +36,6 @@ import org.junit.jupiter.api.extension.RegisterExtension;
import java.math.BigDecimal;
import java.sql.Types;
-import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
@@ -345,12 +345,13 @@ public class ProcedureCallIT {
* An ugly hack - converting row keys to uppercase ... Tracked via CAY-148.
*/
private DataRow uppercaseConverter(DataRow row) {
- DataRow converted = DataRow.of(row.getEntityName(), new HashMap<>(row.size()), SnapshotVersion.next());
-
+ String[] names = new String[row.size()];
+ Object[] values = new Object[names.length];
+ int i = 0;
for (Map.Entry<String, Object> entry : row.entrySet()) {
- converted.put(entry.getKey().toUpperCase(), entry.getValue());
+ names[i] = entry.getKey().toUpperCase();
+ values[i++] = entry.getValue();
}
-
- return converted;
+ return DataRow.of(row.getEntityName(), DataRowLayout.of(names), values, SnapshotVersion.next());
}
}
diff --git a/cayenne/src/test/java/org/apache/cayenne/reflect/PersistentDescriptor_SnapshotLayoutIT.java b/cayenne/src/test/java/org/apache/cayenne/reflect/PersistentDescriptor_SnapshotLayoutIT.java
new file mode 100644
index 000000000..24fa10071
--- /dev/null
+++ b/cayenne/src/test/java/org/apache/cayenne/reflect/PersistentDescriptor_SnapshotLayoutIT.java
@@ -0,0 +1,117 @@
+/*****************************************************************
+ * 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
+ *
+ * https://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.reflect;
+
+import org.apache.cayenne.DataRow;
+import org.apache.cayenne.DataRowLayout;
+import org.apache.cayenne.ObjectContext;
+import org.apache.cayenne.access.DataContext;
+import org.apache.cayenne.testdo.testmap.Artist;
+import org.apache.cayenne.testdo.testmap.Painting;
+import org.apache.cayenne.unit.CayenneProjects;
+import org.apache.cayenne.unit.CayenneTestsEnv;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class PersistentDescriptor_SnapshotLayoutIT {
+
+ @RegisterExtension
+ static final CayenneTestsEnv env = CayenneTestsEnv.forProject(CayenneProjects.TESTMAP_PROJECT);
+
+ private static Set<String> names(DataRowLayout layout) {
+ Set<String> names = new HashSet<>();
+ for (int i = 0; i < layout.size(); i++) {
+ names.add(layout.nameAt(i));
+ }
+ return names;
+ }
+
+ private DataRowLayout layout(String entityName) {
+ return env.context().getEntityResolver().getClassDescriptor(entityName).getSnapshotLayout();
+ }
+
+ @Test
+ public void attributesForeignKeysAndPk() {
+ // the FKs of "toArtist" and "toGallery" are columns, the to-dependent-PK "toPaintingInfo" contributes nothing
+ DataRowLayout painting = layout("Painting");
+ assertEquals(Set.of("PAINTING_ID", "PAINTING_TITLE", "PAINTING_DESCRIPTION", "ESTIMATED_PRICE", "ARTIST_ID",
+ "GALLERY_ID"), names(painting));
+ assertEquals(6, painting.size(), "each column is listed once");
+
+ DataRowLayout artist = layout("Artist");
+ assertEquals(Set.of("ARTIST_ID", "ARTIST_NAME", "DATE_OF_BIRTH"), names(artist));
+ }
+
+ @Test
+ public void flattenedAttributesAndAdditionalPks() {
+ Set<String> names = names(layout("CompoundPainting"));
+ assertTrue(names.containsAll(List.of("PAINTING_ID", "PAINTING_TITLE", "ESTIMATED_PRICE",
+ "toArtist.ARTIST_NAME", "toGallery.GALLERY_NAME", "toPaintingInfo.TEXT_REVIEW",
+ "toArtist.ARTIST_ID", "toGallery.GALLERY_ID", "toPaintingInfo.PAINTING_ID")), names.toString());
+ }
+
+ @Test
+ public void sameInstance() {
+ assertSame(layout("Artist"), layout("Artist"));
+ }
+
+ @Test
+ public void snapshotsAreBuiltOverTheLayout() {
+ ObjectContext context = env.context();
+ Artist artist = context.newObject(Artist.class);
+ artist.setArtistName("a");
+ Painting painting = context.newObject(Painting.class);
+ painting.setPaintingTitle("p");
+ painting.setToArtist(artist);
+ context.commitChanges();
+
+ // the keys of a snapshot come in the layout order
+ DataRowLayout layout = layout("Painting");
+ List<String> expected = new ArrayList<>();
+ for (int i = 0; i < layout.size(); i++) {
+ expected.add(layout.nameAt(i));
+ }
+
+ DataRow snapshot = ((DataContext) context).currentSnapshot(painting);
+ assertEquals(expected, new ArrayList<>(snapshot.keySet()));
+ assertEquals(artist.getObjectId().getIdSnapshot().get("ARTIST_ID"), snapshot.get("ARTIST_ID"));
+ assertEquals("p", snapshot.get("PAINTING_TITLE"));
+
+ // a partial snapshot, of an object pointing at an uncommitted one, has only the columns it could fill: a null
+ // to-one is a null FK, an uncommitted one and the object's own temporary id are missing columns
+ Painting uncommitted = context.newObject(Painting.class);
+ uncommitted.setPaintingTitle("u");
+ uncommitted.setToArtist(context.newObject(Artist.class));
+ DataRow partial = ((DataContext) context).currentSnapshot(uncommitted);
+ assertEquals(Set.of("PAINTING_TITLE", "PAINTING_DESCRIPTION", "ESTIMATED_PRICE", "GALLERY_ID"),
+ partial.keySet());
+ assertNull(partial.get("GALLERY_ID"));
+ }
+}