This is an automated email from the ASF dual-hosted git repository. ntimofeev pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/cayenne.git
commit d740e466c671ef8c3b70b908a77ccead5bfb3d26 Merge: 27f9d35 05eb42e Author: Nikita Timofeev <[email protected]> AuthorDate: Wed Jun 2 18:15:56 2021 +0300 Merge PR #460 CAY-2637 Allow forcing a custom Connection for a transaction .../org/apache/cayenne/tx/BaseTransaction.java | 40 +++- .../apache/cayenne/tx/TransactionDescriptor.java | 128 ++++++++++-- .../org/apache/cayenne/tx/TransactionListener.java | 16 ++ .../configuration/server/ServerRuntimeIT.java | 32 ++- .../cayenne/tx/DefaultTransactionManagerIT.java | 22 +- .../cayenne/tx/TransactionCustomConnectionIT.java | 226 +++++++++++++++++++++ .../apache/cayenne/tx/TransactionIsolationIT.java | 8 +- .../tx/TransactionPropagationRollbackIT.java | 24 +-- 8 files changed, 438 insertions(+), 58 deletions(-) diff --cc cayenne-server/src/main/java/org/apache/cayenne/tx/BaseTransaction.java index a83990b,e3215b5..989dc88 --- a/cayenne-server/src/main/java/org/apache/cayenne/tx/BaseTransaction.java +++ b/cayenne-server/src/main/java/org/apache/cayenne/tx/BaseTransaction.java @@@ -204,7 -200,10 +204,11 @@@ public abstract class BaseTransaction i Connection c = getExistingConnection(connectionName); if (c == null || c.isClosed()) { - c = dataSource.getConnection(); - if(descriptor.getCustomConnectionSupplier() != null) - c = descriptor.getCustomConnectionSupplier().get(); - else ++ if(descriptor.getConnectionSupplier() != null) { ++ c = descriptor.getConnectionSupplier().get(); ++ } else { + c = dataSource.getConnection(); ++ } addConnection(connectionName, c); } @@@ -241,6 -239,9 +244,10 @@@ connections = new HashMap<>(); } - if (wrapper == null) ++ if (wrapper == null) { + wrapper = new TransactionConnectionDecorator(connection); ++ } + if (connections.put(connectionName, wrapper) != wrapper) { connectionAdded(connection); } diff --cc cayenne-server/src/main/java/org/apache/cayenne/tx/TransactionDescriptor.java index 7dd60ce,3ced787..734132a --- a/cayenne-server/src/main/java/org/apache/cayenne/tx/TransactionDescriptor.java +++ b/cayenne-server/src/main/java/org/apache/cayenne/tx/TransactionDescriptor.java @@@ -19,10 -19,12 +19,19 @@@ package org.apache.cayenne.tx; + import java.sql.Connection; + import java.util.function.Supplier; + /** - * -- * Descriptor that provide desired transaction isolation level and propagation logic. -- * ++ * Descriptor that allows to customize transaction logic. ++ * It provides following options: ++ * <ul> ++ * <li> transaction isolation level ++ * <li> transaction propagation logic. ++ * <li> custom connection to use in a transaction ++ * </ul> ++ * @see TransactionManager#performInTransaction(TransactionalOperation, TransactionDescriptor) ++ * @see org.apache.cayenne.configuration.server.ServerRuntime#performInTransaction(TransactionalOperation, TransactionDescriptor) * @since 4.1 */ public class TransactionDescriptor { @@@ -32,22 -34,27 +41,27 @@@ */ public static final int ISOLATION_DEFAULT = -1; - private final int isolation; + private int isolation; + + private TransactionPropagation propagation; - private final TransactionPropagation propagation; - private Supplier<Connection> customConnectionSupplier; ++ private Supplier<Connection> connectionSupplier; + + private TransactionDescriptor() { + } /** - * @param isolation one of the following <code>Connection</code> constants: - * <code>Connection.TRANSACTION_READ_UNCOMMITTED</code>, - * <code>Connection.TRANSACTION_READ_COMMITTED</code>, - * <code>Connection.TRANSACTION_REPEATABLE_READ</code>, - * <code>Connection.TRANSACTION_SERIALIZABLE</code>, or - * <code>TransactionDescriptor.ISOLATION_DEFAULT</code> - * + * @param isolation one of the following <code>Connection</code> constants: + * <code>Connection.TRANSACTION_READ_UNCOMMITTED</code>, + * <code>Connection.TRANSACTION_READ_COMMITTED</code>, + * <code>Connection.TRANSACTION_REPEATABLE_READ</code>, + * <code>Connection.TRANSACTION_SERIALIZABLE</code>, or + * <code>TransactionDescriptor.ISOLATION_DEFAULT</code> * @param propagation transaction propagation behaviour - * * @see TransactionPropagation - * @deprecated since 4.2. Use builder instead ++ * @deprecated since 4.2. Use {@link #builder()} method instead. */ + @Deprecated public TransactionDescriptor(int isolation, TransactionPropagation propagation) { this.isolation = isolation; this.propagation = propagation; @@@ -57,22 -64,23 +71,24 @@@ * Create transaction descriptor with desired isolation level and <code>NESTED</code> propagation * * @param isolation one of the following <code>Connection</code> constants: - * <code>Connection.TRANSACTION_READ_UNCOMMITTED</code>, - * <code>Connection.TRANSACTION_READ_COMMITTED</code>, - * <code>Connection.TRANSACTION_REPEATABLE_READ</code>, - * <code>Connection.TRANSACTION_SERIALIZABLE</code>, or - * <code>TransactionDescriptor.ISOLATION_DEFAULT</code> + * <code>Connection.TRANSACTION_READ_UNCOMMITTED</code>, + * <code>Connection.TRANSACTION_READ_COMMITTED</code>, + * <code>Connection.TRANSACTION_REPEATABLE_READ</code>, + * <code>Connection.TRANSACTION_SERIALIZABLE</code>, or + * <code>TransactionDescriptor.ISOLATION_DEFAULT</code> ++ * @deprecated since 4.2. Use {@link #builder()} method instead. */ + @Deprecated public TransactionDescriptor(int isolation) { this(isolation, TransactionPropagation.NESTED); } /** - * * @param propagation transaction propagation behaviour * @see TransactionPropagation - * @deprecated since 4.2. Use builder instead ++ * @deprecated since 4.2. Use {@link #builder()} method instead. */ + @Deprecated public TransactionDescriptor(TransactionPropagation propagation) { this(ISOLATION_DEFAULT, propagation); } @@@ -90,4 -98,71 +106,76 @@@ public TransactionPropagation getPropagation() { return propagation; } + + /** + * @return custom connection supplier, passed by user + */ - public Supplier<Connection> getCustomConnectionSupplier() { - return customConnectionSupplier; ++ public Supplier<Connection> getConnectionSupplier() { ++ return connectionSupplier; + } + + public static Builder builder(){ + return new Builder(); + } + + /** - * Builder class for TransactionDescriptor. - * @since 4.2 ++ * Builder class for the TransactionDescriptor. + */ + public static class Builder { + private final TransactionDescriptor transactionDescriptor = new TransactionDescriptor(); + - private Builder(){} ++ private Builder(){ ++ } + + /** + * @param isolation one of the following <code>Connection</code> constants: + * <code>Connection.TRANSACTION_READ_UNCOMMITTED</code>, + * <code>Connection.TRANSACTION_READ_COMMITTED</code>, + * <code>Connection.TRANSACTION_REPEATABLE_READ</code>, + * <code>Connection.TRANSACTION_SERIALIZABLE</code>, or + * <code>TransactionDescriptor.ISOLATION_DEFAULT</code> + */ + public Builder isolation(int isolation) { + transactionDescriptor.isolation = isolation; + return this; + } + + /** ++ * A custom connection provided by the TransactionDescriptor will be used ++ * instead of any other connection provided by tbe connection pool. ++ * + * @param connection custom connection - * @see Connection ++ * @see #connectionSupplier(Supplier) + */ - public Builder connectionSupplier(Connection connection) { - transactionDescriptor.customConnectionSupplier = () -> connection; ++ public Builder connection(Connection connection) { ++ transactionDescriptor.connectionSupplier = () -> connection; + return this; + } + + /** ++ * A custom connection provided by the TransactionDescriptor will be used ++ * instead of any other connection provided by tbe connection pool. ++ * + * @param connectionSupplier custom connection supplier - * @see Connection - * @see Supplier ++ * @see #connection(Connection) + */ + public Builder connectionSupplier(Supplier<Connection> connectionSupplier){ - transactionDescriptor.customConnectionSupplier = connectionSupplier; ++ transactionDescriptor.connectionSupplier = connectionSupplier; + return this; + } + + /** + * @param propagation transaction propagation behaviour + * @see TransactionPropagation + */ + public Builder propagation(TransactionPropagation propagation) { + transactionDescriptor.propagation = propagation; + return this; + } + + public TransactionDescriptor build() { + return transactionDescriptor; + } + } + } diff --cc cayenne-server/src/main/java/org/apache/cayenne/tx/TransactionListener.java index dc230e1,b891f6b..14b55ad --- a/cayenne-server/src/main/java/org/apache/cayenne/tx/TransactionListener.java +++ b/cayenne-server/src/main/java/org/apache/cayenne/tx/TransactionListener.java @@@ -33,4 -33,8 +33,20 @@@ public interface TransactionListener void willRollback(Transaction tx); void willAddConnection(Transaction tx, String connectionName, Connection connection); + ++ /** ++ * This method could be used to decorate or substitute ++ * new connection initiated inside a Cayenne transaction. ++ * <br/> ++ * The default implementation returns the same connection. ++ * ++ * @param tx transaction that initiated connection ++ * @param connection connection (it could be decorated by other listeners) ++ * @return connection ++ * ++ * @since 4.2 ++ */ + default Connection decorateConnection(Transaction tx, Connection connection){ + return connection; + } } diff --cc cayenne-server/src/test/java/org/apache/cayenne/tx/TransactionCustomConnectionIT.java index 0000000,90f2397..7c73a38 mode 000000,100644..100644 --- a/cayenne-server/src/test/java/org/apache/cayenne/tx/TransactionCustomConnectionIT.java +++ b/cayenne-server/src/test/java/org/apache/cayenne/tx/TransactionCustomConnectionIT.java @@@ -1,0 -1,226 +1,226 @@@ + /***************************************************************** + * 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 org.apache.cayenne.access.DataContext; + import org.apache.cayenne.configuration.server.ServerRuntime; + import org.apache.cayenne.di.Inject; + import org.apache.cayenne.log.JdbcEventLogger; + import org.apache.cayenne.query.ObjectSelect; + import org.apache.cayenne.testdo.testmap.Artist; + import org.apache.cayenne.unit.di.server.CayenneProjects; + import org.apache.cayenne.unit.di.server.ServerCase; + import org.apache.cayenne.unit.di.server.UseServerRuntime; + import org.junit.Before; + import org.junit.Test; + import org.slf4j.Logger; + import org.slf4j.LoggerFactory; + + import java.sql.Connection; + import java.sql.SQLException; + import java.util.ArrayList; + import java.util.List; + + import static org.junit.Assert.*; + import static org.mockito.ArgumentMatchers.any; + import static org.mockito.Mockito.*; + + /** + * Check if connection can be decorated with listeners and that default connection of TransactionDescriptor + * has major priority + * + * @see BaseTransaction,TransactionDescriptor + * @since 4.2 + */ + @UseServerRuntime(CayenneProjects.TESTMAP_PROJECT) + public class TransactionCustomConnectionIT extends ServerCase { + + private final Logger logger = LoggerFactory.getLogger(TransactionIsolationIT.class); + + @Inject + DataContext context; + + @Inject + ServerRuntime runtime; + + @Inject + private JdbcEventLogger jdbcEventLogger; + + TransactionManager manager; + private static boolean firstReadonlyCondition; + + @Before + public void initTransactionManager() { + // no binding in test container, get it from runtime + manager = runtime.getInjector().getInstance(TransactionManager.class); + } + + /** + * Test depends on decoration of readonly property of the connection, but not every driver supports readonly setting. + * So this test calculated for drivers with current support, but if it is not supported, this test mustn't fail + * because it checks if readonly wasn't changed by setter and changes firstReadonlyCondition flag value + * if it is true to avoid fails. In that case test is useless, but it's behavior in other cases can submit + * right behavior of methods + */ + @Test + public void testConnectionDecorationWithListeners() { + Transaction t = new CayenneTransaction(jdbcEventLogger); + //add listeners which will check if connection object will be changed after every decorate call + List<TransactionListener> listeners = addAndGetListenersWithCustomReadonlyTo(t); + BaseTransaction.bindThreadTransaction(t); + try { + ObjectSelect.query(Artist.class).select(context); + + //check if the last listener set readonly property to false + t.getConnections().forEach((key, connection) -> { + try { + assertEquals(connection.isReadOnly(), firstReadonlyCondition); + } catch (SQLException throwables) { + throwables.printStackTrace(); + } + }); + + //check if every decoration from listener was called + for (TransactionListener transactionListener : listeners) { + verify(transactionListener).decorateConnection(any(), any()); + } + } finally { + BaseTransaction.bindThreadTransaction(null); + t.commit(); + } + } + + private List<TransactionListener> addAndGetListenersWithCustomReadonlyTo(Transaction t) { + Class<?>[] classes = new Class[]{ListenerWithFirstReadonlyDecorator.class, ListenerWithSecondReadonlyDecorator.class}; + List<TransactionListener> listeners = new ArrayList<>(); + for (Class<?> aClass : classes) { + TransactionListener listener = (TransactionListener) mock(aClass); + listeners.add(listener); + when(listener.decorateConnection(any(), any())).thenCallRealMethod(); + t.addListener(listener); + } + return listeners; + } + + //listener, which will check if readonly property of connection is false and set it to true + class ListenerWithFirstReadonlyDecorator implements TransactionListener { + + @Override + public void willCommit(Transaction tx) { + + } + + @Override + public void willRollback(Transaction tx) { + + } + + @Override + public void willAddConnection(Transaction tx, String connectionName, Connection connection) { + + } + + @Override + public Connection decorateConnection(Transaction tx, Connection connection) { + try { + firstReadonlyCondition = connection.isReadOnly(); + connection.setReadOnly(!firstReadonlyCondition); + + if (connection.isReadOnly() == firstReadonlyCondition) { + firstReadonlyCondition = !connection.isReadOnly(); + } + } catch (SQLException throwables) { + throwables.printStackTrace(); + } + return connection; + } + } + + //listener, which will check if readonly property of connection is true and set it to false + class ListenerWithSecondReadonlyDecorator implements TransactionListener { + + @Override + public void willCommit(Transaction tx) { + + } + + @Override + public void willRollback(Transaction tx) { + + } + + @Override + public void willAddConnection(Transaction tx, String connectionName, Connection connection) { + + } + + @Override + public Connection decorateConnection(Transaction tx, Connection connection) { + try { + assertEquals(!firstReadonlyCondition, connection.isReadOnly()); + connection.setReadOnly(!connection.isReadOnly()); + if (connection.isReadOnly() == !firstReadonlyCondition) { + firstReadonlyCondition = !firstReadonlyCondition; + } + } catch (SQLException throwables) { + throwables.printStackTrace(); + } + return connection; + } + } + + @Test + public void testDefaultConnectionInDescriptor() { + Transaction t = new CayenneTransaction(jdbcEventLogger); + BaseTransaction.bindThreadTransaction(t); + try { + ObjectSelect.query(Artist.class).select(context); + Connection connection = t.getConnections().values().stream().findFirst().get(); + + try { + connection.setAutoCommit(true); + } catch (SQLException throwables) { + throwables.printStackTrace(); + } + + TransactionDescriptor mockDescriptor = mock(TransactionDescriptor.class); - when(mockDescriptor.getCustomConnectionSupplier()).thenReturn(() -> connection); ++ when(mockDescriptor.getConnectionSupplier()).thenReturn(() -> connection); + when(mockDescriptor.getPropagation()).thenReturn(TransactionPropagation.REQUIRES_NEW); + when(mockDescriptor.getIsolation()).thenReturn(Connection.TRANSACTION_SERIALIZABLE); + + performInTransaction(mockDescriptor); - verify(mockDescriptor, times(2)).getCustomConnectionSupplier(); ++ verify(mockDescriptor, times(2)).getConnectionSupplier(); + } finally { + BaseTransaction.bindThreadTransaction(null); + t.commit(); + } + + } + + private void performInTransaction(TransactionDescriptor descriptor) { + Artist artist = context.newObject(Artist.class); + artist.setArtistName("test"); + manager.performInTransaction(() -> { + artist.setArtistName("test3"); + context.commitChanges(); + return null; + }, descriptor); + } + }
