This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch past-M2
in repository https://gitbox.apache.org/repos/asf/cayenne.git
The following commit(s) were added to refs/heads/past-M2 by this push:
new 5c22b2d47 CAY-2954 Do not wrap selecting queries in transactions
5c22b2d47 is described below
commit 5c22b2d478cac99b5a014083b7827ca75588416a
Author: Andrus Adamchik <[email protected]>
AuthorDate: Sat Jun 13 13:53:49 2026 -0400
CAY-2954 Do not wrap selecting queries in transactions
---
RELEASE-NOTES.txt | 1 +
.../cayenne/access/DataDomainQueryAction.java | 16 ++-
.../apache/cayenne/access/DataNodeQueryAction.java | 14 +--
.../apache/cayenne/access/jdbc/SelectAction.java | 7 +-
.../java/org/apache/cayenne/dba/AutoAdapter.java | 1 +
.../cayenne/dba/postgres/PostgresSelectAction.java | 56 +++++++++
.../org/apache/cayenne/query/ObjectIdQuery.java | 5 +
.../apache/cayenne/query/ObjectSelectMetadata.java | 8 ++
.../org/apache/cayenne/query/QueryMetadata.java | 11 ++
.../apache/cayenne/query/QueryMetadataProxy.java | 5 +
.../apache/cayenne/query/RelationshipQuery.java | 5 +
.../org/apache/cayenne/tx/BaseTransaction.java | 6 +-
.../cayenne/tx/DefaultTransactionFactory.java | 5 -
.../cayenne/tx/DefaultTransactionManager.java | 20 +++-
.../org/apache/cayenne/tx/ReadOnlyTransaction.java | 130 +++++++++++++++++++++
.../org/apache/cayenne/tx/TransactionFactory.java | 5 +-
.../org/apache/cayenne/tx/TransactionManager.java | 15 +++
.../apache/cayenne/tx/TransactionPropagation.java | 6 +-
.../apache/cayenne/access/DataContextBlobIT.java | 25 +++-
.../apache/cayenne/query/ObjectIdQueryTest.java | 5 +
.../org/apache/cayenne/query/ObjectSelectTest.java | 7 ++
.../org/apache/cayenne/query/SQLSelectTest.java | 6 +
.../org/apache/cayenne/query/SQLTemplateTest.java | 8 ++
.../apache/cayenne/tx/ReadOnlyTransactionTest.java | 86 ++++++++++++++
24 files changed, 417 insertions(+), 36 deletions(-)
diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt
index 62c80db7e..cc06801ef 100644
--- a/RELEASE-NOTES.txt
+++ b/RELEASE-NOTES.txt
@@ -13,6 +13,7 @@ Date:
----------------------------------
Changes/New Features:
+CAY-2954 Don't wrap selecting queries in transactions
CAY-2956 Get rid of a dedicated adapter for Oracle 8
CAY-2957 Get rid of adapter for legacy HSQLDB <= 1.8
CAY-2963 Replace TypesHandler / types.xml with hardcoded map
diff --git
a/cayenne/src/main/java/org/apache/cayenne/access/DataDomainQueryAction.java
b/cayenne/src/main/java/org/apache/cayenne/access/DataDomainQueryAction.java
index 0d0719f5b..fd1dd1475 100644
--- a/cayenne/src/main/java/org/apache/cayenne/access/DataDomainQueryAction.java
+++ b/cayenne/src/main/java/org/apache/cayenne/access/DataDomainQueryAction.java
@@ -54,6 +54,7 @@ import org.apache.cayenne.query.RelationshipQuery;
import org.apache.cayenne.reflect.ClassDescriptor;
import org.apache.cayenne.reflect.LifecycleCallbackRegistry;
import org.apache.cayenne.tx.BaseTransaction;
+import org.apache.cayenne.tx.ReadOnlyTransaction;
import org.apache.cayenne.tx.Transaction;
import org.apache.cayenne.util.GenericResponse;
import org.apache.cayenne.util.ListResponse;
@@ -483,10 +484,17 @@ class DataDomainQueryAction implements QueryRouter,
OperationObserver {
* Gets response from the underlying DataNodes.
*/
void runQueryInTransaction() {
- domain.getTransactionManager().performInTransaction(() -> {
- runQuery();
- return null;
- });
+ if (metadata.isReadOnly() && BaseTransaction.getThreadTransaction() ==
null) {
+ domain.getTransactionManager().performInTransaction(() -> {
+ runQuery();
+ return null;
+ }, descriptor -> new ReadOnlyTransaction());
+ } else {
+ domain.getTransactionManager().performInTransaction(() -> {
+ runQuery();
+ return null;
+ });
+ }
}
private void runQuery() {
diff --git
a/cayenne/src/main/java/org/apache/cayenne/access/DataNodeQueryAction.java
b/cayenne/src/main/java/org/apache/cayenne/access/DataNodeQueryAction.java
index a7787b795..a071a92c6 100644
--- a/cayenne/src/main/java/org/apache/cayenne/access/DataNodeQueryAction.java
+++ b/cayenne/src/main/java/org/apache/cayenne/access/DataNodeQueryAction.java
@@ -19,20 +19,19 @@
package org.apache.cayenne.access;
-import java.sql.Connection;
-import java.sql.SQLException;
-import java.util.List;
-
import org.apache.cayenne.ObjectId;
import org.apache.cayenne.ResultIterator;
import org.apache.cayenne.query.Query;
import org.apache.cayenne.query.SQLAction;
+import java.sql.Connection;
+import java.util.List;
+
/**
* A helper that executes a sequence of queries, providing correct mapping of
the results
* to the original query. Note that this class is not thread-safe as it stores
current
* query execution state.
- *
+ *
* @since 1.2
*/
class DataNodeQueryAction {
@@ -45,8 +44,7 @@ class DataNodeQueryAction {
this.node = node;
}
- public void runQuery(Connection connection, final Query originalQuery)
- throws SQLException, Exception {
+ public void runQuery(Connection connection, Query originalQuery) throws
Exception {
// wrap to ensure that the result is mapped back to the original
query, even if
// the underlying SQLAction uses query substitute...
@@ -71,7 +69,7 @@ class DataNodeQueryAction {
public void nextRows(Query q, ResultIterator it) {
observer.nextRows(originalQuery, it);
}
-
+
@Override
public void nextGeneratedRows(Query query, ResultIterator<?> keys,
List<ObjectId> idsToUpdate) {
observer.nextGeneratedRows(originalQuery, keys, idsToUpdate);
diff --git
a/cayenne/src/main/java/org/apache/cayenne/access/jdbc/SelectAction.java
b/cayenne/src/main/java/org/apache/cayenne/access/jdbc/SelectAction.java
index e3e2b5aeb..acaaf155b 100644
--- a/cayenne/src/main/java/org/apache/cayenne/access/jdbc/SelectAction.java
+++ b/cayenne/src/main/java/org/apache/cayenne/access/jdbc/SelectAction.java
@@ -75,12 +75,15 @@ public class SelectAction extends BaseSQLAction {
@Override
public void performAction(Connection connection, OperationObserver
observer) throws Exception {
+ TranslatedSelect translated =
dataNode.getSelectTranslator().translate(query, dataNode.getAdapter(),
dataNode.getEntityResolver());
+ performAction(connection, observer, translated);
+ }
+
+ protected void performAction(Connection connection, OperationObserver
observer, TranslatedSelect translated) throws Exception {
long t1 = System.currentTimeMillis();
JdbcEventLogger logger = dataNode.getJdbcEventLogger();
- TranslatedSelect translated = dataNode.getSelectTranslator()
- .translate(query, dataNode.getAdapter(),
dataNode.getEntityResolver());
logger.logQuery(translated.sql(), translated.bindings());
diff --git a/cayenne/src/main/java/org/apache/cayenne/dba/AutoAdapter.java
b/cayenne/src/main/java/org/apache/cayenne/dba/AutoAdapter.java
index 9f270bd9c..b67f80022 100644
--- a/cayenne/src/main/java/org/apache/cayenne/dba/AutoAdapter.java
+++ b/cayenne/src/main/java/org/apache/cayenne/dba/AutoAdapter.java
@@ -152,6 +152,7 @@ public class AutoAdapter implements DbAdapter {
return getAdapter().supportsGeneratedKeysForBatchInserts();
}
+
@Override
public boolean supportsBatchUpdates() {
return getAdapter().supportsBatchUpdates();
diff --git
a/cayenne/src/main/java/org/apache/cayenne/dba/postgres/PostgresSelectAction.java
b/cayenne/src/main/java/org/apache/cayenne/dba/postgres/PostgresSelectAction.java
index 8e93faf9e..0edc106ad 100644
---
a/cayenne/src/main/java/org/apache/cayenne/dba/postgres/PostgresSelectAction.java
+++
b/cayenne/src/main/java/org/apache/cayenne/dba/postgres/PostgresSelectAction.java
@@ -19,9 +19,17 @@
package org.apache.cayenne.dba.postgres;
import org.apache.cayenne.access.DataNode;
+import org.apache.cayenne.access.OperationObserver;
+import org.apache.cayenne.access.jdbc.ColumnDescriptor;
import org.apache.cayenne.access.jdbc.SelectAction;
+import org.apache.cayenne.access.translator.ParameterBinding;
+import org.apache.cayenne.access.translator.select.TranslatedSelect;
import org.apache.cayenne.query.Select;
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.sql.Types;
+
/**
* @since 3.0
*/
@@ -35,4 +43,52 @@ class PostgresSelectAction extends SelectAction {
protected int getInMemoryOffset(int queryOffset) {
return 0;
}
+
+ @Override
+ protected void performAction(Connection connection, OperationObserver
observer, TranslatedSelect translated) throws Exception {
+
+ if (!connection.getAutoCommit() ||
!readsLargeObjects(translated)) {
+ super.performAction(connection, observer, translated);
+ return;
+ }
+
+ // manual tx management for reading LOBs
+ connection.setAutoCommit(false);
+ try {
+ super.performAction(connection, observer, translated);
+ connection.commit();
+ } catch (Exception e) {
+ try {
+ connection.rollback();
+ } catch (SQLException ignored) {
+ // connection is being returned/closed anyway
+ }
+ throw e;
+ } finally {
+ try {
+ connection.setAutoCommit(true);
+ } catch (SQLException ignored) {
+ // connection is being returned/closed anyway
+ }
+ }
+ }
+
+ private static boolean readsLargeObjects(TranslatedSelect translated) {
+ for (ColumnDescriptor column : translated.resultColumns()) {
+ if (isLargeObject(column.getJdbcType())) {
+ return true;
+ }
+ }
+ // a large object bound as a parameter (e.g. in a qualifier)
also needs a transaction
+ for (ParameterBinding binding : translated.bindings()) {
+ if (isLargeObject(binding.getJdbcType())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static boolean isLargeObject(int jdbcType) {
+ return jdbcType == Types.BLOB || jdbcType == Types.CLOB ||
jdbcType == Types.NCLOB;
+ }
}
diff --git a/cayenne/src/main/java/org/apache/cayenne/query/ObjectIdQuery.java
b/cayenne/src/main/java/org/apache/cayenne/query/ObjectIdQuery.java
index 9ba51d25f..ef7bf1134 100644
--- a/cayenne/src/main/java/org/apache/cayenne/query/ObjectIdQuery.java
+++ b/cayenne/src/main/java/org/apache/cayenne/query/ObjectIdQuery.java
@@ -105,6 +105,11 @@ public class ObjectIdQuery extends IndirectQuery {
public boolean isFetchingDataRows() {
return fetchingDataRows;
}
+
+ @Override
+ public boolean isReadOnly() {
+ return true;
+ }
};
this.metadataResolver = resolver;
diff --git
a/cayenne/src/main/java/org/apache/cayenne/query/ObjectSelectMetadata.java
b/cayenne/src/main/java/org/apache/cayenne/query/ObjectSelectMetadata.java
index 451480ea7..4d6c6dd87 100644
--- a/cayenne/src/main/java/org/apache/cayenne/query/ObjectSelectMetadata.java
+++ b/cayenne/src/main/java/org/apache/cayenne/query/ObjectSelectMetadata.java
@@ -39,6 +39,14 @@ class ObjectSelectMetadata extends BaseQueryMetadata {
protected Map<String, String> pathSplitAliases;
+ /**
+ * @since 5.0
+ */
+ @Override
+ public boolean isReadOnly() {
+ return true;
+ }
+
@Override
void copyFromInfo(QueryMetadata info) {
super.copyFromInfo(info);
diff --git a/cayenne/src/main/java/org/apache/cayenne/query/QueryMetadata.java
b/cayenne/src/main/java/org/apache/cayenne/query/QueryMetadata.java
index 412a104ac..262c24b85 100644
--- a/cayenne/src/main/java/org/apache/cayenne/query/QueryMetadata.java
+++ b/cayenne/src/main/java/org/apache/cayenne/query/QueryMetadata.java
@@ -264,4 +264,15 @@ public interface QueryMetadata {
default Function<?, ?> getResultMapper() {
return null;
}
+
+ /**
+ * Returns true if this is a read-only (selecting) query that does not
need to run in its own
+ * transaction. Defaults to false, so any query not explicitly marked
read-only is still wrapped
+ * in a transaction.
+ *
+ * @since 5.0
+ */
+ default boolean isReadOnly() {
+ return false;
+ }
}
diff --git
a/cayenne/src/main/java/org/apache/cayenne/query/QueryMetadataProxy.java
b/cayenne/src/main/java/org/apache/cayenne/query/QueryMetadataProxy.java
index b39b1776f..403ba1b6f 100644
--- a/cayenne/src/main/java/org/apache/cayenne/query/QueryMetadataProxy.java
+++ b/cayenne/src/main/java/org/apache/cayenne/query/QueryMetadataProxy.java
@@ -141,4 +141,9 @@ public class QueryMetadataProxy implements QueryMetadata {
public boolean isSuppressingDistinct() {
return mdDelegate.isSuppressingDistinct();
}
+
+ @Override
+ public boolean isReadOnly() {
+ return mdDelegate.isReadOnly();
+ }
}
diff --git
a/cayenne/src/main/java/org/apache/cayenne/query/RelationshipQuery.java
b/cayenne/src/main/java/org/apache/cayenne/query/RelationshipQuery.java
index bcb6bd6de..786086baf 100644
--- a/cayenne/src/main/java/org/apache/cayenne/query/RelationshipQuery.java
+++ b/cayenne/src/main/java/org/apache/cayenne/query/RelationshipQuery.java
@@ -173,6 +173,11 @@ public class RelationshipQuery extends IndirectQuery {
public int getStatementFetchSize() {
return statementFetchSize;
}
+
+ @Override
+ public boolean isReadOnly() {
+ return true;
+ }
};
this.metadataResolver = resolver;
diff --git a/cayenne/src/main/java/org/apache/cayenne/tx/BaseTransaction.java
b/cayenne/src/main/java/org/apache/cayenne/tx/BaseTransaction.java
index 989dc88d0..d4b8b2f30 100644
--- a/cayenne/src/main/java/org/apache/cayenne/tx/BaseTransaction.java
+++ b/cayenne/src/main/java/org/apache/cayenne/tx/BaseTransaction.java
@@ -19,6 +19,8 @@
package org.apache.cayenne.tx;
+import org.apache.cayenne.CayenneRuntimeException;
+
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
@@ -28,11 +30,7 @@ import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
-import org.apache.cayenne.CayenneRuntimeException;
-
/**
- * A Cayenne transaction. Currently supports managing JDBC connections.
- *
* @since 4.0
*/
public abstract class BaseTransaction implements Transaction {
diff --git
a/cayenne/src/main/java/org/apache/cayenne/tx/DefaultTransactionFactory.java
b/cayenne/src/main/java/org/apache/cayenne/tx/DefaultTransactionFactory.java
index 88fbd3acd..0b2646dc1 100644
--- a/cayenne/src/main/java/org/apache/cayenne/tx/DefaultTransactionFactory.java
+++ b/cayenne/src/main/java/org/apache/cayenne/tx/DefaultTransactionFactory.java
@@ -36,11 +36,6 @@ public class DefaultTransactionFactory implements
TransactionFactory {
this.jdbcEventLogger = jdbcEventLogger;
}
- @Override
- public Transaction createTransaction() {
- return createTransaction(TransactionDescriptor.defaultDescriptor());
- }
-
/**
* @since 4.1
*/
diff --git
a/cayenne/src/main/java/org/apache/cayenne/tx/DefaultTransactionManager.java
b/cayenne/src/main/java/org/apache/cayenne/tx/DefaultTransactionManager.java
index 3545ab815..6a8fc0005 100644
--- a/cayenne/src/main/java/org/apache/cayenne/tx/DefaultTransactionManager.java
+++ b/cayenne/src/main/java/org/apache/cayenne/tx/DefaultTransactionManager.java
@@ -58,8 +58,18 @@ public class DefaultTransactionManager implements
TransactionManager {
*/
@Override
public <T> T performInTransaction(TransactionalOperation<T> op,
TransactionListener callback, TransactionDescriptor descriptor) {
- BaseTransactionHandler handler = getHandler(descriptor);
- return handler.handle(op, callback, descriptor);
+ return getHandler(descriptor).handle(op, callback, descriptor);
+ }
+
+ /**
+ * @since 5.0
+ */
+ @Override
+ public <T> T performInTransaction(TransactionalOperation<T> op,
TransactionFactory transactionFactory) {
+ // TODO: feels kinda dirty to bypass the normal handler flow. Probably
an indicator that handlers need to be
+ // refactored
+ return new NestedTransactionHandler(transactionFactory,
jdbcEventLogger)
+ .handle(op, DoNothingTransactionListener.getInstance(),
TransactionDescriptor.defaultDescriptor());
}
protected BaseTransactionHandler getHandler(TransactionDescriptor
descriptor) {
@@ -84,7 +94,7 @@ public class DefaultTransactionManager implements
TransactionManager {
@Override
protected <T> T handle(TransactionalOperation<T> op,
TransactionListener callback, TransactionDescriptor descriptor) {
Transaction currentTx = BaseTransaction.getThreadTransaction();
- if(currentTx != null) {
+ if (currentTx != null) {
return performInTransaction(currentTx, op, callback);
} else {
return performInNewTransaction(op, callback, descriptor);
@@ -101,7 +111,7 @@ public class DefaultTransactionManager implements
TransactionManager {
@Override
protected <T> T handle(TransactionalOperation<T> op,
TransactionListener callback, TransactionDescriptor descriptor) {
Transaction currentTx = BaseTransaction.getThreadTransaction();
- if(currentTx == null) {
+ if (currentTx == null) {
throw new CayenneRuntimeException("Transaction operation
should join to existing transaction but none found.");
}
return performInTransaction(currentTx, op, callback);
@@ -120,7 +130,7 @@ public class DefaultTransactionManager implements
TransactionManager {
try {
return performInNewTransaction(op, callback, descriptor);
} finally {
- if(currentTx != null) {
+ if (currentTx != null) {
// restore old transaction, if where set
BaseTransaction.bindThreadTransaction(currentTx);
}
diff --git
a/cayenne/src/main/java/org/apache/cayenne/tx/ReadOnlyTransaction.java
b/cayenne/src/main/java/org/apache/cayenne/tx/ReadOnlyTransaction.java
new file mode 100644
index 000000000..950e63c5a
--- /dev/null
+++ b/cayenne/src/main/java/org/apache/cayenne/tx/ReadOnlyTransaction.java
@@ -0,0 +1,130 @@
+/*****************************************************************
+ * 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.tx;
+
+import javax.sql.DataSource;
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A minimal {@link Transaction} for read-only (selecting) queries that run
outside a managed
+ * transaction. It is not a real transaction - it exists only so that a
DataNode has a thread-bound
+ * transaction to acquire and release its connection through, and it keeps
that connection in
+ * autocommit mode (each select commits on its own, which is optimal for
reads). It never commits or
+ * rolls back, and fires no {@link TransactionListener} events.
+ * <p>
+ * Reads that require a real transaction (e.g. materializing PostgreSQL {@code
oid}/BLOB large
+ * objects) are not routed here - they use a {@link CayenneTransaction}.
+ *
+ * @since 5.0
+ */
+public class ReadOnlyTransaction implements Transaction {
+
+ private Map<String, Connection> connections;
+ private boolean rollbackOnly;
+
+ @Override
+ public Connection getOrCreateConnection(String connectionName, DataSource
dataSource) throws SQLException {
+ if (connections == null) {
+ connections = new HashMap<>();
+ }
+
+ Connection c = connections.get(connectionName);
+ if (c == null || c.isClosed()) {
+ c = createConnection(dataSource);
+ connections.put(connectionName, c);
+ }
+
+ // prevent callers from closing the connection - this transaction
closes it on commit/rollback
+ return new TransactionConnectionDecorator(c);
+ }
+
+ private Connection createConnection(DataSource dataSource) throws
SQLException {
+ Connection c = dataSource.getConnection();
+
+ // Auto commit saves time on explicit commit to DB, reducing the
overall load and increasing throughput.
+ // This is the main reason we have ReadOnlyTransaction.
+ if (!c.getAutoCommit()) {
+ c.setAutoCommit(true);
+ }
+
+ return c;
+ }
+
+ @Override
+ public void commit() {
+ closeConnections();
+ }
+
+ @Override
+ public void rollback() {
+ closeConnections();
+ }
+
+ private void closeConnections() {
+ if (connections == null) {
+ return;
+ }
+
+ for (Connection c : connections.values()) {
+ try {
+ c.close();
+ } catch (SQLException ignored) {
+ // returning the connection to the pool; nothing useful to do
on failure
+ }
+ }
+ connections = null;
+ }
+
+ @Override
+ public Map<String, Connection> getConnections() {
+ return connections != null ? Collections.unmodifiableMap(connections)
: Collections.emptyMap();
+ }
+
+ @Override
+ public void begin() {
+ // no real transaction is started
+ }
+
+ @Override
+ public void setRollbackOnly() {
+ // tracked so the TransactionManager closes the connection via
rollback() on the error path;
+ // there is no actual DB transaction to roll back
+ this.rollbackOnly = true;
+ }
+
+ @Override
+ public boolean isRollbackOnly() {
+ return rollbackOnly;
+ }
+
+ @Override
+ public void addListener(TransactionListener listener) {
+ // read-only queries intentionally fire no transaction events
+ }
+
+ @Override
+ public boolean isExternal() {
+ return false;
+ }
+}
diff --git
a/cayenne/src/main/java/org/apache/cayenne/tx/TransactionFactory.java
b/cayenne/src/main/java/org/apache/cayenne/tx/TransactionFactory.java
index 8e13af701..c064985bc 100644
--- a/cayenne/src/main/java/org/apache/cayenne/tx/TransactionFactory.java
+++ b/cayenne/src/main/java/org/apache/cayenne/tx/TransactionFactory.java
@@ -22,9 +22,12 @@ package org.apache.cayenne.tx;
/**
* @since 4.0
*/
+@FunctionalInterface
public interface TransactionFactory {
- Transaction createTransaction();
+ default Transaction createTransaction() {
+ return createTransaction(TransactionDescriptor.defaultDescriptor());
+ }
/**
*
diff --git
a/cayenne/src/main/java/org/apache/cayenne/tx/TransactionManager.java
b/cayenne/src/main/java/org/apache/cayenne/tx/TransactionManager.java
index 355743b40..ff1ca8262 100644
--- a/cayenne/src/main/java/org/apache/cayenne/tx/TransactionManager.java
+++ b/cayenne/src/main/java/org/apache/cayenne/tx/TransactionManager.java
@@ -78,4 +78,19 @@ public interface TransactionManager {
* @since 4.1
*/
<T> T performInTransaction(TransactionalOperation<T> op,
TransactionListener callback, TransactionDescriptor descriptor);
+
+ /**
+ * Performs an operation in a transaction created by the supplied factory,
instead of the
+ * configured one. This lets callers run an operation in a special
transaction, such as a
+ * {@link ReadOnlyTransaction}. Behaves like {@link
TransactionPropagation#NESTED} - joining an
+ * existing transaction if one is bound, otherwise creating a new one from
the given factory.
+ *
+ * @param op an operation to perform within the
transaction.
+ * @param transactionFactory a factory for the transaction to run the
operation in.
+ * @param <T> returned value type
+ * @return a value returned by the "op" operation.
+ *
+ * @since 5.0
+ */
+ <T> T performInTransaction(TransactionalOperation<T> op,
TransactionFactory transactionFactory);
}
diff --git
a/cayenne/src/main/java/org/apache/cayenne/tx/TransactionPropagation.java
b/cayenne/src/main/java/org/apache/cayenne/tx/TransactionPropagation.java
index 18aabdf1f..c49ef6f3d 100644
--- a/cayenne/src/main/java/org/apache/cayenne/tx/TransactionPropagation.java
+++ b/cayenne/src/main/java/org/apache/cayenne/tx/TransactionPropagation.java
@@ -20,17 +20,17 @@
package org.apache.cayenne.tx;
/**
- * Propagation behaviour of transaction
+ * Propagation behavior of transaction
*/
public enum TransactionPropagation {
+
/**
* Support a current transaction, throw an exception if none exists.
*/
MANDATORY,
/**
- * Execute within a nested transaction if a current transaction exists,
- * create a new one if none exists.
+ * Execute within a nested transaction if a current transaction exists,
create a new one if none exists.
*/
NESTED,
diff --git
a/cayenne/src/test/java/org/apache/cayenne/access/DataContextBlobIT.java
b/cayenne/src/test/java/org/apache/cayenne/access/DataContextBlobIT.java
index baa11815c..9efe6359f 100644
--- a/cayenne/src/test/java/org/apache/cayenne/access/DataContextBlobIT.java
+++ b/cayenne/src/test/java/org/apache/cayenne/access/DataContextBlobIT.java
@@ -19,8 +19,6 @@
package org.apache.cayenne.access;
-import java.util.List;
-
import org.apache.cayenne.access.types.ByteArrayTypeTest;
import org.apache.cayenne.query.ObjectSelect;
import org.apache.cayenne.testdo.lob.BlobTestEntity;
@@ -30,8 +28,9 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNull;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
public class DataContextBlobIT {
@@ -115,6 +114,24 @@ public class DataContextBlobIT {
runWithBlobSize(1024 * 1024);
}
+ @Test
+ public void selectWithBlobInQualifier() {
+ if (skipTests()) {
+ return;
+ }
+
+ byte[] bytes = {'a', 'b', 'c', 'd'};
+ BlobTestEntity blob = context.newObject(BlobTestEntity.class);
+ blob.setBlobCol(bytes);
+ context.commitChanges();
+
+ // TODO: change this to a real query one PostgreSQLAdapter start
supporting LOB-based criteria (that will require
+ // a special SQL syntax).
+ assertDoesNotThrow(() -> ObjectSelect.query(BlobTestEntity.class)
+ .where(BlobTestEntity.BLOB_COL.eq(bytes))
+ .selectCount(context2));
+ }
+
@Test
public void nullBlob() throws Exception {
if (skipTests()) {
diff --git
a/cayenne/src/test/java/org/apache/cayenne/query/ObjectIdQueryTest.java
b/cayenne/src/test/java/org/apache/cayenne/query/ObjectIdQueryTest.java
index 8f594adf4..2443e9a34 100644
--- a/cayenne/src/test/java/org/apache/cayenne/query/ObjectIdQueryTest.java
+++ b/cayenne/src/test/java/org/apache/cayenne/query/ObjectIdQueryTest.java
@@ -94,4 +94,9 @@ public class ObjectIdQueryTest {
QueryMetadata md3 = q3.getMetaData(null);
assertFalse(md3.isFetchingDataRows());
}
+
+ @Test
+ public void readOnlyMetadata() {
+ assertTrue(new ObjectIdQuery(ObjectId.of("abc", "a",
1)).getMetaData(null).isReadOnly());
+ }
}
diff --git
a/cayenne/src/test/java/org/apache/cayenne/query/ObjectSelectTest.java
b/cayenne/src/test/java/org/apache/cayenne/query/ObjectSelectTest.java
index bac7ccc3f..2808b9d27 100644
--- a/cayenne/src/test/java/org/apache/cayenne/query/ObjectSelectTest.java
+++ b/cayenne/src/test/java/org/apache/cayenne/query/ObjectSelectTest.java
@@ -25,10 +25,12 @@ import java.util.Collections;
import org.apache.cayenne.DataRow;
import org.apache.cayenne.exp.Expression;
import org.apache.cayenne.exp.ExpressionFactory;
+import org.apache.cayenne.map.EntityResolver;
import org.apache.cayenne.testdo.testmap.Artist;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.mock;
public class ObjectSelectTest {
@@ -435,4 +437,9 @@ public class ObjectSelectTest {
assertEquals(2, query.getQueryTimeout());
}
+ @Test
+ public void readOnlyMetadata() {
+
assertTrue(ObjectSelect.query(Artist.class).getMetaData(mock(EntityResolver.class)).isReadOnly());
+ }
+
}
diff --git a/cayenne/src/test/java/org/apache/cayenne/query/SQLSelectTest.java
b/cayenne/src/test/java/org/apache/cayenne/query/SQLSelectTest.java
index 646f2e982..027e44d2c 100644
--- a/cayenne/src/test/java/org/apache/cayenne/query/SQLSelectTest.java
+++ b/cayenne/src/test/java/org/apache/cayenne/query/SQLSelectTest.java
@@ -119,4 +119,10 @@ public class SQLSelectTest {
assertNotEquals(md1.getCacheKey(), md2.getCacheKey());
assertNotEquals(md3.getCacheKey(), md2.getCacheKey());
}
+
+ @Test
+ public void notReadOnlyMetadata() {
+ // raw SQL is opaque, so SQLSelect keeps the regular transaction
+ assertFalse(SQLSelect.dataRowQuery("SELECT * FROM
X").getMetaData(mock(EntityResolver.class)).isReadOnly());
+ }
}
diff --git
a/cayenne/src/test/java/org/apache/cayenne/query/SQLTemplateTest.java
b/cayenne/src/test/java/org/apache/cayenne/query/SQLTemplateTest.java
index 2b5969819..95d1212e7 100644
--- a/cayenne/src/test/java/org/apache/cayenne/query/SQLTemplateTest.java
+++ b/cayenne/src/test/java/org/apache/cayenne/query/SQLTemplateTest.java
@@ -19,16 +19,19 @@
package org.apache.cayenne.query;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotSame;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
+import org.apache.cayenne.map.EntityResolver;
import org.apache.cayenne.util.Util;
import org.junit.jupiter.api.Test;
@@ -134,4 +137,9 @@ public class SQLTemplateTest {
assertEquals(o.getRoot(), c1.getRoot());
assertEquals(o.getDefaultTemplate(), c1.getDefaultTemplate());
}
+
+ @Test
+ public void notReadOnlyMetadata() {
+ assertFalse(new SQLTemplate("E1",
"SELECT").getMetaData(mock(EntityResolver.class)).isReadOnly());
+ }
}
diff --git
a/cayenne/src/test/java/org/apache/cayenne/tx/ReadOnlyTransactionTest.java
b/cayenne/src/test/java/org/apache/cayenne/tx/ReadOnlyTransactionTest.java
new file mode 100644
index 000000000..7d4fbcddd
--- /dev/null
+++ b/cayenne/src/test/java/org/apache/cayenne/tx/ReadOnlyTransactionTest.java
@@ -0,0 +1,86 @@
+/*****************************************************************
+ * 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.tx;
+
+import java.sql.Connection;
+
+import javax.sql.DataSource;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class ReadOnlyTransactionTest {
+
+ @Test
+ public void notExternal() {
+ assertFalse(new ReadOnlyTransaction().isExternal());
+ }
+
+ @Test
+ public void addListenerIsNoOp() {
+ // read-only queries must not fire transaction events
+ new ReadOnlyTransaction().addListener(mock(TransactionListener.class));
+ }
+
+ @Test
+ public void ensuresAutoCommitAndDoesNotCloseEarly() throws Exception {
+ Connection connection = mock(Connection.class);
+ when(connection.getAutoCommit()).thenReturn(false);
+ DataSource dataSource = mock(DataSource.class);
+ when(dataSource.getConnection()).thenReturn(connection);
+
+ ReadOnlyTransaction tx = new ReadOnlyTransaction();
+ Connection handed = tx.getOrCreateConnection("node", dataSource);
+
+ // reads run in autocommit mode
+ verify(connection, times(1)).setAutoCommit(true);
+
+ // the handed-out connection is a decorator that does not close the
real connection
+ assertNotSame(connection, handed);
+ handed.close();
+ verify(connection, never()).close();
+ }
+
+ @Test
+ public void commitClosesWithoutCommittingOrRollingBack() throws Exception {
+ Connection connection = mock(Connection.class);
+ when(connection.getAutoCommit()).thenReturn(true);
+ DataSource dataSource = mock(DataSource.class);
+ when(dataSource.getConnection()).thenReturn(connection);
+
+ ReadOnlyTransaction tx = new ReadOnlyTransaction();
+ tx.getOrCreateConnection("node", dataSource);
+ tx.commit();
+
+ // a read-only transaction never commits, rolls back, or disables
autocommit; it just
+ // returns the connection to the pool by closing it
+ verify(connection, never()).commit();
+ verify(connection, never()).rollback();
+ verify(connection, never()).setAutoCommit(false);
+ verify(connection, times(1)).close();
+ }
+}