From ea3fabfca9c90f2d24677134156cd7a20e5536c9 Mon Sep 17 00:00:00 2001 From: Arne Burmeister Date: Wed, 11 May 2022 08:53:11 +0200 Subject: [PATCH 01/55] #80 create transaction manager --- .../query/QueryTransactionBridge.java | 30 ++++++ .../transaction/ArangoTransaction.java | 71 ++++++++++++++ ...ArangoTransactionManagementConfigurer.java | 24 +++++ .../transaction/ArangoTransactionManager.java | 80 +++++++++++++++ .../query/QueryTransactionBridgeTest.java | 30 ++++++ .../ArangoTransactionManagerTest.java | 97 +++++++++++++++++++ 6 files changed, 332 insertions(+) create mode 100644 src/main/java/com/arangodb/springframework/repository/query/QueryTransactionBridge.java create mode 100644 src/main/java/com/arangodb/springframework/transaction/ArangoTransaction.java create mode 100644 src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManagementConfigurer.java create mode 100644 src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManager.java create mode 100644 src/test/java/com/arangodb/springframework/repository/query/QueryTransactionBridgeTest.java create mode 100644 src/test/java/com/arangodb/springframework/transaction/ArangoTransactionManagerTest.java diff --git a/src/main/java/com/arangodb/springframework/repository/query/QueryTransactionBridge.java b/src/main/java/com/arangodb/springframework/repository/query/QueryTransactionBridge.java new file mode 100644 index 000000000..b65808ce9 --- /dev/null +++ b/src/main/java/com/arangodb/springframework/repository/query/QueryTransactionBridge.java @@ -0,0 +1,30 @@ +package com.arangodb.springframework.repository.query; + +import org.springframework.core.NamedInheritableThreadLocal; + +import java.util.Collection; +import java.util.function.Function; + +/** + * Bridge to postpone late transaction start to be able to inject collections from query side. + */ +public class QueryTransactionBridge { + private static final Function, String> NO_TRANSACTION = any -> null; + private static final ThreadLocal, String>> CURRENT_TRANSACTION_BEGIN = new NamedInheritableThreadLocal<>("ArangoTransactionBegin"); + + public QueryTransactionBridge() { + CURRENT_TRANSACTION_BEGIN.set(NO_TRANSACTION); + } + + public void setCurrentTransactionBegin(Function, String> begin) { + CURRENT_TRANSACTION_BEGIN.set(begin); + } + + public void clearCurrentTransactionBegin() { + CURRENT_TRANSACTION_BEGIN.set(NO_TRANSACTION); + } + + public String beginCurrentTransaction(Collection collections) { + return CURRENT_TRANSACTION_BEGIN.get().apply(collections); + } +} diff --git a/src/main/java/com/arangodb/springframework/transaction/ArangoTransaction.java b/src/main/java/com/arangodb/springframework/transaction/ArangoTransaction.java new file mode 100644 index 000000000..baed9ad08 --- /dev/null +++ b/src/main/java/com/arangodb/springframework/transaction/ArangoTransaction.java @@ -0,0 +1,71 @@ +package com.arangodb.springframework.transaction; + +import com.arangodb.ArangoDatabase; +import com.arangodb.entity.StreamTransactionEntity; +import com.arangodb.entity.StreamTransactionStatus; +import com.arangodb.model.StreamTransactionOptions; +import org.springframework.transaction.IllegalTransactionStateException; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.interceptor.TransactionAttribute; +import org.springframework.transaction.support.SmartTransactionObject; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +class ArangoTransaction implements SmartTransactionObject { + + private final ArangoDatabase database; + private TransactionDefinition definition; + private StreamTransactionEntity transaction; + + ArangoTransaction(ArangoDatabase database) { + this.database = database; + } + + boolean exists() { + return transaction != null; + } + + void configure(TransactionDefinition definition) { + this.definition = definition; + } + + String begin(Collection collections) { + if (transaction != null) { + throw new IllegalTransactionStateException("Stream transaction already started"); + } + Set allCollections = new HashSet<>(collections); + if (definition instanceof TransactionAttribute) { + allCollections.addAll(((TransactionAttribute) definition).getLabels()); + } + StreamTransactionOptions options = new StreamTransactionOptions().allowImplicit(true) + .writeCollections(allCollections.toArray(new String[0])) + .lockTimeout(definition.getTimeout() == -1 ? 0 : definition.getTimeout()); + transaction = database.beginStreamTransaction(options); + return transaction.getId(); + } + + void commit() { + database.commitStreamTransaction(transaction.getId()); + } + + void rollback() { + database.abortStreamTransaction(transaction.getId()); + } + + @Override + public boolean isRollbackOnly() { + return transaction != null && transaction.getStatus() == StreamTransactionStatus.aborted; + } + + @Override + public void flush() { + // nothing to do + } + + @Override + public String toString() { + return transaction == null ? "(not begun)" : transaction.getId(); + } +} diff --git a/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManagementConfigurer.java b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManagementConfigurer.java new file mode 100644 index 000000000..b0dad3073 --- /dev/null +++ b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManagementConfigurer.java @@ -0,0 +1,24 @@ +package com.arangodb.springframework.transaction; + +import com.arangodb.springframework.core.ArangoOperations; +import com.arangodb.springframework.repository.query.QueryTransactionBridge; +import org.springframework.context.annotation.Bean; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.annotation.TransactionManagementConfigurer; + +public class ArangoTransactionManagementConfigurer implements TransactionManagementConfigurer { + + private final ArangoOperations operations; + private final QueryTransactionBridge bridge; + + public ArangoTransactionManagementConfigurer(ArangoOperations operations, QueryTransactionBridge bridge) { + this.operations = operations; + this.bridge = bridge; + } + + @Override + @Bean + public PlatformTransactionManager annotationDrivenTransactionManager() { + return new ArangoTransactionManager(operations, bridge); + } +} diff --git a/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManager.java b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManager.java new file mode 100644 index 000000000..6295b47fe --- /dev/null +++ b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManager.java @@ -0,0 +1,80 @@ +package com.arangodb.springframework.transaction; + +import com.arangodb.ArangoDatabase; +import com.arangodb.model.StreamTransactionOptions; +import com.arangodb.springframework.core.ArangoOperations; +import com.arangodb.springframework.repository.query.QueryTransactionBridge; +import org.springframework.transaction.InvalidIsolationLevelException; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionException; +import org.springframework.transaction.support.AbstractPlatformTransactionManager; +import org.springframework.transaction.support.DefaultTransactionStatus; + +import java.util.Collection; +import java.util.function.Function; + +/** + * Transaction manager using ArangoDB stream transactions on the + * {@linkplain ArangoOperations#getDatabaseName()} current database} of the template. + * Isolation level {@linkplain TransactionDefinition#ISOLATION_SERIALIZABLE serializable} is not supported. + * + * @see ArangoDatabase#beginStreamTransaction(StreamTransactionOptions) + */ +public class ArangoTransactionManager extends AbstractPlatformTransactionManager { + + private final ArangoOperations operations; + private final QueryTransactionBridge bridge; + + public ArangoTransactionManager(ArangoOperations operations, QueryTransactionBridge bridge) { + this.operations = operations; + this.bridge = bridge; + } + + @Override + protected Object doGetTransaction() throws TransactionException { + return new ArangoTransaction(operations.driver().db(operations.getDatabaseName())); + } + + @Override + protected void doBegin(Object transaction, TransactionDefinition definition) throws InvalidIsolationLevelException { + int isolationLevel = definition.getIsolationLevel(); + if (isolationLevel != -1 && (isolationLevel & TransactionDefinition.ISOLATION_SERIALIZABLE) != 0) { + throw new InvalidIsolationLevelException("ArangoDB does not support isolation level serializable"); + } + ArangoTransaction tx = (ArangoTransaction) transaction; + tx.configure(definition); + Function, String> begin = tx::begin; + bridge.setCurrentTransactionBegin(begin.andThen(id -> { + if (logger.isDebugEnabled()) { + logger.debug("Began stream transaction " + id); + } + return id; + })); + } + + @Override + protected void doCommit(DefaultTransactionStatus status) throws TransactionException { + ArangoTransaction tx = (ArangoTransaction) status.getTransaction(); + if (logger.isDebugEnabled()) { + logger.debug("Commit stream transaction " + tx); + } + tx.commit(); + bridge.clearCurrentTransactionBegin(); + } + + @Override + protected void doRollback(DefaultTransactionStatus status) throws TransactionException { + ArangoTransaction tx = (ArangoTransaction) status.getTransaction(); + if (logger.isDebugEnabled()) { + logger.debug("Rollback stream transaction " + tx); + } + tx.rollback(); + bridge.clearCurrentTransactionBegin(); + } + + @Override + protected boolean isExistingTransaction(Object transaction) throws TransactionException { + return transaction instanceof ArangoTransaction + && ((ArangoTransaction) transaction).exists(); + } +} diff --git a/src/test/java/com/arangodb/springframework/repository/query/QueryTransactionBridgeTest.java b/src/test/java/com/arangodb/springframework/repository/query/QueryTransactionBridgeTest.java new file mode 100644 index 000000000..9a6c1b7f9 --- /dev/null +++ b/src/test/java/com/arangodb/springframework/repository/query/QueryTransactionBridgeTest.java @@ -0,0 +1,30 @@ +package com.arangodb.springframework.repository.query; + +import org.hamcrest.Matchers; +import org.junit.After; +import org.junit.Test; + +import java.util.Collections; + +import static org.hamcrest.MatcherAssert.assertThat; + +public class QueryTransactionBridgeTest { + + private QueryTransactionBridge underTest = new QueryTransactionBridge(); + + @Test + public void beginCurrentTransactionInitiallyReturnsNull() { + assertThat(underTest.beginCurrentTransaction(Collections.singleton("test")), Matchers.nullValue()); + } + + @Test + public void setCurrentTransactionBeginIsAppliedOnBeginCurrentTransaction() { + underTest.setCurrentTransactionBegin(collections -> collections.iterator().next()); + assertThat(underTest.beginCurrentTransaction(Collections.singleton("test")), Matchers.is("test")); + } + + @After + public void cleanup() { + underTest.clearCurrentTransactionBegin(); + } +} diff --git a/src/test/java/com/arangodb/springframework/transaction/ArangoTransactionManagerTest.java b/src/test/java/com/arangodb/springframework/transaction/ArangoTransactionManagerTest.java new file mode 100644 index 000000000..21fd5c570 --- /dev/null +++ b/src/test/java/com/arangodb/springframework/transaction/ArangoTransactionManagerTest.java @@ -0,0 +1,97 @@ +package com.arangodb.springframework.transaction; + +import com.arangodb.ArangoDB; +import com.arangodb.ArangoDatabase; +import com.arangodb.DbName; +import com.arangodb.entity.StreamTransactionEntity; +import com.arangodb.model.StreamTransactionOptions; +import com.arangodb.springframework.core.ArangoOperations; +import com.arangodb.springframework.repository.query.QueryTransactionBridge; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.transaction.InvalidIsolationLevelException; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.interceptor.DefaultTransactionAttribute; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.function.Function; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@RunWith(MockitoJUnitRunner.class) +public class ArangoTransactionManagerTest { + + private static final DbName DATABASE_NAME = DbName.of("test"); + + @Mock + private ArangoOperations operations; + @Mock + private QueryTransactionBridge bridge; + @InjectMocks + private ArangoTransactionManager underTest; + @Mock + private ArangoDB driver; + @Mock + private ArangoDatabase database; + @Mock + private StreamTransactionEntity streamTransaction; + @Captor + private ArgumentCaptor, String>> beginPassed; + @Captor + private ArgumentCaptor optionsPassed; + + @Before + public void setupMocks() { + when(operations.getDatabaseName()) + .thenReturn(DATABASE_NAME); + when(operations.driver()) + .thenReturn(driver); + when(driver.db(any(DbName.class))) + .thenReturn(database); + } + + @Test + public void getTransactionReturnsNewTransactionWithoutStreamTransaction() { + TransactionStatus transaction = underTest.getTransaction(new DefaultTransactionAttribute()); + assertThat(transaction.isNewTransaction(), is(true)); + verify(driver).db(DATABASE_NAME); + verify(bridge).setCurrentTransactionBegin(any()); + verifyNoInteractions(database); + } + + @Test + public void getTransactionReturnsTransactionCreatesStreamTransactionOnBridgeBeginCall() { + DefaultTransactionAttribute definition = new DefaultTransactionAttribute(); + definition.setLabels(Collections.singleton("baz")); + definition.setTimeout(20); + TransactionStatus transaction = underTest.getTransaction(definition); + when(streamTransaction.getId()) + .thenReturn("123"); + when(database.beginStreamTransaction(any())) + .thenReturn(streamTransaction); + verify(bridge).setCurrentTransactionBegin(beginPassed.capture()); + beginPassed.getValue().apply(Arrays.asList("foo", "bar")); + verify(database).beginStreamTransaction(optionsPassed.capture()); + assertThat(optionsPassed.getValue().getAllowImplicit(), is(true)); + assertThat(optionsPassed.getValue().getLockTimeout(), is(20)); + } + + @Test(expected = InvalidIsolationLevelException.class) + public void getTransactionThrowsInvalidIsolationLevelExceptionForIsolationSerializable() { + DefaultTransactionAttribute definition = new DefaultTransactionAttribute(); + definition.setIsolationLevel(TransactionDefinition.ISOLATION_SERIALIZABLE); + underTest.getTransaction(definition); + } +} From 9432a65e8c00d1a7a029d3c90e9dab154a531ebd Mon Sep 17 00:00:00 2001 From: Arne Burmeister Date: Wed, 11 May 2022 09:02:55 +0200 Subject: [PATCH 02/55] #80 expose database name --- .../springframework/core/ArangoOperations.java | 2 ++ .../springframework/core/template/ArangoTemplate.java | 11 ++++++++--- .../transaction/ArangoTransaction.java | 3 ++- .../core/template/ArangoTemplateTest.java | 1 + 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/arangodb/springframework/core/ArangoOperations.java b/src/main/java/com/arangodb/springframework/core/ArangoOperations.java index 57d71cc28..4758fa001 100644 --- a/src/main/java/com/arangodb/springframework/core/ArangoOperations.java +++ b/src/main/java/com/arangodb/springframework/core/ArangoOperations.java @@ -54,6 +54,8 @@ public interface ArangoOperations { */ ArangoDBVersion getVersion() throws DataAccessException; + DbName getDatabaseName(); + /** * Performs a database query using the given {@code query} and {@code bindVars}, then returns a new * {@code ArangoCursor} instance for the result list. diff --git a/src/main/java/com/arangodb/springframework/core/template/ArangoTemplate.java b/src/main/java/com/arangodb/springframework/core/template/ArangoTemplate.java index 639f30dc0..299087063 100644 --- a/src/main/java/com/arangodb/springframework/core/template/ArangoTemplate.java +++ b/src/main/java/com/arangodb/springframework/core/template/ArangoTemplate.java @@ -113,9 +113,8 @@ public ArangoTemplate(final ArangoDB arango, final String database, final Arango version = null; } - private ArangoDatabase db() { - final String key = databaseExpression != null ? databaseExpression.getValue(context, String.class) - : databaseName; + ArangoDatabase db() { + final String key = getDatabaseName().get(); return databaseCache.computeIfAbsent(key, name -> { final ArangoDatabase db = arango.db(name); if (!db.exists()) { @@ -260,6 +259,12 @@ public ArangoDBVersion getVersion() throws DataAccessException { } } + @Override + public DbName getDatabaseName() { + return DbName.of(databaseExpression != null ? databaseExpression.getValue(context, String.class) + : databaseName); + } + @Override public ArangoCursor query(final String query, final Class entityClass) throws DataAccessException { return query(query, null, null, entityClass); diff --git a/src/main/java/com/arangodb/springframework/transaction/ArangoTransaction.java b/src/main/java/com/arangodb/springframework/transaction/ArangoTransaction.java index baed9ad08..6bb6bc856 100644 --- a/src/main/java/com/arangodb/springframework/transaction/ArangoTransaction.java +++ b/src/main/java/com/arangodb/springframework/transaction/ArangoTransaction.java @@ -39,7 +39,8 @@ String begin(Collection collections) { if (definition instanceof TransactionAttribute) { allCollections.addAll(((TransactionAttribute) definition).getLabels()); } - StreamTransactionOptions options = new StreamTransactionOptions().allowImplicit(true) + StreamTransactionOptions options = new StreamTransactionOptions() + .allowImplicit(true) .writeCollections(allCollections.toArray(new String[0])) .lockTimeout(definition.getTimeout() == -1 ? 0 : definition.getTimeout()); transaction = database.beginStreamTransaction(options); diff --git a/src/test/java/com/arangodb/springframework/core/template/ArangoTemplateTest.java b/src/test/java/com/arangodb/springframework/core/template/ArangoTemplateTest.java index 84398bd0a..5544a2c76 100644 --- a/src/test/java/com/arangodb/springframework/core/template/ArangoTemplateTest.java +++ b/src/test/java/com/arangodb/springframework/core/template/ArangoTemplateTest.java @@ -62,6 +62,7 @@ public void template() { assertThat(version.getLicense(), is(notNullValue())); assertThat(version.getServer(), is(notNullValue())); assertThat(version.getVersion(), is(notNullValue())); + assertThat(template.getDatabaseName(), is(notNullValue())); } @Test From c0ba609d536048e026a7dd0446b9258759a52f3e Mon Sep 17 00:00:00 2001 From: Arne Burmeister Date: Wed, 11 May 2022 09:16:03 +0200 Subject: [PATCH 03/55] #80 allow collections from queries --- .../repository/ArangoRepositoryFactory.java | 17 +++++-- .../ArangoRepositoryFactoryBean.java | 9 +++- .../repository/query/AbstractArangoQuery.java | 28 +++++++---- .../repository/query/DerivedArangoQuery.java | 15 +++--- .../query/StringBasedArangoQuery.java | 47 +++++++++++++++---- .../query/derived/DerivedQueryCreator.java | 7 +-- 6 files changed, 89 insertions(+), 34 deletions(-) diff --git a/src/main/java/com/arangodb/springframework/repository/ArangoRepositoryFactory.java b/src/main/java/com/arangodb/springframework/repository/ArangoRepositoryFactory.java index dc54b6511..481206084 100644 --- a/src/main/java/com/arangodb/springframework/repository/ArangoRepositoryFactory.java +++ b/src/main/java/com/arangodb/springframework/repository/ArangoRepositoryFactory.java @@ -25,6 +25,7 @@ import com.arangodb.springframework.config.ArangoConfiguration; import com.arangodb.springframework.core.template.ArangoTemplate; +import com.arangodb.springframework.repository.query.QueryTransactionBridge; import org.springframework.context.ApplicationContext; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.projection.ProjectionFactory; @@ -61,14 +62,17 @@ public class ArangoRepositoryFactory extends RepositoryFactorySupport { private final ArangoTemplate arangoTemplate; private final ApplicationContext applicationContext; + private final QueryTransactionBridge transactionBridge; private final boolean returnOriginalEntities; private final MappingContext, ArangoPersistentProperty> context; public ArangoRepositoryFactory(final ArangoTemplate arangoTemplate, - final ApplicationContext applicationContext, + final ApplicationContext applicationContext, + final QueryTransactionBridge transactionBridge, final ArangoConfiguration arangoConfiguration) { this.arangoTemplate = arangoTemplate; this.applicationContext = applicationContext; + this.transactionBridge = transactionBridge; this.context = arangoTemplate.getConverter().getMappingContext(); returnOriginalEntities = arangoConfiguration.returnOriginalEntities(); } @@ -124,11 +128,14 @@ static class DefaultArangoQueryLookupStrategy implements QueryLookupStrategy { private final ArangoOperations operations; private final ApplicationContext applicationContext; + private final QueryTransactionBridge transactionBridge; public DefaultArangoQueryLookupStrategy(final ArangoOperations operations, - final ApplicationContext applicationContext) { + final QueryTransactionBridge transactionBridge, + final ApplicationContext applicationContext) { this.operations = operations; this.applicationContext = applicationContext; + this.transactionBridge = transactionBridge; } @Override @@ -143,11 +150,11 @@ public RepositoryQuery resolveQuery( if (namedQueries.hasQuery(namedQueryName)) { final String namedQuery = namedQueries.getQuery(namedQueryName); - return new StringBasedArangoQuery(namedQuery, queryMethod, operations, applicationContext); + return new StringBasedArangoQuery(namedQuery, queryMethod, operations, transactionBridge, applicationContext); } else if (queryMethod.hasAnnotatedQuery()) { - return new StringBasedArangoQuery(queryMethod, operations, applicationContext); + return new StringBasedArangoQuery(queryMethod, operations, transactionBridge, applicationContext); } else { - return new DerivedArangoQuery(queryMethod, operations); + return new DerivedArangoQuery(queryMethod, operations, transactionBridge); } } diff --git a/src/main/java/com/arangodb/springframework/repository/ArangoRepositoryFactoryBean.java b/src/main/java/com/arangodb/springframework/repository/ArangoRepositoryFactoryBean.java index d49433f75..216f18a1d 100644 --- a/src/main/java/com/arangodb/springframework/repository/ArangoRepositoryFactoryBean.java +++ b/src/main/java/com/arangodb/springframework/repository/ArangoRepositoryFactoryBean.java @@ -22,6 +22,7 @@ import com.arangodb.springframework.config.ArangoConfiguration; import com.arangodb.springframework.core.template.ArangoTemplate; +import com.arangodb.springframework.repository.query.QueryTransactionBridge; import org.springframework.beans.BeansException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; @@ -39,6 +40,7 @@ public class ArangoRepositoryFactoryBean, S, ID> private ArangoTemplate arangoTemplate; private ApplicationContext applicationContext; + private QueryTransactionBridge transactionBridge; private ArangoConfiguration arangoConfiguration; @Autowired @@ -51,6 +53,11 @@ public void setArangoTemplate(final ArangoTemplate arangoTemplate) { this.arangoTemplate = arangoTemplate; } + @Autowired + public void setTransactionBridge(final QueryTransactionBridge transactionBridge) { + this.transactionBridge = transactionBridge; + } + @Autowired public void setArangoConfiguration(final ArangoConfiguration arangoConfiguration) { this.arangoConfiguration = arangoConfiguration; @@ -59,7 +66,7 @@ public void setArangoConfiguration(final ArangoConfiguration arangoConfiguration @Override protected RepositoryFactorySupport createRepositoryFactory() { Assert.notNull(arangoTemplate, "arangoOperations not configured"); - return new ArangoRepositoryFactory(arangoTemplate, applicationContext, arangoConfiguration); + return new ArangoRepositoryFactory(arangoTemplate, applicationContext, transactionBridge, arangoConfiguration); } @Override diff --git a/src/main/java/com/arangodb/springframework/repository/query/AbstractArangoQuery.java b/src/main/java/com/arangodb/springframework/repository/query/AbstractArangoQuery.java index 54c1f6af0..1a2739a69 100644 --- a/src/main/java/com/arangodb/springframework/repository/query/AbstractArangoQuery.java +++ b/src/main/java/com/arangodb/springframework/repository/query/AbstractArangoQuery.java @@ -30,6 +30,7 @@ import org.slf4j.LoggerFactory; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.ResultProcessor; +import org.springframework.data.util.Pair; import org.springframework.util.Assert; import com.arangodb.ArangoCursor; @@ -51,14 +52,18 @@ public abstract class AbstractArangoQuery implements RepositoryQuery { protected final ArangoOperations operations; protected final ArangoMappingContext mappingContext; protected final Class domainClass; + private final QueryTransactionBridge transactionBridge; - public AbstractArangoQuery(final ArangoQueryMethod method, final ArangoOperations operations) { + public AbstractArangoQuery(final ArangoQueryMethod method, final ArangoOperations operations, + final QueryTransactionBridge transactionBridge) { Assert.notNull(method, "ArangoQueryMethod must not be null!"); Assert.notNull(operations, "ArangoOperations must not be null!"); + Assert.notNull(transactionBridge, "QueryTransactionBridge must not be null!"); this.method = method; this.operations = operations; mappingContext = (ArangoMappingContext) operations.getConverter().getMappingContext(); this.domainClass = method.getEntityInformation().getJavaType(); + this.transactionBridge = transactionBridge; } @Override @@ -75,12 +80,16 @@ public Object execute(final Object[] parameters) { options.fullCount(true); } - final String query = createQuery(accessor, bindVars, options); + final Pair> queryAndCollection = createQuery(accessor, bindVars, options); + if (options.getStreamTransactionId() == null) { + options.streamTransactionId(transactionBridge.beginCurrentTransaction(queryAndCollection.getSecond())); + } + final ResultProcessor processor = method.getResultProcessor().withDynamicProjection(accessor); final Class typeToRead = getTypeToRead(processor); - final ArangoCursor result = operations.query(query, bindVars, options, typeToRead); + final ArangoCursor result = operations.query(queryAndCollection.getFirst(), bindVars, options, typeToRead); logWarningsIfNecessary(result); return processor.processResult(convertResult(result, accessor)); } @@ -107,12 +116,12 @@ public ArangoQueryMethod getQueryMethod() { * the binding parameter map * @param options * contains the merged {@link com.arangodb.model.AqlQueryOptions} - * @return the created AQL query + * @return a pair of the created AQL query and all collection names */ - protected abstract String createQuery( - ArangoParameterAccessor accessor, - Map bindVars, - AqlQueryOptions options); + protected abstract Pair> createQuery( + ArangoParameterAccessor accessor, + Map bindVars, + AqlQueryOptions options); protected abstract boolean isCountQuery(); @@ -178,6 +187,9 @@ protected AqlQueryOptions mergeQueryOptions(final AqlQueryOptions oldStatic, fin mergedOptions.allowDirtyRead(oldStatic.getAllowDirtyRead()); } + if (mergedOptions.getStreamTransactionId() == null) { + mergedOptions.streamTransactionId(oldStatic.getStreamTransactionId()); + } return mergedOptions; } diff --git a/src/main/java/com/arangodb/springframework/repository/query/DerivedArangoQuery.java b/src/main/java/com/arangodb/springframework/repository/query/DerivedArangoQuery.java index 559a45a2a..7d86e4a10 100644 --- a/src/main/java/com/arangodb/springframework/repository/query/DerivedArangoQuery.java +++ b/src/main/java/com/arangodb/springframework/repository/query/DerivedArangoQuery.java @@ -22,12 +22,13 @@ import com.arangodb.entity.IndexEntity; import com.arangodb.entity.IndexType; -import com.arangodb.model.AqlQueryOptions; import com.arangodb.springframework.core.ArangoOperations; import com.arangodb.springframework.repository.query.derived.BindParameterBinding; import com.arangodb.springframework.repository.query.derived.DerivedQueryCreator; import org.springframework.data.repository.query.parser.PartTree; +import org.springframework.data.util.Pair; +import java.util.Collection; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -44,17 +45,17 @@ public class DerivedArangoQuery extends AbstractArangoQuery { private final PartTree tree; private final List geoFields; - public DerivedArangoQuery(final ArangoQueryMethod method, final ArangoOperations operations) { - super(method, operations); + public DerivedArangoQuery(final ArangoQueryMethod method, final ArangoOperations operations, + final QueryTransactionBridge transactionBridge) { + super(method, operations, transactionBridge); tree = new PartTree(method.getName(), domainClass); geoFields = getGeoFields(); } @Override - protected String createQuery( - final ArangoParameterAccessor accessor, - final Map bindVars, - final AqlQueryOptions options) { + protected Pair> createQuery( + final ArangoParameterAccessor accessor, + final Map bindVars) { return new DerivedQueryCreator(mappingContext, domainClass, tree, accessor, new BindParameterBinding(bindVars), geoFields).createQuery(); diff --git a/src/main/java/com/arangodb/springframework/repository/query/StringBasedArangoQuery.java b/src/main/java/com/arangodb/springframework/repository/query/StringBasedArangoQuery.java index 3cc068901..ad6713fd1 100644 --- a/src/main/java/com/arangodb/springframework/repository/query/StringBasedArangoQuery.java +++ b/src/main/java/com/arangodb/springframework/repository/query/StringBasedArangoQuery.java @@ -20,13 +20,16 @@ package com.arangodb.springframework.repository.query; -import com.arangodb.model.AqlQueryOptions; +import com.arangodb.springframework.annotation.Document; +import com.arangodb.springframework.annotation.Edge; import com.arangodb.springframework.core.ArangoOperations; import com.arangodb.springframework.core.util.AqlUtils; import com.arangodb.springframework.repository.query.ArangoParameters.ArangoParameter; import org.springframework.context.ApplicationContext; import org.springframework.context.expression.BeanFactoryAccessor; import org.springframework.context.expression.BeanFactoryResolver; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.data.util.Pair; import org.springframework.expression.Expression; import org.springframework.expression.ParserContext; import org.springframework.expression.spel.standard.SpelExpressionParser; @@ -69,13 +72,14 @@ public class StringBasedArangoQuery extends AbstractArangoQuery { private final ApplicationContext applicationContext; public StringBasedArangoQuery(final ArangoQueryMethod method, final ArangoOperations operations, - final ApplicationContext applicationContext) { - this(method.getAnnotatedQuery(), method, operations, applicationContext); + final QueryTransactionBridge transactionBridge, final ApplicationContext applicationContext) { + this(method.getAnnotatedQuery(), method, operations, transactionBridge, applicationContext); } public StringBasedArangoQuery(final String query, final ArangoQueryMethod method, - final ArangoOperations operations, final ApplicationContext applicationContext) { - super(method, operations); + final ArangoOperations operations, final QueryTransactionBridge transactionBridge, + final ApplicationContext applicationContext) { + super(method, operations, transactionBridge); Assert.notNull(query, "Query must not be null!"); this.query = query; @@ -90,14 +94,37 @@ public StringBasedArangoQuery(final String query, final ArangoQueryMethod method } @Override - protected String createQuery( - final ArangoParameterAccessor accessor, - final Map bindVars, - final AqlQueryOptions options) { + protected Pair> createQuery( + final ArangoParameterAccessor accessor, + final Map bindVars) { extractBindVars(accessor, bindVars); - return prepareQuery(accessor); + return Pair.of(prepareQuery(accessor), allCollectionNames(collectionName, bindVars)); + } + + private Collection allCollectionNames(String collectionName, Map bindVars) { + HashSet allCollections = new HashSet<>(); + allCollections.add(collectionName); + bindVars.entrySet().stream() + .filter(entry -> entry.getKey().startsWith("@")) + .map(Map.Entry::getValue) + .map(value -> value instanceof Class ? getCollectionName((Class) value): value.toString()) + .filter(Objects::nonNull) + .forEach(allCollections::add); + return allCollections; + } + + private String getCollectionName(Class value) { + Document document = AnnotationUtils.findAnnotation(value, Document.class); + if (document != null) { + return document.value(); + } + Edge edge = AnnotationUtils.findAnnotation(value, Edge.class); + if (edge != null) { + return edge.value(); + } + return null; } @Override diff --git a/src/main/java/com/arangodb/springframework/repository/query/derived/DerivedQueryCreator.java b/src/main/java/com/arangodb/springframework/repository/query/derived/DerivedQueryCreator.java index 853c50324..7db218616 100644 --- a/src/main/java/com/arangodb/springframework/repository/query/derived/DerivedQueryCreator.java +++ b/src/main/java/com/arangodb/springframework/repository/query/derived/DerivedQueryCreator.java @@ -42,6 +42,7 @@ import org.springframework.data.repository.query.parser.AbstractQueryCreator; import org.springframework.data.repository.query.parser.Part; import org.springframework.data.repository.query.parser.PartTree; +import org.springframework.data.util.Pair; import org.springframework.util.Assert; import java.util.*; @@ -50,7 +51,7 @@ /** * Creates a full AQL query from a PartTree and ArangoParameterAccessor */ -public class DerivedQueryCreator extends AbstractQueryCreator { +public class DerivedQueryCreator extends AbstractQueryCreator>, Criteria> { private static final Logger LOGGER = LoggerFactory.getLogger(DerivedQueryCreator.class); private static final Set UNSUPPORTED_IGNORE_CASE = new HashSet<>(); @@ -120,7 +121,7 @@ protected Criteria or(final Criteria base, final Criteria criteria) { * @return */ @Override - protected String complete(final Criteria criteria, final Sort sort) { + protected Pair> complete(final Criteria criteria, final Sort sort) { if (tree.isDistinct() && !tree.isCountProjection()) { LOGGER.debug("Use of 'Distinct' is meaningful only in count queries"); } @@ -191,7 +192,7 @@ protected String complete(final Criteria criteria, final Sort sort) { } } } - return query.toString(); + return Pair.of(query.toString(), withCollections); } public double[] getUniquePoint() { From 3744296973642c806e37150faef1cb50fefeca0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Malte=20J=C3=B6rgens?= Date: Wed, 11 May 2022 10:34:30 +0200 Subject: [PATCH 04/55] #80 made QueryTransactionBridge an optional @Bean --- .../repository/ArangoRepositoryFactoryBean.java | 2 +- .../repository/query/AbstractArangoQuery.java | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/arangodb/springframework/repository/ArangoRepositoryFactoryBean.java b/src/main/java/com/arangodb/springframework/repository/ArangoRepositoryFactoryBean.java index 216f18a1d..bb5ba8491 100644 --- a/src/main/java/com/arangodb/springframework/repository/ArangoRepositoryFactoryBean.java +++ b/src/main/java/com/arangodb/springframework/repository/ArangoRepositoryFactoryBean.java @@ -53,7 +53,7 @@ public void setArangoTemplate(final ArangoTemplate arangoTemplate) { this.arangoTemplate = arangoTemplate; } - @Autowired + @Autowired(required = false) public void setTransactionBridge(final QueryTransactionBridge transactionBridge) { this.transactionBridge = transactionBridge; } diff --git a/src/main/java/com/arangodb/springframework/repository/query/AbstractArangoQuery.java b/src/main/java/com/arangodb/springframework/repository/query/AbstractArangoQuery.java index 1a2739a69..351d2e832 100644 --- a/src/main/java/com/arangodb/springframework/repository/query/AbstractArangoQuery.java +++ b/src/main/java/com/arangodb/springframework/repository/query/AbstractArangoQuery.java @@ -38,7 +38,7 @@ import com.arangodb.springframework.core.ArangoOperations; /** - * + * * @author Audrius Malele * @author Mark McCormick * @author Mark Vollmary @@ -58,7 +58,6 @@ public AbstractArangoQuery(final ArangoQueryMethod method, final ArangoOperation final QueryTransactionBridge transactionBridge) { Assert.notNull(method, "ArangoQueryMethod must not be null!"); Assert.notNull(operations, "ArangoOperations must not be null!"); - Assert.notNull(transactionBridge, "QueryTransactionBridge must not be null!"); this.method = method; this.operations = operations; mappingContext = (ArangoMappingContext) operations.getConverter().getMappingContext(); @@ -81,7 +80,7 @@ public Object execute(final Object[] parameters) { } final Pair> queryAndCollection = createQuery(accessor, bindVars, options); - if (options.getStreamTransactionId() == null) { + if (options.getStreamTransactionId() == null && transactionBridge != null) { options.streamTransactionId(transactionBridge.beginCurrentTransaction(queryAndCollection.getSecond())); } From 8803e1ddb1774003729d5b0a0d283237f8eeb763 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Malte=20J=C3=B6rgens?= Date: Wed, 11 May 2022 15:57:46 +0200 Subject: [PATCH 05/55] #80 started tests --- .../ArangoTransactionalTestConfiguration.java | 20 +++++++++ ...rangoTransactionManagerRepositoryTest.java | 45 +++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 src/test/java/com/arangodb/springframework/ArangoTransactionalTestConfiguration.java create mode 100644 src/test/java/com/arangodb/springframework/transaction/ArangoTransactionManagerRepositoryTest.java diff --git a/src/test/java/com/arangodb/springframework/ArangoTransactionalTestConfiguration.java b/src/test/java/com/arangodb/springframework/ArangoTransactionalTestConfiguration.java new file mode 100644 index 000000000..acdaadc00 --- /dev/null +++ b/src/test/java/com/arangodb/springframework/ArangoTransactionalTestConfiguration.java @@ -0,0 +1,20 @@ +package com.arangodb.springframework; + +import com.arangodb.springframework.repository.query.QueryTransactionBridge; +import com.arangodb.springframework.transaction.ArangoTransactionManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.transaction.TransactionalTestExecutionListener; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +@EnableTransactionManagement +@TestExecutionListeners(TransactionalTestExecutionListener.class) +@Import(ArangoTransactionManager.class) +public class ArangoTransactionalTestConfiguration extends ArangoTestConfiguration { + + @Bean + public QueryTransactionBridge queryTransactionBridge() { + return new QueryTransactionBridge(); + } +} diff --git a/src/test/java/com/arangodb/springframework/transaction/ArangoTransactionManagerRepositoryTest.java b/src/test/java/com/arangodb/springframework/transaction/ArangoTransactionManagerRepositoryTest.java new file mode 100644 index 000000000..9fd19f0cb --- /dev/null +++ b/src/test/java/com/arangodb/springframework/transaction/ArangoTransactionManagerRepositoryTest.java @@ -0,0 +1,45 @@ +package com.arangodb.springframework.transaction; + +import com.arangodb.springframework.AbstractArangoTest; +import com.arangodb.springframework.ArangoTransactionalTestConfiguration; +import com.arangodb.springframework.repository.HumanBeingRepository; +import com.arangodb.springframework.testdata.HumanBeing; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.transaction.TestTransaction; +import org.springframework.transaction.annotation.Transactional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +@ContextConfiguration(classes = { ArangoTransactionalTestConfiguration.class }) +public class ArangoTransactionManagerRepositoryTest extends AbstractArangoTest { + + private final HumanBeing anakin = new HumanBeing("Anakin", "Skywalker", false); + + @Autowired + private HumanBeingRepository humanBeingRepository; + + @Test + public void shouldWorkWithoutTransaction() { + assertDoesNotThrow(() -> humanBeingRepository.save(anakin)); + } + + @Test + @Transactional + public void shouldWorkWithinTransaction() { + TestTransaction.flagForCommit(); + assertDoesNotThrow(() -> humanBeingRepository.save(anakin)); + } + + @Test + @Transactional + public void shouldRollbackWithinTransaction() { + humanBeingRepository.save(anakin); + TestTransaction.flagForRollback(); + TestTransaction.end(); + + assertThat(humanBeingRepository.findByNameAndSurname(anakin.getName(), anakin.getSurname())).isNotPresent(); + } +} From 5150bb2bdcbd969c1d5866f08dd09fc53189cde8 Mon Sep 17 00:00:00 2001 From: Arne Burmeister Date: Fri, 26 Aug 2022 14:53:23 +0200 Subject: [PATCH 06/55] #80 fix merge error --- .../repository/query/StringBasedArangoQuery.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/com/arangodb/springframework/repository/query/StringBasedArangoQuery.java b/src/main/java/com/arangodb/springframework/repository/query/StringBasedArangoQuery.java index ad6713fd1..12f4db781 100644 --- a/src/main/java/com/arangodb/springframework/repository/query/StringBasedArangoQuery.java +++ b/src/main/java/com/arangodb/springframework/repository/query/StringBasedArangoQuery.java @@ -36,9 +36,7 @@ import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.util.Assert; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; +import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; From d07766f865096a1f8903dbada3009417e2216d8e Mon Sep 17 00:00:00 2001 From: Arne Burmeister Date: Fri, 13 May 2022 17:07:19 +0200 Subject: [PATCH 07/55] #80 allow multiple transaction calls unless there are no additional collections allowed --- .../repository/query/AbstractArangoQuery.java | 2 +- .../query/QueryTransactionBridge.java | 16 ++++++++-------- .../transaction/ArangoTransaction.java | 19 ++++++++++++------- .../transaction/ArangoTransactionManager.java | 8 ++++---- .../query/QueryTransactionBridgeTest.java | 12 ++++++------ .../ArangoTransactionManagerTest.java | 4 ++-- 6 files changed, 33 insertions(+), 28 deletions(-) diff --git a/src/main/java/com/arangodb/springframework/repository/query/AbstractArangoQuery.java b/src/main/java/com/arangodb/springframework/repository/query/AbstractArangoQuery.java index 351d2e832..7fd558474 100644 --- a/src/main/java/com/arangodb/springframework/repository/query/AbstractArangoQuery.java +++ b/src/main/java/com/arangodb/springframework/repository/query/AbstractArangoQuery.java @@ -81,7 +81,7 @@ public Object execute(final Object[] parameters) { final Pair> queryAndCollection = createQuery(accessor, bindVars, options); if (options.getStreamTransactionId() == null && transactionBridge != null) { - options.streamTransactionId(transactionBridge.beginCurrentTransaction(queryAndCollection.getSecond())); + options.streamTransactionId(transactionBridge.getCurrentTransaction(queryAndCollection.getSecond())); } diff --git a/src/main/java/com/arangodb/springframework/repository/query/QueryTransactionBridge.java b/src/main/java/com/arangodb/springframework/repository/query/QueryTransactionBridge.java index b65808ce9..80a418001 100644 --- a/src/main/java/com/arangodb/springframework/repository/query/QueryTransactionBridge.java +++ b/src/main/java/com/arangodb/springframework/repository/query/QueryTransactionBridge.java @@ -10,21 +10,21 @@ */ public class QueryTransactionBridge { private static final Function, String> NO_TRANSACTION = any -> null; - private static final ThreadLocal, String>> CURRENT_TRANSACTION_BEGIN = new NamedInheritableThreadLocal<>("ArangoTransactionBegin"); + private static final ThreadLocal, String>> CURRENT_TRANSACTION = new NamedInheritableThreadLocal<>("ArangoTransactionBegin"); public QueryTransactionBridge() { - CURRENT_TRANSACTION_BEGIN.set(NO_TRANSACTION); + CURRENT_TRANSACTION.set(NO_TRANSACTION); } - public void setCurrentTransactionBegin(Function, String> begin) { - CURRENT_TRANSACTION_BEGIN.set(begin); + public void setCurrentTransaction(Function, String> begin) { + CURRENT_TRANSACTION.set(begin); } - public void clearCurrentTransactionBegin() { - CURRENT_TRANSACTION_BEGIN.set(NO_TRANSACTION); + public void clearCurrentTransaction() { + CURRENT_TRANSACTION.set(NO_TRANSACTION); } - public String beginCurrentTransaction(Collection collections) { - return CURRENT_TRANSACTION_BEGIN.get().apply(collections); + public String getCurrentTransaction(Collection collections) { + return CURRENT_TRANSACTION.get().apply(collections); } } diff --git a/src/main/java/com/arangodb/springframework/transaction/ArangoTransaction.java b/src/main/java/com/arangodb/springframework/transaction/ArangoTransaction.java index 6bb6bc856..316aff05e 100644 --- a/src/main/java/com/arangodb/springframework/transaction/ArangoTransaction.java +++ b/src/main/java/com/arangodb/springframework/transaction/ArangoTransaction.java @@ -16,6 +16,8 @@ class ArangoTransaction implements SmartTransactionObject { private final ArangoDatabase database; + + private final Set writeCollections = new HashSet<>(); private TransactionDefinition definition; private StreamTransactionEntity transaction; @@ -29,19 +31,22 @@ boolean exists() { void configure(TransactionDefinition definition) { this.definition = definition; + if (definition instanceof TransactionAttribute) { + writeCollections.addAll(((TransactionAttribute) definition).getLabels()); + } } - String begin(Collection collections) { + String getOrBegin(Collection collections) { if (transaction != null) { - throw new IllegalTransactionStateException("Stream transaction already started"); - } - Set allCollections = new HashSet<>(collections); - if (definition instanceof TransactionAttribute) { - allCollections.addAll(((TransactionAttribute) definition).getLabels()); + if (!writeCollections.containsAll(collections)) { + throw new IllegalTransactionStateException("Stream transaction already started, no additional collections allowed"); + } + return transaction.getId(); } + writeCollections.addAll(collections); StreamTransactionOptions options = new StreamTransactionOptions() .allowImplicit(true) - .writeCollections(allCollections.toArray(new String[0])) + .writeCollections(writeCollections.toArray(new String[0])) .lockTimeout(definition.getTimeout() == -1 ? 0 : definition.getTimeout()); transaction = database.beginStreamTransaction(options); return transaction.getId(); diff --git a/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManager.java b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManager.java index 6295b47fe..b13749272 100644 --- a/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManager.java +++ b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManager.java @@ -43,8 +43,8 @@ protected void doBegin(Object transaction, TransactionDefinition definition) thr } ArangoTransaction tx = (ArangoTransaction) transaction; tx.configure(definition); - Function, String> begin = tx::begin; - bridge.setCurrentTransactionBegin(begin.andThen(id -> { + Function, String> begin = tx::getOrBegin; + bridge.setCurrentTransaction(begin.andThen(id -> { if (logger.isDebugEnabled()) { logger.debug("Began stream transaction " + id); } @@ -59,7 +59,7 @@ protected void doCommit(DefaultTransactionStatus status) throws TransactionExcep logger.debug("Commit stream transaction " + tx); } tx.commit(); - bridge.clearCurrentTransactionBegin(); + bridge.clearCurrentTransaction(); } @Override @@ -69,7 +69,7 @@ protected void doRollback(DefaultTransactionStatus status) throws TransactionExc logger.debug("Rollback stream transaction " + tx); } tx.rollback(); - bridge.clearCurrentTransactionBegin(); + bridge.clearCurrentTransaction(); } @Override diff --git a/src/test/java/com/arangodb/springframework/repository/query/QueryTransactionBridgeTest.java b/src/test/java/com/arangodb/springframework/repository/query/QueryTransactionBridgeTest.java index 9a6c1b7f9..d7735c506 100644 --- a/src/test/java/com/arangodb/springframework/repository/query/QueryTransactionBridgeTest.java +++ b/src/test/java/com/arangodb/springframework/repository/query/QueryTransactionBridgeTest.java @@ -13,18 +13,18 @@ public class QueryTransactionBridgeTest { private QueryTransactionBridge underTest = new QueryTransactionBridge(); @Test - public void beginCurrentTransactionInitiallyReturnsNull() { - assertThat(underTest.beginCurrentTransaction(Collections.singleton("test")), Matchers.nullValue()); + public void getCurrentTransactionInitiallyReturnsNull() { + assertThat(underTest.getCurrentTransaction(Collections.singleton("test")), Matchers.nullValue()); } @Test - public void setCurrentTransactionBeginIsAppliedOnBeginCurrentTransaction() { - underTest.setCurrentTransactionBegin(collections -> collections.iterator().next()); - assertThat(underTest.beginCurrentTransaction(Collections.singleton("test")), Matchers.is("test")); + public void setCurrentTransactionIsAppliedOnGetCurrentTransaction() { + underTest.setCurrentTransaction(collections -> collections.iterator().next()); + assertThat(underTest.getCurrentTransaction(Collections.singleton("test")), Matchers.is("test")); } @After public void cleanup() { - underTest.clearCurrentTransactionBegin(); + underTest.clearCurrentTransaction(); } } diff --git a/src/test/java/com/arangodb/springframework/transaction/ArangoTransactionManagerTest.java b/src/test/java/com/arangodb/springframework/transaction/ArangoTransactionManagerTest.java index 21fd5c570..0ce92b368 100644 --- a/src/test/java/com/arangodb/springframework/transaction/ArangoTransactionManagerTest.java +++ b/src/test/java/com/arangodb/springframework/transaction/ArangoTransactionManagerTest.java @@ -67,7 +67,7 @@ public void getTransactionReturnsNewTransactionWithoutStreamTransaction() { TransactionStatus transaction = underTest.getTransaction(new DefaultTransactionAttribute()); assertThat(transaction.isNewTransaction(), is(true)); verify(driver).db(DATABASE_NAME); - verify(bridge).setCurrentTransactionBegin(any()); + verify(bridge).setCurrentTransaction(any()); verifyNoInteractions(database); } @@ -81,7 +81,7 @@ public void getTransactionReturnsTransactionCreatesStreamTransactionOnBridgeBegi .thenReturn("123"); when(database.beginStreamTransaction(any())) .thenReturn(streamTransaction); - verify(bridge).setCurrentTransactionBegin(beginPassed.capture()); + verify(bridge).setCurrentTransaction(beginPassed.capture()); beginPassed.getValue().apply(Arrays.asList("foo", "bar")); verify(database).beginStreamTransaction(optionsPassed.capture()); assertThat(optionsPassed.getValue().getAllowImplicit(), is(true)); From a8a39d3b77447deade540e5cbf36fbd28002d0ed Mon Sep 17 00:00:00 2001 From: Arne Burmeister Date: Tue, 17 May 2022 09:44:37 +0200 Subject: [PATCH 08/55] #80 more tests --- ...rangoTransactionManagerRepositoryTest.java | 27 ++++++++-- .../ArangoTransactionManagerTest.java | 49 ++++++++++++++++++- 2 files changed, 71 insertions(+), 5 deletions(-) diff --git a/src/test/java/com/arangodb/springframework/transaction/ArangoTransactionManagerRepositoryTest.java b/src/test/java/com/arangodb/springframework/transaction/ArangoTransactionManagerRepositoryTest.java index 9fd19f0cb..777245c9d 100644 --- a/src/test/java/com/arangodb/springframework/transaction/ArangoTransactionManagerRepositoryTest.java +++ b/src/test/java/com/arangodb/springframework/transaction/ArangoTransactionManagerRepositoryTest.java @@ -4,6 +4,7 @@ import com.arangodb.springframework.ArangoTransactionalTestConfiguration; import com.arangodb.springframework.repository.HumanBeingRepository; import com.arangodb.springframework.testdata.HumanBeing; +import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; @@ -11,7 +12,6 @@ import org.springframework.transaction.annotation.Transactional; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; @ContextConfiguration(classes = { ArangoTransactionalTestConfiguration.class }) public class ArangoTransactionManagerRepositoryTest extends AbstractArangoTest { @@ -21,16 +21,37 @@ public class ArangoTransactionManagerRepositoryTest extends AbstractArangoTest { @Autowired private HumanBeingRepository humanBeingRepository; + @Before + public void setUp() { + humanBeingRepository.deleteAll(); + } + @Test public void shouldWorkWithoutTransaction() { - assertDoesNotThrow(() -> humanBeingRepository.save(anakin)); + humanBeingRepository.save(anakin); + + assertThat(humanBeingRepository.findByNameAndSurname(anakin.getName(), anakin.getSurname())).isPresent(); } @Test @Transactional public void shouldWorkWithinTransaction() { + humanBeingRepository.save(anakin); + + assertThat(humanBeingRepository.findByNameAndSurname(anakin.getName(), anakin.getSurname())).isPresent(); + } + + @Test + @Transactional + public void shouldWorkAfterTransaction() { TestTransaction.flagForCommit(); - assertDoesNotThrow(() -> humanBeingRepository.save(anakin)); + + humanBeingRepository.save(anakin); + + assertThat(TestTransaction.isFlaggedForRollback()).isFalse(); + TestTransaction.end(); + + assertThat(humanBeingRepository.findByNameAndSurname(anakin.getName(), anakin.getSurname())).isPresent(); } @Test diff --git a/src/test/java/com/arangodb/springframework/transaction/ArangoTransactionManagerTest.java b/src/test/java/com/arangodb/springframework/transaction/ArangoTransactionManagerTest.java index 0ce92b368..6dd158d12 100644 --- a/src/test/java/com/arangodb/springframework/transaction/ArangoTransactionManagerTest.java +++ b/src/test/java/com/arangodb/springframework/transaction/ArangoTransactionManagerTest.java @@ -5,6 +5,7 @@ import com.arangodb.DbName; import com.arangodb.entity.StreamTransactionEntity; import com.arangodb.model.StreamTransactionOptions; +import com.arangodb.model.TransactionCollectionOptions; import com.arangodb.springframework.core.ArangoOperations; import com.arangodb.springframework.repository.query.QueryTransactionBridge; import org.junit.Before; @@ -15,6 +16,7 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.transaction.IllegalTransactionStateException; import org.springframework.transaction.InvalidIsolationLevelException; import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.TransactionStatus; @@ -26,9 +28,10 @@ import java.util.function.Function; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; +import static org.springframework.beans.PropertyAccessorFactory.forDirectFieldAccess; @RunWith(MockitoJUnitRunner.class) public class ArangoTransactionManagerTest { @@ -72,7 +75,7 @@ public void getTransactionReturnsNewTransactionWithoutStreamTransaction() { } @Test - public void getTransactionReturnsTransactionCreatesStreamTransactionOnBridgeBeginCall() { + public void getTransactionReturnsTransactionCreatesStreamTransactionWithAllCollectionsOnBridgeBeginCall() { DefaultTransactionAttribute definition = new DefaultTransactionAttribute(); definition.setLabels(Collections.singleton("baz")); definition.setTimeout(20); @@ -83,9 +86,47 @@ public void getTransactionReturnsTransactionCreatesStreamTransactionOnBridgeBegi .thenReturn(streamTransaction); verify(bridge).setCurrentTransaction(beginPassed.capture()); beginPassed.getValue().apply(Arrays.asList("foo", "bar")); + assertThat(transaction.isCompleted(), is(false)); verify(database).beginStreamTransaction(optionsPassed.capture()); assertThat(optionsPassed.getValue().getAllowImplicit(), is(true)); assertThat(optionsPassed.getValue().getLockTimeout(), is(20)); + TransactionCollectionOptions collections = getCollections(optionsPassed.getValue()); + assertThat(collections.getRead(), nullValue()); + assertThat(collections.getExclusive(), nullValue()); + assertThat(collections.getWrite(), hasItems("baz", "foo", "bar")); + } + + @Test + public void getTransactionWithMultipleBridgeCallsWorksForKnownCollections() { + DefaultTransactionAttribute definition = new DefaultTransactionAttribute(); + definition.setLabels(Collections.singleton("baz")); + definition.setTimeout(20); + underTest.getTransaction(definition); + when(streamTransaction.getId()) + .thenReturn("123"); + when(database.beginStreamTransaction(any())) + .thenReturn(streamTransaction); + verify(bridge).setCurrentTransaction(beginPassed.capture()); + beginPassed.getValue().apply(Collections.singletonList("foo")); + beginPassed.getValue().apply(Arrays.asList("foo", "baz")); + verify(database).beginStreamTransaction(optionsPassed.capture()); + TransactionCollectionOptions collections = getCollections(optionsPassed.getValue()); + assertThat(collections.getWrite(), hasItems("baz", "foo")); + } + + @Test(expected = IllegalTransactionStateException.class) + public void getTransactionWithMultipleBridgeCallsFailsForAdditionalCollection() { + DefaultTransactionAttribute definition = new DefaultTransactionAttribute(); + definition.setLabels(Collections.singleton("baz")); + definition.setTimeout(20); + underTest.getTransaction(definition); + when(streamTransaction.getId()) + .thenReturn("123"); + when(database.beginStreamTransaction(any())) + .thenReturn(streamTransaction); + verify(bridge).setCurrentTransaction(beginPassed.capture()); + beginPassed.getValue().apply(Collections.singletonList("foo")); + beginPassed.getValue().apply(Collections.singletonList("bar")); } @Test(expected = InvalidIsolationLevelException.class) @@ -94,4 +135,8 @@ public void getTransactionThrowsInvalidIsolationLevelExceptionForIsolationSerial definition.setIsolationLevel(TransactionDefinition.ISOLATION_SERIALIZABLE); underTest.getTransaction(definition); } + + private TransactionCollectionOptions getCollections(StreamTransactionOptions options) { + return (TransactionCollectionOptions) forDirectFieldAccess(options).getPropertyValue("collections"); + } } From 61ed67dc3400f0bc4a6e3954c7f4d7aae7571023 Mon Sep 17 00:00:00 2001 From: Arne Burmeister Date: Tue, 17 May 2022 13:33:31 +0200 Subject: [PATCH 09/55] #80 refactor all operations with defaults without options, find also with options --- .../core/ArangoOperations.java | 87 +++++++++++---- .../core/template/ArangoTemplate.java | 104 +++--------------- 2 files changed, 80 insertions(+), 111 deletions(-) diff --git a/src/main/java/com/arangodb/springframework/core/ArangoOperations.java b/src/main/java/com/arangodb/springframework/core/ArangoOperations.java index 4758fa001..06de184b0 100644 --- a/src/main/java/com/arangodb/springframework/core/ArangoOperations.java +++ b/src/main/java/com/arangodb/springframework/core/ArangoOperations.java @@ -87,8 +87,10 @@ ArangoCursor query(String query, Map bindVars, AqlQueryOp * @return cursor of the results * @throws DataAccessException */ - ArangoCursor query(String query, Map bindVars, Class entityClass) - throws DataAccessException; + default ArangoCursor query(String query, Map bindVars, Class entityClass) + throws DataAccessException { + return query(query, bindVars, new AqlQueryOptions(), entityClass); + } /** * Performs a database query using the given {@code query}, then returns a new {@code ArangoCursor} instance for the @@ -116,7 +118,9 @@ ArangoCursor query(String query, Map bindVars, Class e * @return cursor of the results * @throws DataAccessException */ - ArangoCursor query(String query, Class entityClass) throws DataAccessException; + default ArangoCursor query(String query, Class entityClass) throws DataAccessException { + return query(query, new AqlQueryOptions(), entityClass); + } /** * Deletes multiple documents from a collection. @@ -145,7 +149,10 @@ MultiDocumentEntity> deleteAll( * @return information about the documents * @throws DataAccessException */ - MultiDocumentEntity> deleteAll(Iterable values, Class entityClass) throws DataAccessException; + default MultiDocumentEntity> deleteAll(Iterable values, Class entityClass) + throws DataAccessException { + return deleteAll(values, new DocumentDeleteOptions(), entityClass); + } /** * Deletes multiple documents with the given IDs from a collection. @@ -200,7 +207,9 @@ MultiDocumentEntity> deleteAllById( * @return information about the document * @throws DataAccessException */ - DocumentDeleteEntity delete(Object id, Class entityClass) throws DataAccessException; + default DocumentDeleteEntity delete(Object id, Class entityClass) throws DataAccessException { + return delete(id, new DocumentDeleteOptions(), entityClass); + } /** * Partially updates documents, the documents to update are specified by the _key attributes in the objects on @@ -240,7 +249,10 @@ MultiDocumentEntity> updateAll( * @return information about the documents * @throws DataAccessException */ - MultiDocumentEntity> updateAll(Iterable values, Class entityClass) throws DataAccessException; + default MultiDocumentEntity> updateAll(Iterable values, Class entityClass) + throws DataAccessException { + return updateAll(values, new DocumentUpdateOptions(), entityClass); + } /** * Partially updates the document identified by document id or key. The value must contain a document with the @@ -270,7 +282,9 @@ MultiDocumentEntity> updateAll( * @return information about the document * @throws DataAccessException */ - DocumentUpdateEntity update(Object id, Object value) throws DataAccessException; + default DocumentUpdateEntity update(Object id, T value) throws DataAccessException { + return update(id, value, new DocumentUpdateOptions()); + } /** * Replaces multiple documents in the specified collection with the ones in the values, the replaced documents are @@ -305,8 +319,10 @@ MultiDocumentEntity> replaceAll( * @return information about the documents * @throws DataAccessException */ - MultiDocumentEntity> replaceAll(Iterable values, Class entityClass) - throws DataAccessException; + default MultiDocumentEntity> replaceAll(Iterable values, Class entityClass) + throws DataAccessException { + return replaceAll(values, new DocumentReplaceOptions(), entityClass); + } /** * Replaces the document with {@code id} with the one in the body, provided there is such a document and no @@ -334,7 +350,9 @@ MultiDocumentEntity> replaceAll(Iterable replace(Object id, Object value) throws DataAccessException; + default DocumentUpdateEntity replace(Object id, T value) throws DataAccessException { + return replace(id, value, new DocumentReplaceOptions()); + } /** * Retrieves the document with the given {@code id} from a collection. @@ -360,7 +378,9 @@ MultiDocumentEntity> replaceAll(Iterable Optional find(Object id, Class entityClass) throws DataAccessException; + default Optional find(Object id, Class entityClass) throws DataAccessException { + return find(id, entityClass, new DocumentReadOptions()); + } /** * Retrieves all documents from a collection. @@ -370,7 +390,11 @@ MultiDocumentEntity> replaceAll(Iterable Iterable findAll(Class entityClass) throws DataAccessException; + Iterable findAll(Class entityClass, DocumentReadOptions options) throws DataAccessException; + + default Iterable findAll(Class entityClass) throws DataAccessException { + return findAll(entityClass, new DocumentReadOptions()); + } /** * Retrieves multiple documents with the given {@code ids} from a collection. @@ -382,7 +406,11 @@ MultiDocumentEntity> replaceAll(Iterable Iterable findAll(final Iterable ids, final Class entityClass) throws DataAccessException; + Iterable findAll(final Iterable ids, final Class entityClass, DocumentReadOptions options) throws DataAccessException; + + default Iterable findAll(final Iterable ids, final Class entityClass) throws DataAccessException { + return findAll(ids, entityClass, new DocumentReadOptions()); + } /** * Creates new documents from the given documents, unless there is already a document with the _key given. If no @@ -415,8 +443,10 @@ MultiDocumentEntity> insertAll( * @return information about the documents * @throws DataAccessException */ - MultiDocumentEntity> insertAll(Iterable values, Class entityClass) - throws DataAccessException; + default MultiDocumentEntity> insertAll(Iterable values, Class entityClass) + throws DataAccessException { + return insertAll(values, new DocumentCreateOptions(), entityClass); + } /** * Creates a new document from the given document, unless there is already a document with the _key given. If no @@ -438,8 +468,9 @@ MultiDocumentEntity> insertAll(Iterable * A representation of a single document * @return information about the document */ - DocumentCreateEntity insert(Object value) throws DataAccessException; - + default DocumentCreateEntity insert(T value) throws DataAccessException { + return insert(value, new DocumentCreateOptions()); + } /** * Creates a new document from the given document, unless there is already a document with the id given. In that * case it replaces the document. @@ -449,7 +480,11 @@ MultiDocumentEntity> insertAll(Iterable * @throws DataAccessException * @since ArangoDB 3.4 */ - T repsert(T value) throws DataAccessException; + void repsert(T value, AqlQueryOptions options) throws DataAccessException; + + default T repsert(T value) throws DataAccessException { + repsert(value, new AqlQueryOptions()); + } /** * Creates new documents from the given documents, unless there already exists. In that case it replaces the @@ -462,7 +497,11 @@ MultiDocumentEntity> insertAll(Iterable * @throws DataAccessException * @since ArangoDB 3.4 */ - Iterable repsertAll(Iterable values, Class entityClass) throws DataAccessException; + Iterable repsertAll(Iterable values, Class entityClass, AqlQueryOptions options) throws DataAccessException; + + default Iterable repsertAll(Iterable values, Class entityClass) throws DataAccessException { + return repsertAll(values, entityClass, new AqlQueryOptions()); + } /** * Checks whether the document exists by reading a single document head @@ -474,7 +513,11 @@ MultiDocumentEntity> insertAll(Iterable * @return true if the document exists, false if not * @throws DataAccessException */ - boolean exists(Object id, Class entityClass) throws DataAccessException; + boolean exists(Object id, Class entityClass, DocumentExistsOptions options) throws DataAccessException; + + default boolean exists(Object id, Class entityClass) throws DataAccessException { + return exists(id, entityClass, new DocumentExistsOptions()); + } /** * Drop an existing database @@ -503,7 +546,9 @@ MultiDocumentEntity> insertAll(Iterable * @return {@link CollectionOperations} * @throws DataAccessException */ - CollectionOperations collection(String name) throws DataAccessException; + default CollectionOperations collection(String name) throws DataAccessException { + return collection(name, new CollectionCreateOptions()); + } /** * Returns the operations interface for a collection. If the collection does not exists, it is created diff --git a/src/main/java/com/arangodb/springframework/core/template/ArangoTemplate.java b/src/main/java/com/arangodb/springframework/core/template/ArangoTemplate.java index 299087063..aeef173e4 100644 --- a/src/main/java/com/arangodb/springframework/core/template/ArangoTemplate.java +++ b/src/main/java/com/arangodb/springframework/core/template/ArangoTemplate.java @@ -265,17 +265,6 @@ public DbName getDatabaseName() { : databaseName); } - @Override - public ArangoCursor query(final String query, final Class entityClass) throws DataAccessException { - return query(query, null, null, entityClass); - } - - @Override - public ArangoCursor query(final String query, final Map bindVars, final Class entityClass) - throws DataAccessException { - return query(query, bindVars, null, entityClass); - } - @Override public ArangoCursor query(final String query, final AqlQueryOptions options, final Class entityClass) throws DataAccessException { @@ -325,15 +314,6 @@ public MultiDocumentEntity> deleteAll( return result; } - @Override - @SuppressWarnings({"unchecked", "rawtypes"}) - public MultiDocumentEntity> deleteAll( - final Iterable values, - final Class entityClass - ) throws DataAccessException { - return deleteAll(values, new DocumentDeleteOptions(), (Class) entityClass); - } - @Override public MultiDocumentEntity> deleteAllById(Iterable ids, DocumentDeleteOptions options, Class entityClass) throws DataAccessException { if (ids == null) { @@ -349,12 +329,6 @@ public MultiDocumentEntity> deleteAllById(Iterable> deleteAllById(Iterable ids, Class entityClass) throws DataAccessException { - return deleteAllById(ids, new DocumentDeleteOptions(), (Class) entityClass); - } - @Override public DocumentDeleteEntity delete(final Object id, final DocumentDeleteOptions options, final Class entityClass) throws DataAccessException { @@ -372,11 +346,6 @@ public DocumentDeleteEntity delete(final Object id, final DocumentDeleteO return result; } - @Override - public DocumentDeleteEntity delete(final Object id, final Class entityClass) throws DataAccessException { - return delete(id, new DocumentDeleteOptions(), entityClass); - } - @Override public MultiDocumentEntity> updateAll( final Iterable values, @@ -398,15 +367,6 @@ public MultiDocumentEntity> updateAll( return result; } - @Override - @SuppressWarnings({"rawtypes", "unchecked"}) - public MultiDocumentEntity> updateAll( - final Iterable values, - final Class entityClass - ) throws DataAccessException { - return updateAll(values, new DocumentUpdateOptions(), (Class) entityClass); - } - @Override public DocumentUpdateEntity update(final Object id, final T value, final DocumentUpdateOptions options) throws DataAccessException { @@ -425,11 +385,6 @@ public DocumentUpdateEntity update(final Object id, final T value, final return result; } - @Override - public DocumentUpdateEntity update(final Object id, final Object value) throws DataAccessException { - return update(id, value, new DocumentUpdateOptions()); - } - @Override public MultiDocumentEntity> replaceAll( final Iterable values, @@ -451,15 +406,6 @@ public MultiDocumentEntity> replaceAll( return result; } - @Override - @SuppressWarnings({"rawtypes", "unchecked"}) - public MultiDocumentEntity> replaceAll( - final Iterable values, - final Class entityClass - ) throws DataAccessException { - return replaceAll(values, new DocumentReplaceOptions(), (Class) entityClass); - } - @Override public DocumentUpdateEntity replace(final Object id, final T value, final DocumentReplaceOptions options) throws DataAccessException { @@ -477,11 +423,6 @@ public DocumentUpdateEntity replace(final Object id, final T value, final return result; } - @Override - public DocumentUpdateEntity replace(final Object id, final Object value) throws DataAccessException { - return replace(id, value, new DocumentReplaceOptions()); - } - @Override public Optional find(final Object id, final Class entityClass, final DocumentReadOptions options) throws DataAccessException { @@ -497,24 +438,19 @@ public Optional find(final Object id, final Class entityClass, final D } @Override - public Optional find(final Object id, final Class entityClass) throws DataAccessException { - return find(id, entityClass, new DocumentReadOptions()); - } - - @Override - public Iterable findAll(final Class entityClass) throws DataAccessException { + public Iterable findAll(final Class entityClass, DocumentReadOptions options) throws DataAccessException { final String query = "FOR entity IN @@col RETURN entity"; final Map bindVars = Collections.singletonMap("@col", entityClass); - return query(query, bindVars, null, entityClass).asListRemaining(); + return query(query, bindVars, asQueryOptions(options), entityClass).asListRemaining(); } @Override - public Iterable findAll(final Iterable ids, final Class entityClass) + public Iterable findAll(final Iterable ids, final Class entityClass, DocumentReadOptions options) throws DataAccessException { try { final Collection keys = new ArrayList<>(); ids.forEach(id -> keys.add(determineDocumentKeyFromId(id))); - Collection docs = _collection(entityClass).getDocuments(keys, entityClass).getDocuments(); + Collection docs = _collection(entityClass).getDocuments(keys, entityClass).getDocuments(); for (T doc : docs) { if (doc != null) { potentiallyEmitEvent(new AfterLoadEvent<>(doc)); @@ -544,12 +480,6 @@ public MultiDocumentEntity> insertAll( return result; } - @Override - @SuppressWarnings({"unchecked", "rawtypes"}) - public MultiDocumentEntity> insertAll(Iterable values, Class entityClass) throws DataAccessException { - return insertAll(values, new DocumentCreateOptions(), (Class) entityClass); - } - @Override public DocumentCreateEntity insert(final T value, final DocumentCreateOptions options) throws DataAccessException { potentiallyEmitEvent(new BeforeSaveEvent<>(value)); @@ -567,12 +497,7 @@ public DocumentCreateEntity insert(final T value, final DocumentCreateOpt } @Override - public DocumentCreateEntity insert(final Object value) throws DataAccessException { - return insert(value, new DocumentCreateOptions()); - } - - @Override - public T repsert(final T value) throws DataAccessException { + public T repsert(final T value, AqlQueryOptions options) throws DataAccessException { @SuppressWarnings("unchecked") final Class clazz = (Class) value.getClass(); final String collectionName = _collection(clazz).name(); @@ -587,7 +512,7 @@ public T repsert(final T value) throws DataAccessException { ArangoCursor it = query( REPSERT_QUERY, bindVars, - clazz + options, clazz ); result = it.hasNext() ? it.next() : null; } catch (final ArangoDBException e) { @@ -601,7 +526,7 @@ public T repsert(final T value) throws DataAccessException { @SuppressWarnings({"rawtypes", "unchecked"}) @Override - public Iterable repsertAll(final Iterable values, final Class entityClass) throws DataAccessException { + public Iterable repsertAll(final Iterable values, final Class entityClass, AqlQueryOptions options) throws DataAccessException { if (!values.iterator().hasNext()) { return Collections.emptyList(); } @@ -618,7 +543,7 @@ public Iterable repsertAll(final Iterable values, final Class entityClass) throws DataAccessException { + public boolean exists(final Object id, final Class entityClass, DocumentExistsOptions options) throws DataAccessException { try { - return _collection(entityClass).documentExists(determineDocumentKeyFromId(id)); + return _collection(entityClass).documentExists(determineDocumentKeyFromId(id), options); } catch (final ArangoDBException e) { throw translateException(e); } @@ -731,11 +656,6 @@ public CollectionOperations collection(final Class entityClass) throws DataAc return collection(_collection(entityClass)); } - @Override - public CollectionOperations collection(final String name) throws DataAccessException { - return collection(_collection(name)); - } - @Override public CollectionOperations collection(final String name, final CollectionCreateOptions options) throws DataAccessException { @@ -831,4 +751,8 @@ private List toList(Iterable it) { it.forEach(l::add); return l; } + + private AqlQueryOptions asQueryOptions(DocumentReadOptions source) { + return new AqlQueryOptions().streamTransactionId(source.getStreamTransactionId()).allowDirtyRead(source.getAllowDirtyRead()); + } } From 55241c1be57104a7a0c2924161d6539dbb9056e2 Mon Sep 17 00:00:00 2001 From: Arne Burmeister Date: Tue, 17 May 2022 13:41:34 +0200 Subject: [PATCH 10/55] #80 allow commit/rollback if not started yet --- .../springframework/transaction/ArangoTransaction.java | 9 ++++++--- .../transaction/ArangoTransactionManager.java | 7 ++++++- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/arangodb/springframework/transaction/ArangoTransaction.java b/src/main/java/com/arangodb/springframework/transaction/ArangoTransaction.java index 316aff05e..6cb1dbd0d 100644 --- a/src/main/java/com/arangodb/springframework/transaction/ArangoTransaction.java +++ b/src/main/java/com/arangodb/springframework/transaction/ArangoTransaction.java @@ -16,7 +16,6 @@ class ArangoTransaction implements SmartTransactionObject { private final ArangoDatabase database; - private final Set writeCollections = new HashSet<>(); private TransactionDefinition definition; private StreamTransactionEntity transaction; @@ -53,11 +52,15 @@ String getOrBegin(Collection collections) { } void commit() { - database.commitStreamTransaction(transaction.getId()); + if (transaction != null && transaction.getStatus() == StreamTransactionStatus.running) { + database.commitStreamTransaction(transaction.getId()); + } } void rollback() { - database.abortStreamTransaction(transaction.getId()); + if (transaction != null && transaction.getStatus() == StreamTransactionStatus.running) { + database.abortStreamTransaction(transaction.getId()); + } } @Override diff --git a/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManager.java b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManager.java index b13749272..675645e8e 100644 --- a/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManager.java +++ b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManager.java @@ -1,6 +1,7 @@ package com.arangodb.springframework.transaction; import com.arangodb.ArangoDatabase; +import com.arangodb.DbName; import com.arangodb.model.StreamTransactionOptions; import com.arangodb.springframework.core.ArangoOperations; import com.arangodb.springframework.repository.query.QueryTransactionBridge; @@ -32,7 +33,11 @@ public ArangoTransactionManager(ArangoOperations operations, QueryTransactionBri @Override protected Object doGetTransaction() throws TransactionException { - return new ArangoTransaction(operations.driver().db(operations.getDatabaseName())); + DbName database = operations.getDatabaseName(); + if (logger.isDebugEnabled()) { + logger.debug("Create new transaction for database " + database); + } + return new ArangoTransaction(operations.driver().db(database)); } @Override From adc9ccf5f3604e916dfb834b4783e9b4f1aa0c18 Mon Sep 17 00:00:00 2001 From: Arne Burmeister Date: Tue, 17 May 2022 13:42:53 +0200 Subject: [PATCH 11/55] #80 inject bridge to simple repo --- .../springframework/repository/ArangoRepositoryFactory.java | 4 ++-- .../springframework/repository/SimpleArangoRepository.java | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/arangodb/springframework/repository/ArangoRepositoryFactory.java b/src/main/java/com/arangodb/springframework/repository/ArangoRepositoryFactory.java index 481206084..787249c0e 100644 --- a/src/main/java/com/arangodb/springframework/repository/ArangoRepositoryFactory.java +++ b/src/main/java/com/arangodb/springframework/repository/ArangoRepositoryFactory.java @@ -87,7 +87,7 @@ public ArangoEntityInformation getEntityInformation(final Class getQueryLookupStrategy( QueryLookupStrategy strategy = null; switch (key) { case CREATE_IF_NOT_FOUND: - strategy = new DefaultArangoQueryLookupStrategy(arangoTemplate, applicationContext); + strategy = new DefaultArangoQueryLookupStrategy(arangoTemplate, transactionBridge, applicationContext); break; case CREATE: break; diff --git a/src/main/java/com/arangodb/springframework/repository/SimpleArangoRepository.java b/src/main/java/com/arangodb/springframework/repository/SimpleArangoRepository.java index 2ccc1c0a2..88c99d79b 100644 --- a/src/main/java/com/arangodb/springframework/repository/SimpleArangoRepository.java +++ b/src/main/java/com/arangodb/springframework/repository/SimpleArangoRepository.java @@ -33,6 +33,7 @@ import com.arangodb.springframework.core.mapping.ArangoPersistentEntity; import com.arangodb.springframework.core.template.ArangoTemplate; import com.arangodb.springframework.core.util.AqlUtils; +import com.arangodb.springframework.repository.query.QueryTransactionBridge; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.dao.OptimisticLockingFailureException; @@ -62,6 +63,7 @@ public class SimpleArangoRepository implements ArangoRepository { private final Class domainClass; private final boolean returnOriginalEntities; private final ArangoPersistentEntity persistentEntity; + private final QueryTransactionBridge transactionBridge; /** * @param arangoTemplate The template used to execute much of the @@ -69,12 +71,14 @@ public class SimpleArangoRepository implements ArangoRepository { * @param domainClass the class type of this repository * @param returnOriginalEntities whether save and saveAll should return the * original entities or new ones + * @param transactionBridge the optional transaction bridge */ - public SimpleArangoRepository(final ArangoTemplate arangoTemplate, final Class domainClass, boolean returnOriginalEntities) { + public SimpleArangoRepository(final ArangoTemplate arangoTemplate, final Class domainClass, boolean returnOriginalEntities, final QueryTransactionBridge transactionBridge) { super(); this.arangoTemplate = arangoTemplate; this.domainClass = domainClass; this.returnOriginalEntities = returnOriginalEntities; + this.transactionBridge = transactionBridge; converter = arangoTemplate.getConverter(); mappingContext = (ArangoMappingContext) converter.getMappingContext(); exampleConverter = new ArangoExampleConverter(mappingContext, arangoTemplate.getResolverFactory()); From bb4bef5b607d5b2579e5048c510729f102d97485 Mon Sep 17 00:00:00 2001 From: Arne Burmeister Date: Tue, 17 May 2022 13:46:13 +0200 Subject: [PATCH 12/55] #80 use options with transaction id where ever possible --- .../repository/SimpleArangoRepository.java | 58 +++++++++++++++---- ...rangoTransactionManagerRepositoryTest.java | 4 +- 2 files changed, 48 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/arangodb/springframework/repository/SimpleArangoRepository.java b/src/main/java/com/arangodb/springframework/repository/SimpleArangoRepository.java index 88c99d79b..9dcdfa722 100644 --- a/src/main/java/com/arangodb/springframework/repository/SimpleArangoRepository.java +++ b/src/main/java/com/arangodb/springframework/repository/SimpleArangoRepository.java @@ -27,6 +27,8 @@ import com.arangodb.entity.MultiDocumentEntity; import com.arangodb.model.AqlQueryOptions; import com.arangodb.model.DocumentDeleteOptions; +import com.arangodb.model.DocumentExistsOptions; +import com.arangodb.model.DocumentReadOptions; import com.arangodb.springframework.core.DocumentNotFoundException; import com.arangodb.springframework.core.convert.ArangoConverter; import com.arangodb.springframework.core.mapping.ArangoMappingContext; @@ -93,7 +95,7 @@ public SimpleArangoRepository(final ArangoTemplate arangoTemplate, final Class S save(final S entity) { - S saved = arangoTemplate.repsert(entity); + S saved = arangoTemplate.repsert(entity, defaultQueryOptions()); return returnOriginalEntities ? entity : saved; } @@ -106,7 +108,7 @@ public S save(final S entity) { */ @Override public Iterable saveAll(final Iterable entities) { - Iterable saved = arangoTemplate.repsertAll(entities, domainClass); + Iterable saved = arangoTemplate.repsertAll(entities, domainClass, defaultQueryOptions()); return returnOriginalEntities ? entities : saved; } @@ -118,7 +120,7 @@ public Iterable saveAll(final Iterable entities) { */ @Override public Optional findById(final ID id) { - return arangoTemplate.find(id, domainClass); + return arangoTemplate.find(id, domainClass, defaultReadOptions()); } /** @@ -129,7 +131,7 @@ public Optional findById(final ID id) { */ @Override public boolean existsById(final ID id) { - return arangoTemplate.exists(id, domainClass); + return arangoTemplate.exists(id, domainClass, defaultExistsOptions()); } /** @@ -139,7 +141,7 @@ public boolean existsById(final ID id) { */ @Override public Iterable findAll() { - return arangoTemplate.findAll(domainClass); + return arangoTemplate.findAll(domainClass, defaultReadOptions()); } /** @@ -151,7 +153,7 @@ public Iterable findAll() { */ @Override public Iterable findAllById(final Iterable ids) { - return arangoTemplate.findAll(ids, domainClass); + return arangoTemplate.findAll(ids, domainClass, defaultReadOptions()); } /** @@ -173,7 +175,7 @@ public long count() { @Override public void deleteById(final ID id) { try { - arangoTemplate.delete(id, domainClass); + arangoTemplate.delete(id, domainClass, defaultDeleteOptions()); } catch (DocumentNotFoundException unknown) { // silently ignored } @@ -195,7 +197,7 @@ public void delete(final T entity) { .ifPresent(opts::ifMatch); try { - arangoTemplate.delete(id, opts, domainClass); + arangoTemplate.delete(id, opts, domainClass, defaultDeleteOptions()); } catch (DocumentNotFoundException e) { throw new OptimisticLockingFailureException(e.getMessage(), e); } @@ -206,7 +208,7 @@ public void delete(final T entity) { * @implNote do not add @Override annotation to keep backwards compatibility with spring-data-commons 2.4 */ public void deleteAllById(Iterable ids) { - MultiDocumentEntity> res = arangoTemplate.deleteAllById(ids, domainClass); + MultiDocumentEntity> res = arangoTemplate.deleteAllById(ids, domainClass, defaultDeleteOptions()); for (ErrorEntity error : res.getErrors()) { // Entities that aren't found in the persistence store are silently ignored. if (error.getErrorNum() != 1202) { @@ -347,7 +349,7 @@ public long count(final Example example) { final String filter = predicate.length() == 0 ? "" : " FILTER " + predicate; final String query = String.format("FOR e IN @@col %s COLLECT WITH COUNT INTO length RETURN length", filter); arangoTemplate.collection(domainClass); - final ArangoCursor cursor = arangoTemplate.query(query, bindVars, null, Long.class); + final ArangoCursor cursor = arangoTemplate.query(query, bindVars, defaultQueryOptions(), Long.class); return cursor.next(); } @@ -373,7 +375,7 @@ private ArangoCursor findAllInternal(final Sort sort, @Nullable final String query = String.format("FOR e IN @@col %s %s RETURN e", buildFilterClause(example, bindVars), buildSortClause(sort, "e")); arangoTemplate.collection(domainClass); - return arangoTemplate.query(query, bindVars, null, domainClass); + return arangoTemplate.query(query, bindVars, defaultQueryOptions(), domainClass); } private ArangoCursor findAllInternal(final Pageable pageable, @Nullable final Example example, @@ -383,7 +385,7 @@ private ArangoCursor findAllInternal(final Pageable pageable, @ buildFilterClause(example, bindVars), buildPageableClause(pageable, "e")); arangoTemplate.collection(domainClass); return arangoTemplate.query(query, bindVars, - pageable != null ? new AqlQueryOptions().fullCount(true) : null, domainClass); + pageable != null ? new AqlQueryOptions().fullCount(true) : defaultQueryOptions(), domainClass); } private String buildFilterClause(final Example example, final Map bindVars) { @@ -411,4 +413,36 @@ private String buildSortClause(final Sort sort, final String varName) { return sort == null ? "" : AqlUtils.buildSortClause(AqlUtils.toPersistentSort(sort, mappingContext, domainClass), varName); } + private DocumentReadOptions defaultReadOptions() { + DocumentReadOptions options = new DocumentReadOptions(); + if (transactionBridge != null) { + options.streamTransactionId(transactionBridge.getCurrentTransaction(Collections.singleton(getCollectionName()))); + } + return options; + } + + private AqlQueryOptions defaultQueryOptions() { + AqlQueryOptions options = new AqlQueryOptions(); + if (transactionBridge != null) { + options.streamTransactionId(transactionBridge.getCurrentTransaction(Collections.singleton(getCollectionName()))); + } + return options; + } + + private DocumentExistsOptions defaultExistsOptions() { + DocumentExistsOptions options = new DocumentExistsOptions(); + if (transactionBridge != null) { + options.streamTransactionId(transactionBridge.getCurrentTransaction(Collections.singleton(getCollectionName()))); + } + return options; + } + + private DocumentDeleteOptions defaultDeleteOptions() { + DocumentDeleteOptions options = new DocumentDeleteOptions(); + if (transactionBridge != null) { + options.streamTransactionId(transactionBridge.getCurrentTransaction(Collections.singleton(getCollectionName()))); + } + return options; + } + } diff --git a/src/test/java/com/arangodb/springframework/transaction/ArangoTransactionManagerRepositoryTest.java b/src/test/java/com/arangodb/springframework/transaction/ArangoTransactionManagerRepositoryTest.java index 777245c9d..41cdc7b36 100644 --- a/src/test/java/com/arangodb/springframework/transaction/ArangoTransactionManagerRepositoryTest.java +++ b/src/test/java/com/arangodb/springframework/transaction/ArangoTransactionManagerRepositoryTest.java @@ -22,8 +22,8 @@ public class ArangoTransactionManagerRepositoryTest extends AbstractArangoTest { private HumanBeingRepository humanBeingRepository; @Before - public void setUp() { - humanBeingRepository.deleteAll(); + public void cleanupDatabase() { + template.collection(HumanBeing.class).truncate(); } @Test From 20c935af350cc340e81aed1fe971b51836ccf27b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Malte=20J=C3=B6rgens?= Date: Wed, 18 May 2022 10:32:22 +0200 Subject: [PATCH 13/55] #80 use another document for test --- .../repository/ActorRepository.java | 6 +++ .../repository/MovieRepository.java | 6 +++ ...rangoTransactionManagerRepositoryTest.java | 41 ++++++++++--------- 3 files changed, 34 insertions(+), 19 deletions(-) create mode 100644 src/test/java/com/arangodb/springframework/repository/ActorRepository.java create mode 100644 src/test/java/com/arangodb/springframework/repository/MovieRepository.java diff --git a/src/test/java/com/arangodb/springframework/repository/ActorRepository.java b/src/test/java/com/arangodb/springframework/repository/ActorRepository.java new file mode 100644 index 000000000..374cead81 --- /dev/null +++ b/src/test/java/com/arangodb/springframework/repository/ActorRepository.java @@ -0,0 +1,6 @@ +package com.arangodb.springframework.repository; + +import com.arangodb.springframework.testdata.Actor; + +public interface ActorRepository extends ArangoRepository { +} diff --git a/src/test/java/com/arangodb/springframework/repository/MovieRepository.java b/src/test/java/com/arangodb/springframework/repository/MovieRepository.java new file mode 100644 index 000000000..4ea6d82b2 --- /dev/null +++ b/src/test/java/com/arangodb/springframework/repository/MovieRepository.java @@ -0,0 +1,6 @@ +package com.arangodb.springframework.repository; + +import com.arangodb.springframework.testdata.Movie; + +public interface MovieRepository extends ArangoRepository { +} diff --git a/src/test/java/com/arangodb/springframework/transaction/ArangoTransactionManagerRepositoryTest.java b/src/test/java/com/arangodb/springframework/transaction/ArangoTransactionManagerRepositoryTest.java index 41cdc7b36..5c0d9b90f 100644 --- a/src/test/java/com/arangodb/springframework/transaction/ArangoTransactionManagerRepositoryTest.java +++ b/src/test/java/com/arangodb/springframework/transaction/ArangoTransactionManagerRepositoryTest.java @@ -2,9 +2,10 @@ import com.arangodb.springframework.AbstractArangoTest; import com.arangodb.springframework.ArangoTransactionalTestConfiguration; -import com.arangodb.springframework.repository.HumanBeingRepository; -import com.arangodb.springframework.testdata.HumanBeing; -import org.junit.Before; +import com.arangodb.springframework.repository.ActorRepository; +import com.arangodb.springframework.repository.MovieRepository; +import com.arangodb.springframework.testdata.Actor; +import com.arangodb.springframework.testdata.Movie; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; @@ -16,29 +17,34 @@ @ContextConfiguration(classes = { ArangoTransactionalTestConfiguration.class }) public class ArangoTransactionManagerRepositoryTest extends AbstractArangoTest { - private final HumanBeing anakin = new HumanBeing("Anakin", "Skywalker", false); + public ArangoTransactionManagerRepositoryTest() { + super(Movie.class, Actor.class); + } @Autowired - private HumanBeingRepository humanBeingRepository; + private MovieRepository movieRepository; + @Autowired + private ActorRepository actorRepository; - @Before - public void cleanupDatabase() { - template.collection(HumanBeing.class).truncate(); + Movie starWars = new Movie(); + + { + starWars.setName("Star Wars"); } @Test public void shouldWorkWithoutTransaction() { - humanBeingRepository.save(anakin); + movieRepository.save(starWars); - assertThat(humanBeingRepository.findByNameAndSurname(anakin.getName(), anakin.getSurname())).isPresent(); + assertThat(movieRepository.findById(starWars.getId())).isPresent(); } @Test @Transactional public void shouldWorkWithinTransaction() { - humanBeingRepository.save(anakin); + movieRepository.save(starWars); - assertThat(humanBeingRepository.findByNameAndSurname(anakin.getName(), anakin.getSurname())).isPresent(); + assertThat(movieRepository.findById(starWars.getId())).isPresent(); } @Test @@ -46,21 +52,18 @@ public void shouldWorkWithinTransaction() { public void shouldWorkAfterTransaction() { TestTransaction.flagForCommit(); - humanBeingRepository.save(anakin); - - assertThat(TestTransaction.isFlaggedForRollback()).isFalse(); + movieRepository.save(starWars); TestTransaction.end(); - assertThat(humanBeingRepository.findByNameAndSurname(anakin.getName(), anakin.getSurname())).isPresent(); + assertThat(movieRepository.findById(starWars.getId())).isPresent(); } @Test @Transactional public void shouldRollbackWithinTransaction() { - humanBeingRepository.save(anakin); - TestTransaction.flagForRollback(); + movieRepository.save(starWars); TestTransaction.end(); - assertThat(humanBeingRepository.findByNameAndSurname(anakin.getName(), anakin.getSurname())).isNotPresent(); + assertThat(movieRepository.findById(starWars.getId())).isNotPresent(); } } From 69ed72a998a05bfea517298005f3613694c2885b Mon Sep 17 00:00:00 2001 From: Arne Burmeister Date: Fri, 26 Aug 2022 18:05:09 +0200 Subject: [PATCH 14/55] #80 improve error message --- .../springframework/transaction/ArangoTransaction.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/arangodb/springframework/transaction/ArangoTransaction.java b/src/main/java/com/arangodb/springframework/transaction/ArangoTransaction.java index 6cb1dbd0d..eb22c2897 100644 --- a/src/main/java/com/arangodb/springframework/transaction/ArangoTransaction.java +++ b/src/main/java/com/arangodb/springframework/transaction/ArangoTransaction.java @@ -38,7 +38,9 @@ void configure(TransactionDefinition definition) { String getOrBegin(Collection collections) { if (transaction != null) { if (!writeCollections.containsAll(collections)) { - throw new IllegalTransactionStateException("Stream transaction already started, no additional collections allowed"); + Set additional = new HashSet<>(collections); + additional.removeAll(writeCollections); + throw new IllegalTransactionStateException("Stream transaction already started on collections " + writeCollections + ", no additional collections allowed: " + additional); } return transaction.getId(); } From a567c05ff22757f4e175607165375367745500c9 Mon Sep 17 00:00:00 2001 From: Arne Burmeister Date: Fri, 26 Aug 2022 18:26:22 +0200 Subject: [PATCH 15/55] #80 do not add collections of abstract entity types --- .../repository/query/StringBasedArangoQuery.java | 9 ++++++--- .../springframework/transaction/ArangoTransaction.java | 6 ++++++ .../transaction/ArangoTransactionManager.java | 8 +------- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/arangodb/springframework/repository/query/StringBasedArangoQuery.java b/src/main/java/com/arangodb/springframework/repository/query/StringBasedArangoQuery.java index 12f4db781..eaddd3314 100644 --- a/src/main/java/com/arangodb/springframework/repository/query/StringBasedArangoQuery.java +++ b/src/main/java/com/arangodb/springframework/repository/query/StringBasedArangoQuery.java @@ -36,6 +36,7 @@ import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.util.Assert; +import java.lang.reflect.Modifier; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -98,12 +99,14 @@ public StringBasedArangoQuery(final String query, final ArangoQueryMethod method extractBindVars(accessor, bindVars); - return Pair.of(prepareQuery(accessor), allCollectionNames(collectionName, bindVars)); + return Pair.of(prepareQuery(accessor), allCollectionNames(bindVars)); } - private Collection allCollectionNames(String collectionName, Map bindVars) { + private Collection allCollectionNames(Map bindVars) { HashSet allCollections = new HashSet<>(); - allCollections.add(collectionName); + if (!Modifier.isAbstract(domainClass.getModifiers())) { + allCollections.add(collectionName); + } bindVars.entrySet().stream() .filter(entry -> entry.getKey().startsWith("@")) .map(Map.Entry::getValue) diff --git a/src/main/java/com/arangodb/springframework/transaction/ArangoTransaction.java b/src/main/java/com/arangodb/springframework/transaction/ArangoTransaction.java index eb22c2897..e0b00baf6 100644 --- a/src/main/java/com/arangodb/springframework/transaction/ArangoTransaction.java +++ b/src/main/java/com/arangodb/springframework/transaction/ArangoTransaction.java @@ -4,6 +4,8 @@ import com.arangodb.entity.StreamTransactionEntity; import com.arangodb.entity.StreamTransactionStatus; import com.arangodb.model.StreamTransactionOptions; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.springframework.transaction.IllegalTransactionStateException; import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.interceptor.TransactionAttribute; @@ -15,6 +17,7 @@ class ArangoTransaction implements SmartTransactionObject { + private final Log logger = LogFactory.getLog(getClass()); private final ArangoDatabase database; private final Set writeCollections = new HashSet<>(); private TransactionDefinition definition; @@ -50,6 +53,9 @@ String getOrBegin(Collection collections) { .writeCollections(writeCollections.toArray(new String[0])) .lockTimeout(definition.getTimeout() == -1 ? 0 : definition.getTimeout()); transaction = database.beginStreamTransaction(options); + if (logger.isDebugEnabled()) { + logger.debug("Began stream transaction " + transaction.getId() + " writing collections " + writeCollections); + } return transaction.getId(); } diff --git a/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManager.java b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManager.java index 675645e8e..97d944a8b 100644 --- a/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManager.java +++ b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManager.java @@ -48,13 +48,7 @@ protected void doBegin(Object transaction, TransactionDefinition definition) thr } ArangoTransaction tx = (ArangoTransaction) transaction; tx.configure(definition); - Function, String> begin = tx::getOrBegin; - bridge.setCurrentTransaction(begin.andThen(id -> { - if (logger.isDebugEnabled()) { - logger.debug("Began stream transaction " + id); - } - return id; - })); + bridge.setCurrentTransaction(tx::getOrBegin); } @Override From 0c4cb88c10edc3234ff0ae7457d9ae0c91b1b09a Mon Sep 17 00:00:00 2001 From: Arne Burmeister Date: Mon, 29 Aug 2022 12:45:00 +0200 Subject: [PATCH 16/55] #80 refactor resolvers: move template up --- .../core/convert/resolver/DocumentFromResolver.java | 6 +++--- .../core/convert/resolver/DocumentToResolver.java | 6 +++--- .../core/convert/resolver/EdgeFromResolver.java | 5 +---- .../core/convert/resolver/EdgeToResolver.java | 5 +---- .../springframework/core/convert/resolver/RefResolver.java | 6 +++--- .../core/convert/resolver/RelationsResolver.java | 6 +++--- 6 files changed, 14 insertions(+), 20 deletions(-) diff --git a/src/main/java/com/arangodb/springframework/core/convert/resolver/DocumentFromResolver.java b/src/main/java/com/arangodb/springframework/core/convert/resolver/DocumentFromResolver.java index 4f32a8ccb..e04bac825 100644 --- a/src/main/java/com/arangodb/springframework/core/convert/resolver/DocumentFromResolver.java +++ b/src/main/java/com/arangodb/springframework/core/convert/resolver/DocumentFromResolver.java @@ -38,11 +38,11 @@ */ public class DocumentFromResolver extends AbstractResolver implements RelationResolver { - private final ArangoOperations template; + private final ArangoOperations template; public DocumentFromResolver(final ArangoOperations template) { - super(template.getConverter().getConversionService()); - this.template = template; + super(template.getConverter().getConversionService()); + this.template = template; } @Override diff --git a/src/main/java/com/arangodb/springframework/core/convert/resolver/DocumentToResolver.java b/src/main/java/com/arangodb/springframework/core/convert/resolver/DocumentToResolver.java index e8440a4e3..b86cf9f9a 100644 --- a/src/main/java/com/arangodb/springframework/core/convert/resolver/DocumentToResolver.java +++ b/src/main/java/com/arangodb/springframework/core/convert/resolver/DocumentToResolver.java @@ -38,11 +38,11 @@ */ public class DocumentToResolver extends AbstractResolver implements RelationResolver { - private final ArangoOperations template; + private final ArangoOperations template; public DocumentToResolver(final ArangoOperations template) { - super(template.getConverter().getConversionService()); - this.template = template; + super(template.getConverter().getConversionService()); + this.template = template; } @Override diff --git a/src/main/java/com/arangodb/springframework/core/convert/resolver/EdgeFromResolver.java b/src/main/java/com/arangodb/springframework/core/convert/resolver/EdgeFromResolver.java index a1dcc0837..d8a6d6c97 100644 --- a/src/main/java/com/arangodb/springframework/core/convert/resolver/EdgeFromResolver.java +++ b/src/main/java/com/arangodb/springframework/core/convert/resolver/EdgeFromResolver.java @@ -33,11 +33,8 @@ */ public class EdgeFromResolver extends AbstractResolver implements RelationResolver { - private final ArangoOperations template; - public EdgeFromResolver(final ArangoOperations template) { - super(template.getConverter().getConversionService()); - this.template = template; + super(template); } @Override diff --git a/src/main/java/com/arangodb/springframework/core/convert/resolver/EdgeToResolver.java b/src/main/java/com/arangodb/springframework/core/convert/resolver/EdgeToResolver.java index 69e92369a..21ad99226 100644 --- a/src/main/java/com/arangodb/springframework/core/convert/resolver/EdgeToResolver.java +++ b/src/main/java/com/arangodb/springframework/core/convert/resolver/EdgeToResolver.java @@ -33,11 +33,8 @@ */ public class EdgeToResolver extends AbstractResolver implements RelationResolver { - private final ArangoOperations template; - public EdgeToResolver(final ArangoOperations template) { - super(template.getConverter().getConversionService()); - this.template = template; + super(template); } @Override diff --git a/src/main/java/com/arangodb/springframework/core/convert/resolver/RefResolver.java b/src/main/java/com/arangodb/springframework/core/convert/resolver/RefResolver.java index 6dfc80586..1b43b3460 100644 --- a/src/main/java/com/arangodb/springframework/core/convert/resolver/RefResolver.java +++ b/src/main/java/com/arangodb/springframework/core/convert/resolver/RefResolver.java @@ -37,11 +37,11 @@ */ public class RefResolver extends AbstractResolver implements ReferenceResolver { - private final ArangoOperations template; + private final ArangoOperations template; public RefResolver(final ArangoOperations template) { - super(template.getConverter().getConversionService()); - this.template = template; + super(template.getConverter().getConversionService()); + this.template = template; } @Override diff --git a/src/main/java/com/arangodb/springframework/core/convert/resolver/RelationsResolver.java b/src/main/java/com/arangodb/springframework/core/convert/resolver/RelationsResolver.java index 7a69c8929..7941aa4a4 100644 --- a/src/main/java/com/arangodb/springframework/core/convert/resolver/RelationsResolver.java +++ b/src/main/java/com/arangodb/springframework/core/convert/resolver/RelationsResolver.java @@ -36,11 +36,11 @@ */ public class RelationsResolver extends AbstractResolver implements RelationResolver { - private final ArangoOperations template; + private final ArangoOperations template; public RelationsResolver(final ArangoOperations template) { - super(template.getConverter().getConversionService()); - this.template = template; + super(template.getConverter().getConversionService()); + this.template = template; } @Override From 3282e9d7071140808790fef3cf63ca4d447c85c1 Mon Sep 17 00:00:00 2001 From: Arne Burmeister Date: Mon, 29 Aug 2022 12:46:48 +0200 Subject: [PATCH 17/55] #80 refactor resolvers: prepare read and query options --- .../core/convert/resolver/AbstractResolver.java | 10 ++++++++++ .../core/convert/resolver/DocumentFromResolver.java | 3 +-- .../core/convert/resolver/DocumentToResolver.java | 3 +-- .../core/convert/resolver/EdgeFromResolver.java | 2 +- .../core/convert/resolver/EdgeToResolver.java | 2 +- .../core/convert/resolver/RefResolver.java | 2 +- .../core/convert/resolver/RelationsResolver.java | 2 +- 7 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/arangodb/springframework/core/convert/resolver/AbstractResolver.java b/src/main/java/com/arangodb/springframework/core/convert/resolver/AbstractResolver.java index a98372dcd..6ae874e3f 100644 --- a/src/main/java/com/arangodb/springframework/core/convert/resolver/AbstractResolver.java +++ b/src/main/java/com/arangodb/springframework/core/convert/resolver/AbstractResolver.java @@ -93,6 +93,16 @@ private Class enhancedTypeFor(final Class type) { return enhancer.createClass(); } + protected DocumentReadOptions defaultReadOptions() { + DocumentReadOptions options = new DocumentReadOptions(); + return options; + } + + protected AqlQueryOptions defaultQueryOptions() { + AqlQueryOptions options = new AqlQueryOptions(); + return options; + } + static class ProxyInterceptor implements Serializable, org.springframework.cglib.proxy.MethodInterceptor, org.aopalliance.intercept.MethodInterceptor { diff --git a/src/main/java/com/arangodb/springframework/core/convert/resolver/DocumentFromResolver.java b/src/main/java/com/arangodb/springframework/core/convert/resolver/DocumentFromResolver.java index e04bac825..a22702692 100644 --- a/src/main/java/com/arangodb/springframework/core/convert/resolver/DocumentFromResolver.java +++ b/src/main/java/com/arangodb/springframework/core/convert/resolver/DocumentFromResolver.java @@ -23,7 +23,6 @@ import org.springframework.data.util.TypeInformation; import com.arangodb.ArangoCursor; -import com.arangodb.model.AqlQueryOptions; import com.arangodb.springframework.annotation.From; import com.arangodb.springframework.core.ArangoOperations; @@ -73,7 +72,7 @@ private ArangoCursor _resolve(final String id, final Class type, final boo Map bindVars = new HashMap<>(); bindVars.put("@edge", type); bindVars.put("id", id); - return template.query(query, bindVars, new AqlQueryOptions(), type); + return template.query(query, bindVars, defaultQueryOptions(), type); } } diff --git a/src/main/java/com/arangodb/springframework/core/convert/resolver/DocumentToResolver.java b/src/main/java/com/arangodb/springframework/core/convert/resolver/DocumentToResolver.java index b86cf9f9a..cc4f5702a 100644 --- a/src/main/java/com/arangodb/springframework/core/convert/resolver/DocumentToResolver.java +++ b/src/main/java/com/arangodb/springframework/core/convert/resolver/DocumentToResolver.java @@ -23,7 +23,6 @@ import org.springframework.data.util.TypeInformation; import com.arangodb.ArangoCursor; -import com.arangodb.model.AqlQueryOptions; import com.arangodb.springframework.annotation.To; import com.arangodb.springframework.core.ArangoOperations; @@ -73,7 +72,7 @@ private ArangoCursor _resolve(final String id, final Class type, final boo Map bindVars = new HashMap<>(); bindVars.put("@edge", type); bindVars.put("id", id); - return template.query(query, bindVars, new AqlQueryOptions(), type); + return template.query(query, bindVars, defaultQueryOptions(), type); } } diff --git a/src/main/java/com/arangodb/springframework/core/convert/resolver/EdgeFromResolver.java b/src/main/java/com/arangodb/springframework/core/convert/resolver/EdgeFromResolver.java index d8a6d6c97..1e0ed1a79 100644 --- a/src/main/java/com/arangodb/springframework/core/convert/resolver/EdgeFromResolver.java +++ b/src/main/java/com/arangodb/springframework/core/convert/resolver/EdgeFromResolver.java @@ -45,7 +45,7 @@ public Object resolveOne(final String id, final TypeInformation type, Collect } private Object _resolveOne(final String id, final TypeInformation type) { - return template.find(id, type.getType()) + return template.find(id, type.getType(), defaultReadOptions()) .orElseThrow(() -> cannotResolveException(id, type)); } diff --git a/src/main/java/com/arangodb/springframework/core/convert/resolver/EdgeToResolver.java b/src/main/java/com/arangodb/springframework/core/convert/resolver/EdgeToResolver.java index 21ad99226..f40a74111 100644 --- a/src/main/java/com/arangodb/springframework/core/convert/resolver/EdgeToResolver.java +++ b/src/main/java/com/arangodb/springframework/core/convert/resolver/EdgeToResolver.java @@ -45,7 +45,7 @@ public Object resolveOne(final String id, final TypeInformation type, Collect } private Object _resolveOne(final String id, final TypeInformation type) { - return template.find(id, type.getType()) + return template.find(id, type.getType(), defaultReadOptions()) .orElseThrow(() -> cannotResolveException(id, type)); } diff --git a/src/main/java/com/arangodb/springframework/core/convert/resolver/RefResolver.java b/src/main/java/com/arangodb/springframework/core/convert/resolver/RefResolver.java index 1b43b3460..6a5ec5835 100644 --- a/src/main/java/com/arangodb/springframework/core/convert/resolver/RefResolver.java +++ b/src/main/java/com/arangodb/springframework/core/convert/resolver/RefResolver.java @@ -58,7 +58,7 @@ public Object resolveMultiple(final Collection ids, final TypeInformatio } private Object _resolve(final String id, final TypeInformation type) { - return template.find(id, type.getType()) + return template.find(id, type.getType(), defaultReadOptions()) .orElseThrow(() -> cannotResolveException(id, type)); } diff --git a/src/main/java/com/arangodb/springframework/core/convert/resolver/RelationsResolver.java b/src/main/java/com/arangodb/springframework/core/convert/resolver/RelationsResolver.java index 7941aa4a4..a64259a4f 100644 --- a/src/main/java/com/arangodb/springframework/core/convert/resolver/RelationsResolver.java +++ b/src/main/java/com/arangodb/springframework/core/convert/resolver/RelationsResolver.java @@ -108,7 +108,7 @@ private ArangoCursor _resolve( edges, // limit ? "LIMIT 1" : ""); - return template.query(query, bindVars, type); + return template.query(query, bindVars, defaultQueryOptions(), type); } } From 72e22c2564a6b535cf8c809d2bc48b258a9ce6f9 Mon Sep 17 00:00:00 2001 From: Arne Burmeister Date: Mon, 29 Aug 2022 12:52:48 +0200 Subject: [PATCH 18/55] #80 prepare usage of stream transaction bridge and id for resolvers --- .../config/ArangoConfiguration.java | 12 ++++++------ .../core/convert/resolver/AbstractResolver.java | 14 +++++++++++++- .../convert/resolver/DocumentFromResolver.java | 5 +++-- .../core/convert/resolver/DocumentToResolver.java | 7 ++++--- .../core/convert/resolver/EdgeFromResolver.java | 5 +++-- .../core/convert/resolver/EdgeToResolver.java | 5 +++-- .../core/convert/resolver/RefResolver.java | 7 ++++--- .../core/convert/resolver/RelationsResolver.java | 7 ++++--- 8 files changed, 40 insertions(+), 22 deletions(-) diff --git a/src/main/java/com/arangodb/springframework/config/ArangoConfiguration.java b/src/main/java/com/arangodb/springframework/config/ArangoConfiguration.java index 71974a951..9718adb89 100644 --- a/src/main/java/com/arangodb/springframework/config/ArangoConfiguration.java +++ b/src/main/java/com/arangodb/springframework/config/ArangoConfiguration.java @@ -164,7 +164,7 @@ public Optional> getReferenceResolve ReferenceResolver resolver = null; try { if (annotation instanceof Ref) { - resolver = (ReferenceResolver) new RefResolver(arangoTemplate()); + resolver = (ReferenceResolver) new RefResolver(arangoTemplate(), null); } } catch (final Exception e) { throw new ArangoDBException(e); @@ -180,18 +180,18 @@ public Optional> getRelationResolver( try { if (annotation instanceof From) { if (collectionType == Edge.class) { - resolver = (RelationResolver) new EdgeFromResolver(arangoTemplate()); + resolver = (RelationResolver) new EdgeFromResolver(arangoTemplate(), null); } else if (collectionType == Document.class) { - resolver = (RelationResolver) new DocumentFromResolver(arangoTemplate()); + resolver = (RelationResolver) new DocumentFromResolver(arangoTemplate(), null); } } else if (annotation instanceof To) { if (collectionType == Edge.class) { - resolver = (RelationResolver) new EdgeToResolver(arangoTemplate()); + resolver = (RelationResolver) new EdgeToResolver(arangoTemplate(), null); } else if (collectionType == Document.class) { - resolver = (RelationResolver) new DocumentToResolver(arangoTemplate()); + resolver = (RelationResolver) new DocumentToResolver(arangoTemplate(), null); } } else if (annotation instanceof Relations) { - resolver = (RelationResolver) new RelationsResolver(arangoTemplate()); + resolver = (RelationResolver) new RelationsResolver(arangoTemplate(), null); } } catch (final Exception e) { throw new ArangoDBException(e); diff --git a/src/main/java/com/arangodb/springframework/core/convert/resolver/AbstractResolver.java b/src/main/java/com/arangodb/springframework/core/convert/resolver/AbstractResolver.java index 6ae874e3f..9ba14af0c 100644 --- a/src/main/java/com/arangodb/springframework/core/convert/resolver/AbstractResolver.java +++ b/src/main/java/com/arangodb/springframework/core/convert/resolver/AbstractResolver.java @@ -22,8 +22,12 @@ import java.io.Serializable; import java.lang.reflect.Method; +import java.util.Collections; import java.util.function.Supplier; +import com.arangodb.model.AqlQueryOptions; +import com.arangodb.model.DocumentReadOptions; +import com.arangodb.springframework.repository.query.QueryTransactionBridge; import org.aopalliance.intercept.MethodInvocation; import org.springframework.aop.framework.ProxyFactory; import org.springframework.cglib.proxy.Callback; @@ -58,10 +62,12 @@ public abstract class AbstractResolver { private final ObjenesisStd objenesis; private final ConversionService conversionService; + private final QueryTransactionBridge transactionBridge; - protected AbstractResolver(final ConversionService conversionService) { + protected AbstractResolver(final ConversionService conversionService, final QueryTransactionBridge transactionBridge) { super(); this.conversionService = conversionService; + this.transactionBridge = transactionBridge; this.objenesis = new ObjenesisStd(true); } @@ -95,11 +101,17 @@ private Class enhancedTypeFor(final Class type) { protected DocumentReadOptions defaultReadOptions() { DocumentReadOptions options = new DocumentReadOptions(); + if (transactionBridge != null) { + options.streamTransactionId(transactionBridge.getCurrentTransaction(Collections.emptySet())); + } return options; } protected AqlQueryOptions defaultQueryOptions() { AqlQueryOptions options = new AqlQueryOptions(); + if (transactionBridge != null) { + options.streamTransactionId(transactionBridge.getCurrentTransaction(Collections.emptySet())); + } return options; } diff --git a/src/main/java/com/arangodb/springframework/core/convert/resolver/DocumentFromResolver.java b/src/main/java/com/arangodb/springframework/core/convert/resolver/DocumentFromResolver.java index a22702692..3b0a4228b 100644 --- a/src/main/java/com/arangodb/springframework/core/convert/resolver/DocumentFromResolver.java +++ b/src/main/java/com/arangodb/springframework/core/convert/resolver/DocumentFromResolver.java @@ -20,6 +20,7 @@ package com.arangodb.springframework.core.convert.resolver; +import com.arangodb.springframework.repository.query.QueryTransactionBridge; import org.springframework.data.util.TypeInformation; import com.arangodb.ArangoCursor; @@ -39,8 +40,8 @@ public class DocumentFromResolver extends AbstractResolver implements RelationRe private final ArangoOperations template; - public DocumentFromResolver(final ArangoOperations template) { - super(template.getConverter().getConversionService()); + public DocumentFromResolver(final ArangoOperations template, QueryTransactionBridge transactionBridge) { + super(template.getConverter().getConversionService(), transactionBridge); this.template = template; } diff --git a/src/main/java/com/arangodb/springframework/core/convert/resolver/DocumentToResolver.java b/src/main/java/com/arangodb/springframework/core/convert/resolver/DocumentToResolver.java index cc4f5702a..0d7e5e6c2 100644 --- a/src/main/java/com/arangodb/springframework/core/convert/resolver/DocumentToResolver.java +++ b/src/main/java/com/arangodb/springframework/core/convert/resolver/DocumentToResolver.java @@ -20,6 +20,7 @@ package com.arangodb.springframework.core.convert.resolver; +import com.arangodb.springframework.repository.query.QueryTransactionBridge; import org.springframework.data.util.TypeInformation; import com.arangodb.ArangoCursor; @@ -39,9 +40,9 @@ public class DocumentToResolver extends AbstractResolver implements RelationReso private final ArangoOperations template; - public DocumentToResolver(final ArangoOperations template) { - super(template.getConverter().getConversionService()); - this.template = template; + public DocumentToResolver(final ArangoOperations template, QueryTransactionBridge transactionBridge) { + super(template.getConverter().getConversionService(), transactionBridge); + this.template = template; } @Override diff --git a/src/main/java/com/arangodb/springframework/core/convert/resolver/EdgeFromResolver.java b/src/main/java/com/arangodb/springframework/core/convert/resolver/EdgeFromResolver.java index 1e0ed1a79..c8157611f 100644 --- a/src/main/java/com/arangodb/springframework/core/convert/resolver/EdgeFromResolver.java +++ b/src/main/java/com/arangodb/springframework/core/convert/resolver/EdgeFromResolver.java @@ -20,6 +20,7 @@ package com.arangodb.springframework.core.convert.resolver; +import com.arangodb.springframework.repository.query.QueryTransactionBridge; import org.springframework.data.util.TypeInformation; import com.arangodb.springframework.annotation.From; @@ -33,8 +34,8 @@ */ public class EdgeFromResolver extends AbstractResolver implements RelationResolver { - public EdgeFromResolver(final ArangoOperations template) { - super(template); + public EdgeFromResolver(final ArangoOperations template, QueryTransactionBridge transactionBridge) { + super(template, transactionBridge); } @Override diff --git a/src/main/java/com/arangodb/springframework/core/convert/resolver/EdgeToResolver.java b/src/main/java/com/arangodb/springframework/core/convert/resolver/EdgeToResolver.java index f40a74111..f5d029d20 100644 --- a/src/main/java/com/arangodb/springframework/core/convert/resolver/EdgeToResolver.java +++ b/src/main/java/com/arangodb/springframework/core/convert/resolver/EdgeToResolver.java @@ -20,6 +20,7 @@ package com.arangodb.springframework.core.convert.resolver; +import com.arangodb.springframework.repository.query.QueryTransactionBridge; import org.springframework.data.util.TypeInformation; import com.arangodb.springframework.annotation.To; @@ -33,8 +34,8 @@ */ public class EdgeToResolver extends AbstractResolver implements RelationResolver { - public EdgeToResolver(final ArangoOperations template) { - super(template); + public EdgeToResolver(final ArangoOperations template, QueryTransactionBridge transactionBridge) { + super(template, transactionBridge); } @Override diff --git a/src/main/java/com/arangodb/springframework/core/convert/resolver/RefResolver.java b/src/main/java/com/arangodb/springframework/core/convert/resolver/RefResolver.java index 6a5ec5835..c558f20bc 100644 --- a/src/main/java/com/arangodb/springframework/core/convert/resolver/RefResolver.java +++ b/src/main/java/com/arangodb/springframework/core/convert/resolver/RefResolver.java @@ -26,6 +26,7 @@ import com.arangodb.springframework.core.mapping.ArangoPersistentEntity; import com.arangodb.springframework.core.util.MetadataUtils; +import com.arangodb.springframework.repository.query.QueryTransactionBridge; import org.springframework.data.util.TypeInformation; import com.arangodb.springframework.annotation.Ref; @@ -39,9 +40,9 @@ public class RefResolver extends AbstractResolver implements ReferenceResolver Date: Mon, 29 Aug 2022 12:58:08 +0200 Subject: [PATCH 19/55] #80 extract default resolver factory to allow bean dependency, pass query bridge if available --- .../config/ArangoConfiguration.java | 62 ++---------------- .../resolver/DefaultResolverFactory.java | 63 +++++++++++++++++++ 2 files changed, 67 insertions(+), 58 deletions(-) create mode 100644 src/main/java/com/arangodb/springframework/core/convert/resolver/DefaultResolverFactory.java diff --git a/src/main/java/com/arangodb/springframework/config/ArangoConfiguration.java b/src/main/java/com/arangodb/springframework/config/ArangoConfiguration.java index 9718adb89..6a4c81d34 100644 --- a/src/main/java/com/arangodb/springframework/config/ArangoConfiguration.java +++ b/src/main/java/com/arangodb/springframework/config/ArangoConfiguration.java @@ -4,10 +4,8 @@ package com.arangodb.springframework.config; import java.io.IOException; -import java.lang.annotation.Annotation; import java.util.Collection; import java.util.Collections; -import java.util.Optional; import java.util.Set; import com.arangodb.ContentType; @@ -23,27 +21,13 @@ import org.springframework.data.mapping.model.PropertyNameFieldNamingStrategy; import com.arangodb.ArangoDB; -import com.arangodb.ArangoDBException; -import com.arangodb.springframework.annotation.Document; -import com.arangodb.springframework.annotation.Edge; -import com.arangodb.springframework.annotation.From; -import com.arangodb.springframework.annotation.Ref; -import com.arangodb.springframework.annotation.Relations; -import com.arangodb.springframework.annotation.To; import com.arangodb.springframework.core.ArangoOperations; import com.arangodb.springframework.core.convert.ArangoConverter; import com.arangodb.springframework.core.convert.ArangoCustomConversions; import com.arangodb.springframework.core.convert.ArangoTypeMapper; import com.arangodb.springframework.core.convert.DefaultArangoConverter; import com.arangodb.springframework.core.convert.DefaultArangoTypeMapper; -import com.arangodb.springframework.core.convert.resolver.DocumentFromResolver; -import com.arangodb.springframework.core.convert.resolver.DocumentToResolver; -import com.arangodb.springframework.core.convert.resolver.EdgeFromResolver; -import com.arangodb.springframework.core.convert.resolver.EdgeToResolver; -import com.arangodb.springframework.core.convert.resolver.RefResolver; -import com.arangodb.springframework.core.convert.resolver.ReferenceResolver; -import com.arangodb.springframework.core.convert.resolver.RelationResolver; -import com.arangodb.springframework.core.convert.resolver.RelationsResolver; +import com.arangodb.springframework.core.convert.resolver.DefaultResolverFactory; import com.arangodb.springframework.core.convert.resolver.ResolverFactory; import com.arangodb.springframework.core.mapping.ArangoMappingContext; import com.arangodb.springframework.core.template.ArangoTemplate; @@ -156,49 +140,11 @@ default ArangoTypeMapper arangoTypeMapper() throws Exception { return new DefaultArangoTypeMapper(typeKey(), arangoMappingContext()); } + @Bean default ResolverFactory resolverFactory() { - return new ResolverFactory() { - @SuppressWarnings("unchecked") - @Override - public Optional> getReferenceResolver(final A annotation) { - ReferenceResolver resolver = null; - try { - if (annotation instanceof Ref) { - resolver = (ReferenceResolver) new RefResolver(arangoTemplate(), null); - } - } catch (final Exception e) { - throw new ArangoDBException(e); - } - return Optional.ofNullable(resolver); + return RESOLVER_FACTORY_INSTANCE; } - @SuppressWarnings("unchecked") - @Override - public Optional> getRelationResolver(final A annotation, - final Class collectionType) { - RelationResolver resolver = null; - try { - if (annotation instanceof From) { - if (collectionType == Edge.class) { - resolver = (RelationResolver) new EdgeFromResolver(arangoTemplate(), null); - } else if (collectionType == Document.class) { - resolver = (RelationResolver) new DocumentFromResolver(arangoTemplate(), null); - } - } else if (annotation instanceof To) { - if (collectionType == Edge.class) { - resolver = (RelationResolver) new EdgeToResolver(arangoTemplate(), null); - } else if (collectionType == Document.class) { - resolver = (RelationResolver) new DocumentToResolver(arangoTemplate(), null); - } - } else if (annotation instanceof Relations) { - resolver = (RelationResolver) new RelationsResolver(arangoTemplate(), null); - } - } catch (final Exception e) { - throw new ArangoDBException(e); - } - return Optional.ofNullable(resolver); - } - }; - } + ResolverFactory RESOLVER_FACTORY_INSTANCE = new DefaultResolverFactory(); } \ No newline at end of file diff --git a/src/main/java/com/arangodb/springframework/core/convert/resolver/DefaultResolverFactory.java b/src/main/java/com/arangodb/springframework/core/convert/resolver/DefaultResolverFactory.java new file mode 100644 index 000000000..77df50ed5 --- /dev/null +++ b/src/main/java/com/arangodb/springframework/core/convert/resolver/DefaultResolverFactory.java @@ -0,0 +1,63 @@ +package com.arangodb.springframework.core.convert.resolver; + +import java.lang.annotation.Annotation; +import java.util.Optional; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; + +import com.arangodb.springframework.annotation.Document; +import com.arangodb.springframework.annotation.Edge; +import com.arangodb.springframework.annotation.From; +import com.arangodb.springframework.annotation.Ref; +import com.arangodb.springframework.annotation.Relations; +import com.arangodb.springframework.annotation.To; +import com.arangodb.springframework.core.ArangoOperations; +import com.arangodb.springframework.repository.query.QueryTransactionBridge; + +public class DefaultResolverFactory implements ResolverFactory, ApplicationContextAware { + + private ObjectProvider template; + private ObjectProvider transactionBridge; + + @SuppressWarnings("unchecked") + @Override + public Optional> getReferenceResolver(final A annotation) { + ReferenceResolver resolver = null; + if (annotation instanceof Ref) { + resolver = (ReferenceResolver) new RefResolver(template.getObject(), transactionBridge.getIfUnique()); + } + return Optional.ofNullable(resolver); + } + + @SuppressWarnings("unchecked") + @Override + public Optional> getRelationResolver(final A annotation, + final Class collectionType) { + RelationResolver resolver = null; + if (annotation instanceof From) { + if (collectionType == Edge.class) { + resolver = (RelationResolver) new EdgeFromResolver(template.getObject(), transactionBridge.getIfUnique()); + } else if (collectionType == Document.class) { + resolver = (RelationResolver) new DocumentFromResolver(template.getObject(), transactionBridge.getIfUnique()); + } + } else if (annotation instanceof To) { + if (collectionType == Edge.class) { + resolver = (RelationResolver) new EdgeToResolver(template.getObject(), transactionBridge.getIfUnique()); + } else if (collectionType == Document.class) { + resolver = (RelationResolver) new DocumentToResolver(template.getObject(), transactionBridge.getIfUnique()); + } + } else if (annotation instanceof Relations) { + resolver = (RelationResolver) new RelationsResolver(template.getObject(), transactionBridge.getIfUnique()); + } + return Optional.ofNullable(resolver); + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + template = applicationContext.getBeanProvider(ArangoOperations.class); + transactionBridge = applicationContext.getBeanProvider(QueryTransactionBridge.class); + } +} From 7ced704fd3adffd614931adcaa61161c88236fd8 Mon Sep 17 00:00:00 2001 From: Arne Burmeister Date: Mon, 29 Aug 2022 12:59:26 +0200 Subject: [PATCH 20/55] #80 simplify configurer usage --- ...ArangoTransactionManagementConfigurer.java | 28 ++++++++++++------- .../ArangoTransactionalTestConfiguration.java | 7 ++--- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManagementConfigurer.java b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManagementConfigurer.java index b0dad3073..7b10774ff 100644 --- a/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManagementConfigurer.java +++ b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManagementConfigurer.java @@ -1,24 +1,32 @@ package com.arangodb.springframework.transaction; -import com.arangodb.springframework.core.ArangoOperations; -import com.arangodb.springframework.repository.query.QueryTransactionBridge; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.TransactionManagementConfigurer; +import com.arangodb.springframework.core.ArangoOperations; +import com.arangodb.springframework.repository.query.QueryTransactionBridge; + +/** + * To enable stream transactions for Arango Spring Data, create a + * {@link org.springframework.context.annotation.Configuration} class annotated with + * {@link org.springframework.transaction.annotation.EnableTransactionManagement} and + * {@link org.springframework.context.annotation.Import} this one. + */ public class ArangoTransactionManagementConfigurer implements TransactionManagementConfigurer { - private final ArangoOperations operations; - private final QueryTransactionBridge bridge; - - public ArangoTransactionManagementConfigurer(ArangoOperations operations, QueryTransactionBridge bridge) { - this.operations = operations; - this.bridge = bridge; - } + @Autowired + private ArangoOperations operations; + private final QueryTransactionBridge bridge = new QueryTransactionBridge(); @Override - @Bean public PlatformTransactionManager annotationDrivenTransactionManager() { return new ArangoTransactionManager(operations, bridge); } + + @Bean + QueryTransactionBridge arangoQueryTransactionBridge() { + return bridge; + } } diff --git a/src/test/java/com/arangodb/springframework/ArangoTransactionalTestConfiguration.java b/src/test/java/com/arangodb/springframework/ArangoTransactionalTestConfiguration.java index acdaadc00..5b14b8610 100644 --- a/src/test/java/com/arangodb/springframework/ArangoTransactionalTestConfiguration.java +++ b/src/test/java/com/arangodb/springframework/ArangoTransactionalTestConfiguration.java @@ -1,6 +1,7 @@ package com.arangodb.springframework; import com.arangodb.springframework.repository.query.QueryTransactionBridge; +import com.arangodb.springframework.transaction.ArangoTransactionManagementConfigurer; import com.arangodb.springframework.transaction.ArangoTransactionManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; @@ -10,11 +11,7 @@ @EnableTransactionManagement @TestExecutionListeners(TransactionalTestExecutionListener.class) -@Import(ArangoTransactionManager.class) +@Import(ArangoTransactionManagementConfigurer.class) public class ArangoTransactionalTestConfiguration extends ArangoTestConfiguration { - @Bean - public QueryTransactionBridge queryTransactionBridge() { - return new QueryTransactionBridge(); - } } From ee69109858aa81e512d9aa28918ad6af58ee856e Mon Sep 17 00:00:00 2001 From: Arne Burmeister Date: Mon, 29 Aug 2022 16:45:05 +0200 Subject: [PATCH 21/55] #80 refactor to own type instead of pair --- .../repository/query/AbstractArangoQuery.java | 13 +++++------ .../repository/query/DerivedArangoQuery.java | 2 +- .../query/QueryWithCollections.java | 22 +++++++++++++++++++ .../query/StringBasedArangoQuery.java | 5 ++--- .../query/derived/DerivedQueryCreator.java | 8 +++---- 5 files changed, 35 insertions(+), 15 deletions(-) create mode 100644 src/main/java/com/arangodb/springframework/repository/query/QueryWithCollections.java diff --git a/src/main/java/com/arangodb/springframework/repository/query/AbstractArangoQuery.java b/src/main/java/com/arangodb/springframework/repository/query/AbstractArangoQuery.java index 7fd558474..3f3def6bc 100644 --- a/src/main/java/com/arangodb/springframework/repository/query/AbstractArangoQuery.java +++ b/src/main/java/com/arangodb/springframework/repository/query/AbstractArangoQuery.java @@ -30,7 +30,6 @@ import org.slf4j.LoggerFactory; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.ResultProcessor; -import org.springframework.data.util.Pair; import org.springframework.util.Assert; import com.arangodb.ArangoCursor; @@ -79,16 +78,16 @@ public Object execute(final Object[] parameters) { options.fullCount(true); } - final Pair> queryAndCollection = createQuery(accessor, bindVars, options); + final QueryWithCollections queryAndCollection = createQuery(accessor, bindVars, options); if (options.getStreamTransactionId() == null && transactionBridge != null) { - options.streamTransactionId(transactionBridge.getCurrentTransaction(queryAndCollection.getSecond())); + options.streamTransactionId(transactionBridge.getCurrentTransaction(queryAndCollection.getCollections())); } final ResultProcessor processor = method.getResultProcessor().withDynamicProjection(accessor); final Class typeToRead = getTypeToRead(processor); - final ArangoCursor result = operations.query(queryAndCollection.getFirst(), bindVars, options, typeToRead); + final ArangoCursor result = operations.query(queryAndCollection.getQuery(), bindVars, options, typeToRead); logWarningsIfNecessary(result); return processor.processResult(convertResult(result, accessor)); } @@ -108,7 +107,7 @@ public ArangoQueryMethod getQueryMethod() { * Implementations should create an AQL query with the given * {@link com.arangodb.springframework.repository.query.ArangoParameterAccessor} and set necessary binding * parameters and query options. - * + * * @param accessor * provides access to the actual arguments * @param bindVars @@ -117,7 +116,7 @@ public ArangoQueryMethod getQueryMethod() { * contains the merged {@link com.arangodb.model.AqlQueryOptions} * @return a pair of the created AQL query and all collection names */ - protected abstract Pair> createQuery( + protected abstract QueryWithCollections createQuery( ArangoParameterAccessor accessor, Map bindVars, AqlQueryOptions options); @@ -128,7 +127,7 @@ public ArangoQueryMethod getQueryMethod() { /** * Merges AqlQueryOptions derived from @QueryOptions with dynamically passed AqlQueryOptions which takes priority - * + * * @param oldStatic * @param newDynamic * @return diff --git a/src/main/java/com/arangodb/springframework/repository/query/DerivedArangoQuery.java b/src/main/java/com/arangodb/springframework/repository/query/DerivedArangoQuery.java index 7d86e4a10..7baf3c0e8 100644 --- a/src/main/java/com/arangodb/springframework/repository/query/DerivedArangoQuery.java +++ b/src/main/java/com/arangodb/springframework/repository/query/DerivedArangoQuery.java @@ -53,7 +53,7 @@ public DerivedArangoQuery(final ArangoQueryMethod method, final ArangoOperations } @Override - protected Pair> createQuery( + protected QueryWithCollections createQuery( final ArangoParameterAccessor accessor, final Map bindVars) { diff --git a/src/main/java/com/arangodb/springframework/repository/query/QueryWithCollections.java b/src/main/java/com/arangodb/springframework/repository/query/QueryWithCollections.java new file mode 100644 index 000000000..8300feab9 --- /dev/null +++ b/src/main/java/com/arangodb/springframework/repository/query/QueryWithCollections.java @@ -0,0 +1,22 @@ +package com.arangodb.springframework.repository.query; + +import java.util.Collection; + +public class QueryWithCollections { + + private final String query; + private final Collection collections; + + public QueryWithCollections(String query, Collection collections) { + this.query = query; + this.collections = collections; + } + + public String getQuery() { + return query; + } + + public Collection getCollections() { + return collections; + } +} diff --git a/src/main/java/com/arangodb/springframework/repository/query/StringBasedArangoQuery.java b/src/main/java/com/arangodb/springframework/repository/query/StringBasedArangoQuery.java index eaddd3314..3a8440e69 100644 --- a/src/main/java/com/arangodb/springframework/repository/query/StringBasedArangoQuery.java +++ b/src/main/java/com/arangodb/springframework/repository/query/StringBasedArangoQuery.java @@ -29,7 +29,6 @@ import org.springframework.context.expression.BeanFactoryAccessor; import org.springframework.context.expression.BeanFactoryResolver; import org.springframework.core.annotation.AnnotationUtils; -import org.springframework.data.util.Pair; import org.springframework.expression.Expression; import org.springframework.expression.ParserContext; import org.springframework.expression.spel.standard.SpelExpressionParser; @@ -93,13 +92,13 @@ public StringBasedArangoQuery(final String query, final ArangoQueryMethod method } @Override - protected Pair> createQuery( + protected QueryWithCollections createQuery( final ArangoParameterAccessor accessor, final Map bindVars) { extractBindVars(accessor, bindVars); - return Pair.of(prepareQuery(accessor), allCollectionNames(bindVars)); + return new QueryWithCollections(prepareQuery(accessor), allCollectionNames(bindVars)); } private Collection allCollectionNames(Map bindVars) { diff --git a/src/main/java/com/arangodb/springframework/repository/query/derived/DerivedQueryCreator.java b/src/main/java/com/arangodb/springframework/repository/query/derived/DerivedQueryCreator.java index 7db218616..1cc8cfa03 100644 --- a/src/main/java/com/arangodb/springframework/repository/query/derived/DerivedQueryCreator.java +++ b/src/main/java/com/arangodb/springframework/repository/query/derived/DerivedQueryCreator.java @@ -27,6 +27,7 @@ import com.arangodb.springframework.core.mapping.ArangoPersistentProperty; import com.arangodb.springframework.core.util.AqlUtils; import com.arangodb.springframework.repository.query.ArangoParameterAccessor; +import com.arangodb.springframework.repository.query.QueryWithCollections; import com.arangodb.springframework.repository.query.derived.geo.Ring; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -42,7 +43,6 @@ import org.springframework.data.repository.query.parser.AbstractQueryCreator; import org.springframework.data.repository.query.parser.Part; import org.springframework.data.repository.query.parser.PartTree; -import org.springframework.data.util.Pair; import org.springframework.util.Assert; import java.util.*; @@ -51,7 +51,7 @@ /** * Creates a full AQL query from a PartTree and ArangoParameterAccessor */ -public class DerivedQueryCreator extends AbstractQueryCreator>, Criteria> { +public class DerivedQueryCreator extends AbstractQueryCreator { private static final Logger LOGGER = LoggerFactory.getLogger(DerivedQueryCreator.class); private static final Set UNSUPPORTED_IGNORE_CASE = new HashSet<>(); @@ -121,7 +121,7 @@ protected Criteria or(final Criteria base, final Criteria criteria) { * @return */ @Override - protected Pair> complete(final Criteria criteria, final Sort sort) { + protected QueryWithCollections complete(final Criteria criteria, final Sort sort) { if (tree.isDistinct() && !tree.isCountProjection()) { LOGGER.debug("Use of 'Distinct' is meaningful only in count queries"); } @@ -192,7 +192,7 @@ protected Criteria or(final Criteria base, final Criteria criteria) { } } } - return Pair.of(query.toString(), withCollections); + return new QueryWithCollections(query.toString(), withCollections); } public double[] getUniquePoint() { From 4a06157d327650a7b24fa89a1083ae59d3ebbc78 Mon Sep 17 00:00:00 2001 From: Arne Burmeister Date: Mon, 29 Aug 2022 18:30:48 +0200 Subject: [PATCH 22/55] #80 expose ta as bean --- .../transaction/ArangoTransactionManagementConfigurer.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManagementConfigurer.java b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManagementConfigurer.java index 7b10774ff..1d6550b9a 100644 --- a/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManagementConfigurer.java +++ b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManagementConfigurer.java @@ -21,6 +21,7 @@ public class ArangoTransactionManagementConfigurer implements TransactionManagem private final QueryTransactionBridge bridge = new QueryTransactionBridge(); @Override + @Bean public PlatformTransactionManager annotationDrivenTransactionManager() { return new ArangoTransactionManager(operations, bridge); } From 0ad43eb131ac7d76c5580ae713a9f12534e7a303 Mon Sep 17 00:00:00 2001 From: Arne Burmeister Date: Tue, 30 Aug 2022 12:30:33 +0200 Subject: [PATCH 23/55] #80 refactor to sync resource, wrap db exceptions --- .../transaction/ArangoTransaction.java | 88 -------------- .../transaction/ArangoTransactionManager.java | 81 ++++++++++--- .../transaction/ArangoTransactionObject.java | 110 ++++++++++++++++++ .../ArangoTransactionResource.java | 40 +++++++ .../ArangoTransactionManagerTest.java | 80 ++++++++++++- 5 files changed, 286 insertions(+), 113 deletions(-) delete mode 100644 src/main/java/com/arangodb/springframework/transaction/ArangoTransaction.java create mode 100644 src/main/java/com/arangodb/springframework/transaction/ArangoTransactionObject.java create mode 100644 src/main/java/com/arangodb/springframework/transaction/ArangoTransactionResource.java diff --git a/src/main/java/com/arangodb/springframework/transaction/ArangoTransaction.java b/src/main/java/com/arangodb/springframework/transaction/ArangoTransaction.java deleted file mode 100644 index e0b00baf6..000000000 --- a/src/main/java/com/arangodb/springframework/transaction/ArangoTransaction.java +++ /dev/null @@ -1,88 +0,0 @@ -package com.arangodb.springframework.transaction; - -import com.arangodb.ArangoDatabase; -import com.arangodb.entity.StreamTransactionEntity; -import com.arangodb.entity.StreamTransactionStatus; -import com.arangodb.model.StreamTransactionOptions; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.springframework.transaction.IllegalTransactionStateException; -import org.springframework.transaction.TransactionDefinition; -import org.springframework.transaction.interceptor.TransactionAttribute; -import org.springframework.transaction.support.SmartTransactionObject; - -import java.util.Collection; -import java.util.HashSet; -import java.util.Set; - -class ArangoTransaction implements SmartTransactionObject { - - private final Log logger = LogFactory.getLog(getClass()); - private final ArangoDatabase database; - private final Set writeCollections = new HashSet<>(); - private TransactionDefinition definition; - private StreamTransactionEntity transaction; - - ArangoTransaction(ArangoDatabase database) { - this.database = database; - } - - boolean exists() { - return transaction != null; - } - - void configure(TransactionDefinition definition) { - this.definition = definition; - if (definition instanceof TransactionAttribute) { - writeCollections.addAll(((TransactionAttribute) definition).getLabels()); - } - } - - String getOrBegin(Collection collections) { - if (transaction != null) { - if (!writeCollections.containsAll(collections)) { - Set additional = new HashSet<>(collections); - additional.removeAll(writeCollections); - throw new IllegalTransactionStateException("Stream transaction already started on collections " + writeCollections + ", no additional collections allowed: " + additional); - } - return transaction.getId(); - } - writeCollections.addAll(collections); - StreamTransactionOptions options = new StreamTransactionOptions() - .allowImplicit(true) - .writeCollections(writeCollections.toArray(new String[0])) - .lockTimeout(definition.getTimeout() == -1 ? 0 : definition.getTimeout()); - transaction = database.beginStreamTransaction(options); - if (logger.isDebugEnabled()) { - logger.debug("Began stream transaction " + transaction.getId() + " writing collections " + writeCollections); - } - return transaction.getId(); - } - - void commit() { - if (transaction != null && transaction.getStatus() == StreamTransactionStatus.running) { - database.commitStreamTransaction(transaction.getId()); - } - } - - void rollback() { - if (transaction != null && transaction.getStatus() == StreamTransactionStatus.running) { - database.abortStreamTransaction(transaction.getId()); - } - } - - @Override - public boolean isRollbackOnly() { - return transaction != null && transaction.getStatus() == StreamTransactionStatus.aborted; - } - - @Override - public void flush() { - // nothing to do - } - - @Override - public String toString() { - return transaction == null ? "(not begun)" : transaction.getId(); - } -} diff --git a/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManager.java b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManager.java index 97d944a8b..700ae30f7 100644 --- a/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManager.java +++ b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManager.java @@ -1,18 +1,15 @@ package com.arangodb.springframework.transaction; +import com.arangodb.ArangoDBException; import com.arangodb.ArangoDatabase; import com.arangodb.DbName; import com.arangodb.model.StreamTransactionOptions; import com.arangodb.springframework.core.ArangoOperations; import com.arangodb.springframework.repository.query.QueryTransactionBridge; -import org.springframework.transaction.InvalidIsolationLevelException; -import org.springframework.transaction.TransactionDefinition; -import org.springframework.transaction.TransactionException; +import org.springframework.transaction.*; import org.springframework.transaction.support.AbstractPlatformTransactionManager; import org.springframework.transaction.support.DefaultTransactionStatus; - -import java.util.Collection; -import java.util.function.Function; +import org.springframework.transaction.support.TransactionSynchronizationManager; /** * Transaction manager using ArangoDB stream transactions on the @@ -29,51 +26,97 @@ public class ArangoTransactionManager extends AbstractPlatformTransactionManager public ArangoTransactionManager(ArangoOperations operations, QueryTransactionBridge bridge) { this.operations = operations; this.bridge = bridge; + setNestedTransactionAllowed(false); + setTransactionSynchronization(SYNCHRONIZATION_ALWAYS); + setValidateExistingTransaction(true); + setRollbackOnCommitFailure(true); } + /** + * Creates a new transaction object. Any synchronized resource will be reused. + */ @Override - protected Object doGetTransaction() throws TransactionException { + protected Object doGetTransaction() { DbName database = operations.getDatabaseName(); if (logger.isDebugEnabled()) { logger.debug("Create new transaction for database " + database); } - return new ArangoTransaction(operations.driver().db(database)); + try { + ArangoTransactionResource resource = (ArangoTransactionResource) TransactionSynchronizationManager.getResource(database); + return new ArangoTransactionObject(operations.driver().db(database), getDefaultTimeout(), resource); + } catch (ArangoDBException error) { + throw new TransactionSystemException("Cannot create transaction object", error); + } } + /** + * Configures the new transaction object. The resulting resource will be synchronized and the bridge will be initialized. + * + * @see QueryTransactionBridge + */ @Override - protected void doBegin(Object transaction, TransactionDefinition definition) throws InvalidIsolationLevelException { + protected void doBegin(Object transaction, TransactionDefinition definition) throws TransactionUsageException { int isolationLevel = definition.getIsolationLevel(); if (isolationLevel != -1 && (isolationLevel & TransactionDefinition.ISOLATION_SERIALIZABLE) != 0) { throw new InvalidIsolationLevelException("ArangoDB does not support isolation level serializable"); } - ArangoTransaction tx = (ArangoTransaction) transaction; + ArangoTransactionObject tx = (ArangoTransactionObject) transaction; tx.configure(definition); - bridge.setCurrentTransaction(tx::getOrBegin); + DbName key = operations.getDatabaseName(); + rebind(key, tx.createResource()); + bridge.setCurrentTransaction(collections -> { + ArangoTransactionResource resource = tx.getOrBegin(collections); + rebind(key, resource); + return resource.getStreamTransactionId(); + }); } + /** + * Commit the current stream transaction iff any. The bridge is cleared afterwards. + */ @Override protected void doCommit(DefaultTransactionStatus status) throws TransactionException { - ArangoTransaction tx = (ArangoTransaction) status.getTransaction(); + ArangoTransactionObject tx = (ArangoTransactionObject) status.getTransaction(); if (logger.isDebugEnabled()) { logger.debug("Commit stream transaction " + tx); } - tx.commit(); - bridge.clearCurrentTransaction(); + try { + tx.commit(); + bridge.clearCurrentTransaction(); + } catch (ArangoDBException error) { + throw new TransactionSystemException("Cannot commit transaction " + tx, error); + } } + /** + * Roll back the current stream transaction iff any. The bridge is cleared afterwards. + */ @Override protected void doRollback(DefaultTransactionStatus status) throws TransactionException { - ArangoTransaction tx = (ArangoTransaction) status.getTransaction(); + ArangoTransactionObject tx = (ArangoTransactionObject) status.getTransaction(); if (logger.isDebugEnabled()) { logger.debug("Rollback stream transaction " + tx); } - tx.rollback(); - bridge.clearCurrentTransaction(); + try { + tx.rollback(); + bridge.clearCurrentTransaction(); + } catch (ArangoDBException error) { + throw new TransactionSystemException("Cannot roll back transaction " + tx, error); + } } @Override protected boolean isExistingTransaction(Object transaction) throws TransactionException { - return transaction instanceof ArangoTransaction - && ((ArangoTransaction) transaction).exists(); + return transaction instanceof ArangoTransactionObject && ((ArangoTransactionObject) transaction).exists(); + } + + @Override + protected void doCleanupAfterCompletion(Object transaction) { + TransactionSynchronizationManager.unbindResource(operations.getDatabaseName()); + } + + private static void rebind(DbName key, ArangoTransactionResource resource) { + TransactionSynchronizationManager.unbindResourceIfPossible(key); + TransactionSynchronizationManager.bindResource(key, resource); } } diff --git a/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionObject.java b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionObject.java new file mode 100644 index 000000000..3a9bd82c5 --- /dev/null +++ b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionObject.java @@ -0,0 +1,110 @@ +package com.arangodb.springframework.transaction; + +import com.arangodb.ArangoDatabase; +import com.arangodb.DbName; +import com.arangodb.entity.StreamTransactionEntity; +import com.arangodb.entity.StreamTransactionStatus; +import com.arangodb.model.StreamTransactionOptions; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.lang.Nullable; +import org.springframework.transaction.IllegalTransactionStateException; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.interceptor.TransactionAttribute; +import org.springframework.transaction.support.SmartTransactionObject; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +class ArangoTransactionObject implements SmartTransactionObject { + + private static final Log logger = LogFactory.getLog(ArangoTransactionObject.class); + + private final ArangoDatabase database; + private final Set writeCollections = new HashSet<>(); + private int timeout; + private StreamTransactionEntity streamTransaction; + + ArangoTransactionObject(ArangoDatabase database, int defaultTimeout, @Nullable ArangoTransactionResource resource) { + this.database = database; + this.timeout = defaultTimeout; + if (resource != null) { + writeCollections.addAll(resource.getCollectionNames()); + if (resource.getStreamTransactionId() != null) { + streamTransaction = database.getStreamTransaction(resource.getStreamTransactionId()); + } + } + } + + ArangoTransactionResource createResource() { + return new ArangoTransactionResource(streamTransaction == null ? null : streamTransaction.getId(), writeCollections); + } + + boolean exists() { + return streamTransaction != null; + } + + void configure(TransactionDefinition definition) { + if (definition.getTimeout() != -1) { + this.timeout = definition.getTimeout(); + } + if (definition instanceof TransactionAttribute) { + addCollections(((TransactionAttribute) definition).getLabels()); + } + } + + ArangoTransactionResource getOrBegin(Collection collections) { + addCollections(collections); + if (streamTransaction != null) { + return createResource(); + } + StreamTransactionOptions options = new StreamTransactionOptions() + .allowImplicit(true) + .writeCollections(writeCollections.toArray(new String[0])) + .lockTimeout(Math.max(timeout, 0)); + streamTransaction = database.beginStreamTransaction(options); + if (logger.isDebugEnabled()) { + logger.debug("Began stream transaction " + streamTransaction.getId() + " writing collections " + writeCollections); + } + return createResource(); + } + + void commit() { + if (streamTransaction != null && streamTransaction.getStatus() == StreamTransactionStatus.running) { + database.commitStreamTransaction(streamTransaction.getId()); + } + } + + void rollback() { + if (streamTransaction != null && streamTransaction.getStatus() == StreamTransactionStatus.running) { + database.abortStreamTransaction(streamTransaction.getId()); + } + } + + @Override + public boolean isRollbackOnly() { + return streamTransaction != null && streamTransaction.getStatus() == StreamTransactionStatus.aborted; + } + + @Override + public void flush() { + // nothing to do + } + + @Override + public String toString() { + return streamTransaction == null ? "(not begun)" : streamTransaction.getId(); + } + + private void addCollections(Collection collections) { + if (streamTransaction != null) { + if (!writeCollections.containsAll(collections)) { + Set additional = new HashSet<>(collections); + additional.removeAll(writeCollections); + throw new IllegalTransactionStateException("Stream transaction already started on collections " + writeCollections + ", no additional collections allowed: " + additional); + } + } + writeCollections.addAll(collections); + } +} diff --git a/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionResource.java b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionResource.java new file mode 100644 index 000000000..b96adc57e --- /dev/null +++ b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionResource.java @@ -0,0 +1,40 @@ +package com.arangodb.springframework.transaction; + +import org.springframework.lang.Nullable; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +class ArangoTransactionResource { + + private final String streamTransactionId; + private final Set collectionNames; + + ArangoTransactionResource(@Nullable String streamTransactionId, Collection collectionNames) { + this.streamTransactionId = streamTransactionId; + this.collectionNames = new HashSet<>(collectionNames); + } + + String getStreamTransactionId() { + return streamTransactionId; + } + + Set getCollectionNames() { + return collectionNames; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ArangoTransactionResource that = (ArangoTransactionResource) o; + return Objects.equals(streamTransactionId, that.streamTransactionId) && collectionNames.equals(that.collectionNames); + } + + @Override + public int hashCode() { + return Objects.hash(streamTransactionId); + } +} diff --git a/src/test/java/com/arangodb/springframework/transaction/ArangoTransactionManagerTest.java b/src/test/java/com/arangodb/springframework/transaction/ArangoTransactionManagerTest.java index 6dd158d12..b163e4779 100644 --- a/src/test/java/com/arangodb/springframework/transaction/ArangoTransactionManagerTest.java +++ b/src/test/java/com/arangodb/springframework/transaction/ArangoTransactionManagerTest.java @@ -4,10 +4,12 @@ import com.arangodb.ArangoDatabase; import com.arangodb.DbName; import com.arangodb.entity.StreamTransactionEntity; +import com.arangodb.entity.StreamTransactionStatus; import com.arangodb.model.StreamTransactionOptions; import com.arangodb.model.TransactionCollectionOptions; import com.arangodb.springframework.core.ArangoOperations; import com.arangodb.springframework.repository.query.QueryTransactionBridge; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -16,11 +18,14 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.lang.Nullable; import org.springframework.transaction.IllegalTransactionStateException; import org.springframework.transaction.InvalidIsolationLevelException; import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.interceptor.DefaultTransactionAttribute; +import org.springframework.transaction.support.DefaultTransactionStatus; +import org.springframework.transaction.support.TransactionSynchronizationManager; import java.util.Arrays; import java.util.Collection; @@ -28,9 +33,13 @@ import java.util.function.Function; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.*; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.hasItems; +import static org.hamcrest.Matchers.nullValue; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import static org.springframework.beans.PropertyAccessorFactory.forDirectFieldAccess; @RunWith(MockitoJUnitRunner.class) @@ -65,12 +74,34 @@ public void setupMocks() { .thenReturn(database); } + @After + public void cleanupSync() { + TransactionSynchronizationManager.unbindResourceIfPossible(DATABASE_NAME); + TransactionSynchronizationManager.clear(); + } + @Test public void getTransactionReturnsNewTransactionWithoutStreamTransaction() { - TransactionStatus transaction = underTest.getTransaction(new DefaultTransactionAttribute()); - assertThat(transaction.isNewTransaction(), is(true)); + TransactionStatus status = underTest.getTransaction(new DefaultTransactionAttribute()); + assertThat(status.isNewTransaction(), is(true)); verify(driver).db(DATABASE_NAME); verify(bridge).setCurrentTransaction(any()); + assertThat(TransactionSynchronizationManager.getResource(DATABASE_NAME), + is(new ArangoTransactionResource(null, Collections.emptyList()))); + verifyNoInteractions(database); + } + + @Test + public void nestedGetTransactionReturnsNewTransactionWithFormerCollections() { + DefaultTransactionAttribute first = new DefaultTransactionAttribute(); + first.setLabels(Collections.singleton("foo")); + underTest.getTransaction(first); + DefaultTransactionAttribute second = new DefaultTransactionAttribute(); + second.setLabels(Collections.singleton("bar")); + TransactionStatus status = underTest.getTransaction(second); + assertThat(status.isNewTransaction(), is(true)); + ArangoTransactionObject transactionObject = getTransactionObject(status); + assertThat(transactionObject.createResource().getCollectionNames(), hasItems("foo", "bar")); verifyNoInteractions(database); } @@ -79,14 +110,14 @@ public void getTransactionReturnsTransactionCreatesStreamTransactionWithAllColle DefaultTransactionAttribute definition = new DefaultTransactionAttribute(); definition.setLabels(Collections.singleton("baz")); definition.setTimeout(20); - TransactionStatus transaction = underTest.getTransaction(definition); + TransactionStatus status = underTest.getTransaction(definition); when(streamTransaction.getId()) .thenReturn("123"); when(database.beginStreamTransaction(any())) .thenReturn(streamTransaction); verify(bridge).setCurrentTransaction(beginPassed.capture()); beginPassed.getValue().apply(Arrays.asList("foo", "bar")); - assertThat(transaction.isCompleted(), is(false)); + assertThat(status.isCompleted(), is(false)); verify(database).beginStreamTransaction(optionsPassed.capture()); assertThat(optionsPassed.getValue().getAllowImplicit(), is(true)); assertThat(optionsPassed.getValue().getLockTimeout(), is(20)); @@ -96,6 +127,35 @@ public void getTransactionReturnsTransactionCreatesStreamTransactionWithAllColle assertThat(collections.getWrite(), hasItems("baz", "foo", "bar")); } + @Test + public void nestedGetTransactionReturnsExistingTransactionWithFormerCollections() { + DefaultTransactionAttribute first = new DefaultTransactionAttribute(); + first.setLabels(Collections.singleton("foo")); + TransactionStatus outer = underTest.getTransaction(first); + assertThat(outer.isNewTransaction(), is(true)); + + when(streamTransaction.getId()) + .thenReturn("123"); + when(database.beginStreamTransaction(any())) + .thenReturn(streamTransaction); + when(database.getStreamTransaction(any())) + .thenReturn(streamTransaction); + when(streamTransaction.getStatus()) + .thenReturn(StreamTransactionStatus.running); + verify(bridge).setCurrentTransaction(beginPassed.capture()); + beginPassed.getValue().apply(Arrays.asList("foo", "bar")); + + DefaultTransactionAttribute second = new DefaultTransactionAttribute(); + second.setLabels(Collections.singleton("bar")); + TransactionStatus inner = underTest.getTransaction(second); + assertThat(inner.isNewTransaction(), is(false)); + ArangoTransactionObject transactionObject = getTransactionObject(inner); + assertThat(transactionObject.createResource().getStreamTransactionId(), is("123")); + underTest.commit(inner); + underTest.commit(outer); + verify(database).commitStreamTransaction("123"); + } + @Test public void getTransactionWithMultipleBridgeCallsWorksForKnownCollections() { DefaultTransactionAttribute definition = new DefaultTransactionAttribute(); @@ -136,6 +196,14 @@ public void getTransactionThrowsInvalidIsolationLevelExceptionForIsolationSerial underTest.getTransaction(definition); } + @Nullable + private ArangoTransactionObject getTransactionObject(TransactionStatus status) { + if (status instanceof DefaultTransactionStatus) { + return (ArangoTransactionObject) ((DefaultTransactionStatus) status).getTransaction(); + } + return null; + } + private TransactionCollectionOptions getCollections(StreamTransactionOptions options) { return (TransactionCollectionOptions) forDirectFieldAccess(options).getPropertyValue("collections"); } From 39972cafc143937c34ab6ddb3b28079fdabbe2c9 Mon Sep 17 00:00:00 2001 From: Arne Burmeister Date: Tue, 30 Aug 2022 15:56:50 +0200 Subject: [PATCH 24/55] #80 refactor to mutable sync resource, handle rollback only --- .../transaction/ArangoTransactionManager.java | 21 +++--- .../transaction/ArangoTransactionObject.java | 74 ++++++++++-------- .../ArangoTransactionResource.java | 41 ++++++---- .../ArangoTransactionManagerTest.java | 75 +++++++++---------- 4 files changed, 116 insertions(+), 95 deletions(-) diff --git a/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManager.java b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManager.java index 700ae30f7..cf974f72d 100644 --- a/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManager.java +++ b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManager.java @@ -29,6 +29,7 @@ public ArangoTransactionManager(ArangoOperations operations, QueryTransactionBri setNestedTransactionAllowed(false); setTransactionSynchronization(SYNCHRONIZATION_ALWAYS); setValidateExistingTransaction(true); + setGlobalRollbackOnParticipationFailure(true); setRollbackOnCommitFailure(true); } @@ -63,12 +64,9 @@ protected void doBegin(Object transaction, TransactionDefinition definition) thr ArangoTransactionObject tx = (ArangoTransactionObject) transaction; tx.configure(definition); DbName key = operations.getDatabaseName(); - rebind(key, tx.createResource()); - bridge.setCurrentTransaction(collections -> { - ArangoTransactionResource resource = tx.getOrBegin(collections); - rebind(key, resource); - return resource.getStreamTransactionId(); - }); + TransactionSynchronizationManager.unbindResourceIfPossible(key); + TransactionSynchronizationManager.bindResource(key, tx.getResource()); + bridge.setCurrentTransaction(collections -> tx.getOrBegin(collections).getStreamTransactionId()); } /** @@ -111,12 +109,13 @@ protected boolean isExistingTransaction(Object transaction) throws TransactionEx } @Override - protected void doCleanupAfterCompletion(Object transaction) { - TransactionSynchronizationManager.unbindResource(operations.getDatabaseName()); + protected void doSetRollbackOnly(DefaultTransactionStatus status) throws TransactionException { + ArangoTransactionObject tx = (ArangoTransactionObject) status.getTransaction(); + tx.setRollbackOnly(); } - private static void rebind(DbName key, ArangoTransactionResource resource) { - TransactionSynchronizationManager.unbindResourceIfPossible(key); - TransactionSynchronizationManager.bindResource(key, resource); + @Override + protected void doCleanupAfterCompletion(Object transaction) { + TransactionSynchronizationManager.unbindResourceIfPossible(operations.getDatabaseName()); } } diff --git a/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionObject.java b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionObject.java index 3a9bd82c5..865742ef1 100644 --- a/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionObject.java +++ b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionObject.java @@ -1,7 +1,6 @@ package com.arangodb.springframework.transaction; import com.arangodb.ArangoDatabase; -import com.arangodb.DbName; import com.arangodb.entity.StreamTransactionEntity; import com.arangodb.entity.StreamTransactionStatus; import com.arangodb.model.StreamTransactionOptions; @@ -12,37 +11,37 @@ import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.interceptor.TransactionAttribute; import org.springframework.transaction.support.SmartTransactionObject; +import org.springframework.transaction.support.TransactionSynchronizationUtils; import java.util.Collection; +import java.util.Collections; import java.util.HashSet; import java.util.Set; +/** + * Transaction object created by {@link ArangoTransactionManager}. + */ class ArangoTransactionObject implements SmartTransactionObject { private static final Log logger = LogFactory.getLog(ArangoTransactionObject.class); private final ArangoDatabase database; - private final Set writeCollections = new HashSet<>(); + private final ArangoTransactionResource resource; private int timeout; - private StreamTransactionEntity streamTransaction; + private StreamTransactionEntity transaction; ArangoTransactionObject(ArangoDatabase database, int defaultTimeout, @Nullable ArangoTransactionResource resource) { this.database = database; + this.resource = resource == null ? new ArangoTransactionResource(null, Collections.emptySet(), false) : resource; this.timeout = defaultTimeout; - if (resource != null) { - writeCollections.addAll(resource.getCollectionNames()); - if (resource.getStreamTransactionId() != null) { - streamTransaction = database.getStreamTransaction(resource.getStreamTransactionId()); - } - } } - ArangoTransactionResource createResource() { - return new ArangoTransactionResource(streamTransaction == null ? null : streamTransaction.getId(), writeCollections); + ArangoTransactionResource getResource() { + return resource; } boolean exists() { - return streamTransaction != null; + return resource.getStreamTransactionId() != null; } void configure(TransactionDefinition definition) { @@ -56,55 +55,70 @@ void configure(TransactionDefinition definition) { ArangoTransactionResource getOrBegin(Collection collections) { addCollections(collections); - if (streamTransaction != null) { - return createResource(); + if (resource.getStreamTransactionId() != null) { + return getResource(); } StreamTransactionOptions options = new StreamTransactionOptions() .allowImplicit(true) - .writeCollections(writeCollections.toArray(new String[0])) + .writeCollections(resource.getCollectionNames().toArray(new String[0])) .lockTimeout(Math.max(timeout, 0)); - streamTransaction = database.beginStreamTransaction(options); + transaction = database.beginStreamTransaction(options); + resource.setStreamTransactionId(transaction.getId()); if (logger.isDebugEnabled()) { - logger.debug("Began stream transaction " + streamTransaction.getId() + " writing collections " + writeCollections); + logger.debug("Began stream transaction " + resource.getStreamTransactionId() + " writing collections " + resource.getCollectionNames()); } - return createResource(); + return getResource(); } void commit() { - if (streamTransaction != null && streamTransaction.getStatus() == StreamTransactionStatus.running) { - database.commitStreamTransaction(streamTransaction.getId()); + if (isStatus(StreamTransactionStatus.running)) { + database.commitStreamTransaction(resource.getStreamTransactionId()); } } void rollback() { - if (streamTransaction != null && streamTransaction.getStatus() == StreamTransactionStatus.running) { - database.abortStreamTransaction(streamTransaction.getId()); + if (isStatus(StreamTransactionStatus.running)) { + database.abortStreamTransaction(resource.getStreamTransactionId()); } + setRollbackOnly(); } @Override public boolean isRollbackOnly() { - return streamTransaction != null && streamTransaction.getStatus() == StreamTransactionStatus.aborted; + return resource.isRollbackOnly() || isStatus(StreamTransactionStatus.aborted); + } + + public void setRollbackOnly() { + resource.setRollbackOnly(true); } @Override public void flush() { - // nothing to do + TransactionSynchronizationUtils.triggerFlush(); } @Override public String toString() { - return streamTransaction == null ? "(not begun)" : streamTransaction.getId(); + return resource.getStreamTransactionId() == null ? "(not begun)" : resource.getStreamTransactionId(); } private void addCollections(Collection collections) { - if (streamTransaction != null) { - if (!writeCollections.containsAll(collections)) { + if (resource.getStreamTransactionId() != null) { + if (!resource.getCollectionNames().containsAll(collections)) { Set additional = new HashSet<>(collections); - additional.removeAll(writeCollections); - throw new IllegalTransactionStateException("Stream transaction already started on collections " + writeCollections + ", no additional collections allowed: " + additional); + additional.removeAll(resource.getCollectionNames()); + throw new IllegalTransactionStateException("Stream transaction already started on collections " + resource.getCollectionNames() + ", no additional collections allowed: " + additional); } } - writeCollections.addAll(collections); + HashSet all = new HashSet<>(resource.getCollectionNames()); + all.addAll(collections); + resource.setCollectionNames(all); + } + + private boolean isStatus(StreamTransactionStatus status) { + if (transaction == null && resource.getStreamTransactionId() != null) { + transaction = database.getStreamTransaction(resource.getStreamTransactionId()); + } + return transaction != null && transaction.getStatus() == status; } } diff --git a/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionResource.java b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionResource.java index b96adc57e..fcacfa074 100644 --- a/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionResource.java +++ b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionResource.java @@ -1,40 +1,51 @@ package com.arangodb.springframework.transaction; import org.springframework.lang.Nullable; +import org.springframework.transaction.support.TransactionSynchronizationManager; -import java.util.Collection; import java.util.HashSet; -import java.util.Objects; import java.util.Set; +/** + * Synchronisation resource (has to be mutable). + * + * @see TransactionSynchronizationManager#bindResource(Object, Object) + * @see ArangoTransactionObject + */ class ArangoTransactionResource { - private final String streamTransactionId; - private final Set collectionNames; + private String streamTransactionId; + private Set collectionNames; - ArangoTransactionResource(@Nullable String streamTransactionId, Collection collectionNames) { + private boolean rollbackOnly; + + ArangoTransactionResource(@Nullable String streamTransactionId, Set collectionNames, boolean rollbackOnly) { this.streamTransactionId = streamTransactionId; - this.collectionNames = new HashSet<>(collectionNames); + setCollectionNames(collectionNames); + this.rollbackOnly = rollbackOnly; } String getStreamTransactionId() { return streamTransactionId; } + void setStreamTransactionId(String streamTransactionId) { + this.streamTransactionId = streamTransactionId; + } + Set getCollectionNames() { return collectionNames; } - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - ArangoTransactionResource that = (ArangoTransactionResource) o; - return Objects.equals(streamTransactionId, that.streamTransactionId) && collectionNames.equals(that.collectionNames); + void setCollectionNames(Set collectionNames) { + this.collectionNames = new HashSet<>(collectionNames); + } + + boolean isRollbackOnly() { + return rollbackOnly; } - @Override - public int hashCode() { - return Objects.hash(streamTransactionId); + void setRollbackOnly(boolean rollbackOnly) { + this.rollbackOnly = rollbackOnly; } } diff --git a/src/test/java/com/arangodb/springframework/transaction/ArangoTransactionManagerTest.java b/src/test/java/com/arangodb/springframework/transaction/ArangoTransactionManagerTest.java index b163e4779..1c1431bf2 100644 --- a/src/test/java/com/arangodb/springframework/transaction/ArangoTransactionManagerTest.java +++ b/src/test/java/com/arangodb/springframework/transaction/ArangoTransactionManagerTest.java @@ -19,10 +19,7 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.lang.Nullable; -import org.springframework.transaction.IllegalTransactionStateException; -import org.springframework.transaction.InvalidIsolationLevelException; -import org.springframework.transaction.TransactionDefinition; -import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.*; import org.springframework.transaction.interceptor.DefaultTransactionAttribute; import org.springframework.transaction.support.DefaultTransactionStatus; import org.springframework.transaction.support.TransactionSynchronizationManager; @@ -33,6 +30,7 @@ import java.util.function.Function; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.hasItems; import static org.hamcrest.Matchers.nullValue; @@ -86,8 +84,10 @@ public void getTransactionReturnsNewTransactionWithoutStreamTransaction() { assertThat(status.isNewTransaction(), is(true)); verify(driver).db(DATABASE_NAME); verify(bridge).setCurrentTransaction(any()); - assertThat(TransactionSynchronizationManager.getResource(DATABASE_NAME), - is(new ArangoTransactionResource(null, Collections.emptyList()))); + ArangoTransactionResource resource = (ArangoTransactionResource) TransactionSynchronizationManager.getResource(DATABASE_NAME); + assertThat(resource.getStreamTransactionId(), nullValue()); + assertThat(resource.getCollectionNames(), empty()); + assertThat(resource.isRollbackOnly(), is(false)); verifyNoInteractions(database); } @@ -98,25 +98,28 @@ public void nestedGetTransactionReturnsNewTransactionWithFormerCollections() { underTest.getTransaction(first); DefaultTransactionAttribute second = new DefaultTransactionAttribute(); second.setLabels(Collections.singleton("bar")); - TransactionStatus status = underTest.getTransaction(second); - assertThat(status.isNewTransaction(), is(true)); - ArangoTransactionObject transactionObject = getTransactionObject(status); - assertThat(transactionObject.createResource().getCollectionNames(), hasItems("foo", "bar")); + TransactionStatus inner = underTest.getTransaction(second); + assertThat(inner.isNewTransaction(), is(true)); + ArangoTransactionObject transactionObject = getTransactionObject(inner); + assertThat(transactionObject.getResource().getCollectionNames(), hasItems("foo", "bar")); verifyNoInteractions(database); } + @Test(expected = UnexpectedRollbackException.class) + public void innerRollbackCausesUnexpectedRollbackOnOuterCommit() { + TransactionStatus outer = underTest.getTransaction(new DefaultTransactionAttribute()); + TransactionStatus inner = underTest.getTransaction(new DefaultTransactionAttribute()); + underTest.rollback(inner); + underTest.commit(outer); + } + @Test public void getTransactionReturnsTransactionCreatesStreamTransactionWithAllCollectionsOnBridgeBeginCall() { DefaultTransactionAttribute definition = new DefaultTransactionAttribute(); definition.setLabels(Collections.singleton("baz")); definition.setTimeout(20); TransactionStatus status = underTest.getTransaction(definition); - when(streamTransaction.getId()) - .thenReturn("123"); - when(database.beginStreamTransaction(any())) - .thenReturn(streamTransaction); - verify(bridge).setCurrentTransaction(beginPassed.capture()); - beginPassed.getValue().apply(Arrays.asList("foo", "bar")); + beginTransaction("123", "foo", "bar"); assertThat(status.isCompleted(), is(false)); verify(database).beginStreamTransaction(optionsPassed.capture()); assertThat(optionsPassed.getValue().getAllowImplicit(), is(true)); @@ -134,23 +137,14 @@ public void nestedGetTransactionReturnsExistingTransactionWithFormerCollections( TransactionStatus outer = underTest.getTransaction(first); assertThat(outer.isNewTransaction(), is(true)); - when(streamTransaction.getId()) - .thenReturn("123"); - when(database.beginStreamTransaction(any())) - .thenReturn(streamTransaction); - when(database.getStreamTransaction(any())) - .thenReturn(streamTransaction); - when(streamTransaction.getStatus()) - .thenReturn(StreamTransactionStatus.running); - verify(bridge).setCurrentTransaction(beginPassed.capture()); - beginPassed.getValue().apply(Arrays.asList("foo", "bar")); + beginTransaction("123", "foo", "bar"); DefaultTransactionAttribute second = new DefaultTransactionAttribute(); second.setLabels(Collections.singleton("bar")); TransactionStatus inner = underTest.getTransaction(second); assertThat(inner.isNewTransaction(), is(false)); ArangoTransactionObject transactionObject = getTransactionObject(inner); - assertThat(transactionObject.createResource().getStreamTransactionId(), is("123")); + assertThat(transactionObject.getResource().getStreamTransactionId(), is("123")); underTest.commit(inner); underTest.commit(outer); verify(database).commitStreamTransaction("123"); @@ -162,12 +156,7 @@ public void getTransactionWithMultipleBridgeCallsWorksForKnownCollections() { definition.setLabels(Collections.singleton("baz")); definition.setTimeout(20); underTest.getTransaction(definition); - when(streamTransaction.getId()) - .thenReturn("123"); - when(database.beginStreamTransaction(any())) - .thenReturn(streamTransaction); - verify(bridge).setCurrentTransaction(beginPassed.capture()); - beginPassed.getValue().apply(Collections.singletonList("foo")); + beginTransaction("123", "foo"); beginPassed.getValue().apply(Arrays.asList("foo", "baz")); verify(database).beginStreamTransaction(optionsPassed.capture()); TransactionCollectionOptions collections = getCollections(optionsPassed.getValue()); @@ -180,12 +169,7 @@ public void getTransactionWithMultipleBridgeCallsFailsForAdditionalCollection() definition.setLabels(Collections.singleton("baz")); definition.setTimeout(20); underTest.getTransaction(definition); - when(streamTransaction.getId()) - .thenReturn("123"); - when(database.beginStreamTransaction(any())) - .thenReturn(streamTransaction); - verify(bridge).setCurrentTransaction(beginPassed.capture()); - beginPassed.getValue().apply(Collections.singletonList("foo")); + beginTransaction("123", "foo"); beginPassed.getValue().apply(Collections.singletonList("bar")); } @@ -196,6 +180,19 @@ public void getTransactionThrowsInvalidIsolationLevelExceptionForIsolationSerial underTest.getTransaction(definition); } + private void beginTransaction(String id, String... collectionNames) { + when(streamTransaction.getId()) + .thenReturn(id); + when(database.beginStreamTransaction(any())) + .thenReturn(streamTransaction); + when(database.getStreamTransaction(any())) + .thenReturn(streamTransaction); + when(streamTransaction.getStatus()) + .thenReturn(StreamTransactionStatus.running); + verify(bridge).setCurrentTransaction(beginPassed.capture()); + beginPassed.getValue().apply(Arrays.asList(collectionNames)); + } + @Nullable private ArangoTransactionObject getTransactionObject(TransactionStatus status) { if (status instanceof DefaultTransactionStatus) { From 32992cbbff6980e809a2e98972d820e0857d0786 Mon Sep 17 00:00:00 2001 From: Arne Burmeister Date: Tue, 30 Aug 2022 16:05:58 +0200 Subject: [PATCH 25/55] #80 add template supporting labels --- .../TransactionAttributeTemplate.java | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 src/main/java/com/arangodb/springframework/transaction/TransactionAttributeTemplate.java diff --git a/src/main/java/com/arangodb/springframework/transaction/TransactionAttributeTemplate.java b/src/main/java/com/arangodb/springframework/transaction/TransactionAttributeTemplate.java new file mode 100644 index 000000000..1a6daa718 --- /dev/null +++ b/src/main/java/com/arangodb/springframework/transaction/TransactionAttributeTemplate.java @@ -0,0 +1,41 @@ +package com.arangodb.springframework.transaction; + +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.interceptor.TransactionAttribute; +import org.springframework.transaction.support.TransactionTemplate; + +import java.util.Collection; +import java.util.Collections; + +public class TransactionAttributeTemplate extends TransactionTemplate implements TransactionAttribute { + + private String qualifier; + private Collection labels = Collections.emptyList(); + + public TransactionAttributeTemplate(PlatformTransactionManager transactionManager) { + super(transactionManager); + } + + @Override + public String getQualifier() { + return qualifier; + } + + public void setQualifier(String qualifier) { + this.qualifier = qualifier; + } + + @Override + public Collection getLabels() { + return labels; + } + + public void setLabels(Collection labels) { + this.labels = labels; + } + + @Override + public boolean rollbackOn(Throwable ex) { + return (ex instanceof RuntimeException || ex instanceof Error); + } +} From 6935c58132f116191c01b8f9455a00415f2ad1a9 Mon Sep 17 00:00:00 2001 From: Arne Burmeister Date: Tue, 30 Aug 2022 17:13:49 +0200 Subject: [PATCH 26/55] #80 add some docs --- ChangeLog.md | 1 + .../transaction/ArangoTransactionManager.java | 18 ++++++----- .../transaction/ArangoTransactionObject.java | 9 ++++-- .../TransactionAttributeTemplate.java | 30 ++++++++++++++++++- 4 files changed, 48 insertions(+), 10 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index 4126acc57..8e6918186 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -62,6 +62,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a - return updated entity from `ArangoOperations.repsert()` (#285) - removed deprecated `AbstractArangoConfiguration` in favor of `ArangoConfiguration` - removed support for Joda-Time +- added support for transactions offering a platform transaction manager based on stream transactions (#80) ## [3.10.0] - 2023-05-17 diff --git a/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManager.java b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManager.java index cf974f72d..23620f6a3 100644 --- a/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManager.java +++ b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManager.java @@ -15,8 +15,6 @@ * Transaction manager using ArangoDB stream transactions on the * {@linkplain ArangoOperations#getDatabaseName()} current database} of the template. * Isolation level {@linkplain TransactionDefinition#ISOLATION_SERIALIZABLE serializable} is not supported. - * - * @see ArangoDatabase#beginStreamTransaction(StreamTransactionOptions) */ public class ArangoTransactionManager extends AbstractPlatformTransactionManager { @@ -26,11 +24,7 @@ public class ArangoTransactionManager extends AbstractPlatformTransactionManager public ArangoTransactionManager(ArangoOperations operations, QueryTransactionBridge bridge) { this.operations = operations; this.bridge = bridge; - setNestedTransactionAllowed(false); - setTransactionSynchronization(SYNCHRONIZATION_ALWAYS); setValidateExistingTransaction(true); - setGlobalRollbackOnParticipationFailure(true); - setRollbackOnCommitFailure(true); } /** @@ -53,6 +47,7 @@ protected Object doGetTransaction() { /** * Configures the new transaction object. The resulting resource will be synchronized and the bridge will be initialized. * + * @see ArangoDatabase#beginStreamTransaction(StreamTransactionOptions) * @see QueryTransactionBridge */ @Override @@ -71,6 +66,8 @@ protected void doBegin(Object transaction, TransactionDefinition definition) thr /** * Commit the current stream transaction iff any. The bridge is cleared afterwards. + * + * @see ArangoDatabase#commitStreamTransaction(String) */ @Override protected void doCommit(DefaultTransactionStatus status) throws TransactionException { @@ -88,6 +85,8 @@ protected void doCommit(DefaultTransactionStatus status) throws TransactionExcep /** * Roll back the current stream transaction iff any. The bridge is cleared afterwards. + * + * @see ArangoDatabase#abortStreamTransaction(String) */ @Override protected void doRollback(DefaultTransactionStatus status) throws TransactionException { @@ -103,9 +102,14 @@ protected void doRollback(DefaultTransactionStatus status) throws TransactionExc } } + /** + * Check if the transaction objects has an underlying stream transaction. + * + * @see ArangoDatabase#getStreamTransaction(String) + */ @Override protected boolean isExistingTransaction(Object transaction) throws TransactionException { - return transaction instanceof ArangoTransactionObject && ((ArangoTransactionObject) transaction).exists(); + return ((ArangoTransactionObject) transaction).exists(); } @Override diff --git a/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionObject.java b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionObject.java index 865742ef1..4953c0ba5 100644 --- a/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionObject.java +++ b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionObject.java @@ -41,7 +41,7 @@ ArangoTransactionResource getResource() { } boolean exists() { - return resource.getStreamTransactionId() != null; + return getStreamTransaction() != null; } void configure(TransactionDefinition definition) { @@ -116,9 +116,14 @@ private void addCollections(Collection collections) { } private boolean isStatus(StreamTransactionStatus status) { + getStreamTransaction(); + return transaction != null && transaction.getStatus() == status; + } + + private StreamTransactionEntity getStreamTransaction() { if (transaction == null && resource.getStreamTransactionId() != null) { transaction = database.getStreamTransaction(resource.getStreamTransactionId()); } - return transaction != null && transaction.getStatus() == status; + return transaction; } } diff --git a/src/main/java/com/arangodb/springframework/transaction/TransactionAttributeTemplate.java b/src/main/java/com/arangodb/springframework/transaction/TransactionAttributeTemplate.java index 1a6daa718..08ad506a8 100644 --- a/src/main/java/com/arangodb/springframework/transaction/TransactionAttributeTemplate.java +++ b/src/main/java/com/arangodb/springframework/transaction/TransactionAttributeTemplate.java @@ -1,21 +1,45 @@ package com.arangodb.springframework.transaction; import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.interceptor.TransactionAttribute; import org.springframework.transaction.support.TransactionTemplate; import java.util.Collection; import java.util.Collections; +import java.util.function.Predicate; +/** + * Template class that simplifies programmatic transaction demarcation and + * transaction exception handling in combination with a transaction manager using labels. + * + * @see ArangoTransactionManager + */ public class TransactionAttributeTemplate extends TransactionTemplate implements TransactionAttribute { + private static final Predicate DEFAULT_ROLLBACK_ON = ex -> (ex instanceof RuntimeException || ex instanceof Error); + private String qualifier; private Collection labels = Collections.emptyList(); + private Predicate rollbackOn = DEFAULT_ROLLBACK_ON; + + public TransactionAttributeTemplate() { + } public TransactionAttributeTemplate(PlatformTransactionManager transactionManager) { super(transactionManager); } + public TransactionAttributeTemplate(PlatformTransactionManager transactionManager, TransactionDefinition transactionDefinition) { + super(transactionManager, transactionDefinition); + if (transactionDefinition instanceof TransactionAttribute) { + TransactionAttribute transactionAttribute = (TransactionAttribute) transactionDefinition; + setQualifier(transactionAttribute.getQualifier()); + setLabels(transactionAttribute.getLabels()); + setRollbackOn(transactionAttribute::rollbackOn); + } + } + @Override public String getQualifier() { return qualifier; @@ -36,6 +60,10 @@ public void setLabels(Collection labels) { @Override public boolean rollbackOn(Throwable ex) { - return (ex instanceof RuntimeException || ex instanceof Error); + return rollbackOn.test(ex); + } + + public void setRollbackOn(Predicate rollbackOn) { + this.rollbackOn = rollbackOn; } } From 05e53ed2f5fb99bbed2e131011d33cdfbb8d3bdd Mon Sep 17 00:00:00 2001 From: Arne Burmeister Date: Tue, 30 Aug 2022 17:29:45 +0200 Subject: [PATCH 27/55] #80 fix nested transaction behaviour keeping collections --- .../transaction/ArangoTransactionManager.java | 43 ++++++++++++++++--- .../transaction/ArangoTransactionObject.java | 19 ++++---- .../ArangoTransactionResource.java | 13 ++++++ .../ArangoTransactionManagerTest.java | 32 +++++++++----- 4 files changed, 81 insertions(+), 26 deletions(-) diff --git a/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManager.java b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManager.java index 23620f6a3..0b577745e 100644 --- a/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManager.java +++ b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManager.java @@ -31,7 +31,7 @@ public ArangoTransactionManager(ArangoOperations operations, QueryTransactionBri * Creates a new transaction object. Any synchronized resource will be reused. */ @Override - protected Object doGetTransaction() { + protected ArangoTransactionObject doGetTransaction() { DbName database = operations.getDatabaseName(); if (logger.isDebugEnabled()) { logger.debug("Create new transaction for database " + database); @@ -58,10 +58,13 @@ protected void doBegin(Object transaction, TransactionDefinition definition) thr } ArangoTransactionObject tx = (ArangoTransactionObject) transaction; tx.configure(definition); - DbName key = operations.getDatabaseName(); - TransactionSynchronizationManager.unbindResourceIfPossible(key); - TransactionSynchronizationManager.bindResource(key, tx.getResource()); - bridge.setCurrentTransaction(collections -> tx.getOrBegin(collections).getStreamTransactionId()); + bridge.setCurrentTransaction(collections -> { + try { + return tx.getOrBegin(collections).getStreamTransactionId(); + } catch (ArangoDBException error) { + throw new TransactionSystemException("Cannot begin transaction", error); + } + }); } /** @@ -118,8 +121,36 @@ protected void doSetRollbackOnly(DefaultTransactionStatus status) throws Transac tx.setRollbackOnly(); } + @Override + protected DefaultTransactionStatus newTransactionStatus(TransactionDefinition definition, Object transaction, boolean newTransaction, boolean newSynchronization, boolean debug, Object suspendedResources) { + return super.newTransactionStatus(definition, transaction, newTransaction, newSynchronization, debug, suspendedResources); + } + + /** + * Bind the resource for the first new transaction created. + */ + @Override + protected void prepareSynchronization(DefaultTransactionStatus status, TransactionDefinition definition) { + super.prepareSynchronization(status, definition); + if (status.isNewTransaction()) { + ArangoTransactionResource resource = ((ArangoTransactionObject) status.getTransaction()).getResource(); + resource.increaseReferences(); + if (resource.isSingleReference()) { + TransactionSynchronizationManager.bindResource(operations.getDatabaseName(), resource); + } + } + } + + /** + * Unbind the resource for the last transaction completed. + */ @Override protected void doCleanupAfterCompletion(Object transaction) { - TransactionSynchronizationManager.unbindResourceIfPossible(operations.getDatabaseName()); + ArangoTransactionResource resource = ((ArangoTransactionObject) transaction).getResource(); + if (resource.isSingleReference()) { + TransactionSynchronizationManager.unbindResource(operations.getDatabaseName()); + } + resource.decreasedReferences(); } + } diff --git a/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionObject.java b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionObject.java index 4953c0ba5..0dcab98f3 100644 --- a/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionObject.java +++ b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionObject.java @@ -1,5 +1,6 @@ package com.arangodb.springframework.transaction; +import com.arangodb.ArangoDBException; import com.arangodb.ArangoDatabase; import com.arangodb.entity.StreamTransactionEntity; import com.arangodb.entity.StreamTransactionStatus; @@ -7,7 +8,6 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.lang.Nullable; -import org.springframework.transaction.IllegalTransactionStateException; import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.interceptor.TransactionAttribute; import org.springframework.transaction.support.SmartTransactionObject; @@ -53,7 +53,7 @@ void configure(TransactionDefinition definition) { } } - ArangoTransactionResource getOrBegin(Collection collections) { + ArangoTransactionResource getOrBegin(Collection collections) throws ArangoDBException { addCollections(collections); if (resource.getStreamTransactionId() != null) { return getResource(); @@ -70,13 +70,13 @@ ArangoTransactionResource getOrBegin(Collection collections) { return getResource(); } - void commit() { + void commit() throws ArangoDBException { if (isStatus(StreamTransactionStatus.running)) { database.commitStreamTransaction(resource.getStreamTransactionId()); } } - void rollback() { + void rollback() throws ArangoDBException { if (isStatus(StreamTransactionStatus.running)) { database.abortStreamTransaction(resource.getStreamTransactionId()); } @@ -104,15 +104,16 @@ public String toString() { private void addCollections(Collection collections) { if (resource.getStreamTransactionId() != null) { - if (!resource.getCollectionNames().containsAll(collections)) { + if (!resource.getCollectionNames().containsAll(collections) && logger.isDebugEnabled()) { Set additional = new HashSet<>(collections); additional.removeAll(resource.getCollectionNames()); - throw new IllegalTransactionStateException("Stream transaction already started on collections " + resource.getCollectionNames() + ", no additional collections allowed: " + additional); + logger.debug("Stream transaction already started on collections " + resource.getCollectionNames() + ", assuming additional collections are read only: " + additional); } + } else { + Set all = new HashSet<>(resource.getCollectionNames()); + all.addAll(collections); + resource.setCollectionNames(all); } - HashSet all = new HashSet<>(resource.getCollectionNames()); - all.addAll(collections); - resource.setCollectionNames(all); } private boolean isStatus(StreamTransactionStatus status) { diff --git a/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionResource.java b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionResource.java index fcacfa074..99a329b5b 100644 --- a/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionResource.java +++ b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionResource.java @@ -18,6 +18,7 @@ class ArangoTransactionResource { private Set collectionNames; private boolean rollbackOnly; + int references = 0; ArangoTransactionResource(@Nullable String streamTransactionId, Set collectionNames, boolean rollbackOnly) { this.streamTransactionId = streamTransactionId; @@ -48,4 +49,16 @@ boolean isRollbackOnly() { void setRollbackOnly(boolean rollbackOnly) { this.rollbackOnly = rollbackOnly; } + + void increaseReferences() { + ++references; + } + + boolean isSingleReference() { + return references <= 1; + } + + void decreasedReferences() { + --references; + } } diff --git a/src/test/java/com/arangodb/springframework/transaction/ArangoTransactionManagerTest.java b/src/test/java/com/arangodb/springframework/transaction/ArangoTransactionManagerTest.java index 1c1431bf2..ec0b3163f 100644 --- a/src/test/java/com/arangodb/springframework/transaction/ArangoTransactionManagerTest.java +++ b/src/test/java/com/arangodb/springframework/transaction/ArangoTransactionManagerTest.java @@ -32,7 +32,9 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.hasItems; +import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.nullValue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; @@ -95,7 +97,7 @@ public void getTransactionReturnsNewTransactionWithoutStreamTransaction() { public void nestedGetTransactionReturnsNewTransactionWithFormerCollections() { DefaultTransactionAttribute first = new DefaultTransactionAttribute(); first.setLabels(Collections.singleton("foo")); - underTest.getTransaction(first); + TransactionStatus outer = underTest.getTransaction(first); DefaultTransactionAttribute second = new DefaultTransactionAttribute(); second.setLabels(Collections.singleton("bar")); TransactionStatus inner = underTest.getTransaction(second); @@ -141,13 +143,19 @@ public void nestedGetTransactionReturnsExistingTransactionWithFormerCollections( DefaultTransactionAttribute second = new DefaultTransactionAttribute(); second.setLabels(Collections.singleton("bar")); - TransactionStatus inner = underTest.getTransaction(second); - assertThat(inner.isNewTransaction(), is(false)); - ArangoTransactionObject transactionObject = getTransactionObject(inner); - assertThat(transactionObject.getResource().getStreamTransactionId(), is("123")); - underTest.commit(inner); + TransactionStatus inner1 = underTest.getTransaction(second); + assertThat(inner1.isNewTransaction(), is(false)); + ArangoTransactionObject tx1 = getTransactionObject(inner1); + assertThat(tx1.getResource().getStreamTransactionId(), is("123")); + underTest.commit(inner1); + TransactionStatus inner2 = underTest.getTransaction(second); + assertThat(inner2.isNewTransaction(), is(false)); + ArangoTransactionObject tx2 = getTransactionObject(inner1); + assertThat(tx2.getResource().getStreamTransactionId(), is("123")); + underTest.commit(inner2); underTest.commit(outer); verify(database).commitStreamTransaction("123"); + assertThat(TransactionSynchronizationManager.getResource(DATABASE_NAME), nullValue()); } @Test @@ -163,14 +171,16 @@ public void getTransactionWithMultipleBridgeCallsWorksForKnownCollections() { assertThat(collections.getWrite(), hasItems("baz", "foo")); } - @Test(expected = IllegalTransactionStateException.class) - public void getTransactionWithMultipleBridgeCallsFailsForAdditionalCollection() { + @Test + public void getTransactionWithMultipleBridgeCallsIgnoresAdditionalCollections() { DefaultTransactionAttribute definition = new DefaultTransactionAttribute(); - definition.setLabels(Collections.singleton("baz")); + definition.setLabels(Collections.singleton("bar")); definition.setTimeout(20); - underTest.getTransaction(definition); + TransactionStatus state = underTest.getTransaction(definition); beginTransaction("123", "foo"); - beginPassed.getValue().apply(Collections.singletonList("bar")); + beginPassed.getValue().apply(Collections.singletonList("baz")); + assertThat(getTransactionObject(state).getResource().getCollectionNames(), hasItems("foo", "bar")); + assertThat(getTransactionObject(state).getResource().getCollectionNames(), not(hasItem("baz"))); } @Test(expected = InvalidIsolationLevelException.class) From 501305c016c54f4dbbc471293a2ec44027ac4a11 Mon Sep 17 00:00:00 2001 From: Arne Burmeister Date: Thu, 1 Sep 2022 13:11:35 +0200 Subject: [PATCH 28/55] #80 minor code improvements --- .../springframework/core/ArangoOperations.java | 15 ++++++++------- .../core/template/ArangoTemplate.java | 6 ------ .../repository/query/AbstractArangoQuery.java | 6 +++--- 3 files changed, 11 insertions(+), 16 deletions(-) diff --git a/src/main/java/com/arangodb/springframework/core/ArangoOperations.java b/src/main/java/com/arangodb/springframework/core/ArangoOperations.java index 06de184b0..9398b9a38 100644 --- a/src/main/java/com/arangodb/springframework/core/ArangoOperations.java +++ b/src/main/java/com/arangodb/springframework/core/ArangoOperations.java @@ -93,23 +93,24 @@ default ArangoCursor query(String query, Map bindVars, Cl } /** - * Performs a database query using the given {@code query}, then returns a new {@code ArangoCursor} instance for the - * result list. + * Performs a database query using the given {@code query}, then returns a new {@code ArangoCursor} + * instance for the result list. * * @param query * An AQL query string - * @param options - * Additional options that will be passed to the query API, can be null * @param entityClass * The entity type of the result * @return cursor of the results * @throws DataAccessException */ - ArangoCursor query(String query, AqlQueryOptions options, Class entityClass) throws DataAccessException; + default ArangoCursor query(String query, AqlQueryOptions options, Class entityClass) + throws DataAccessException { + return query(query, null, options, entityClass); + } /** - * Performs a database query using the given {@code query}, then returns a new {@code ArangoCursor} instance for the - * result list. + * Performs a database query using the given {@code query}, then returns a new {@code ArangoCursor} + * instance for the result list. * * @param query * An AQL query string diff --git a/src/main/java/com/arangodb/springframework/core/template/ArangoTemplate.java b/src/main/java/com/arangodb/springframework/core/template/ArangoTemplate.java index aeef173e4..798102032 100644 --- a/src/main/java/com/arangodb/springframework/core/template/ArangoTemplate.java +++ b/src/main/java/com/arangodb/springframework/core/template/ArangoTemplate.java @@ -265,12 +265,6 @@ public DbName getDatabaseName() { : databaseName); } - @Override - public ArangoCursor query(final String query, final AqlQueryOptions options, final Class entityClass) - throws DataAccessException { - return query(query, null, options, entityClass); - } - @Override public ArangoCursor query(final String query, final Map bindVars, final AqlQueryOptions options, final Class entityClass) throws DataAccessException { diff --git a/src/main/java/com/arangodb/springframework/repository/query/AbstractArangoQuery.java b/src/main/java/com/arangodb/springframework/repository/query/AbstractArangoQuery.java index 3f3def6bc..56647bfd7 100644 --- a/src/main/java/com/arangodb/springframework/repository/query/AbstractArangoQuery.java +++ b/src/main/java/com/arangodb/springframework/repository/query/AbstractArangoQuery.java @@ -78,16 +78,16 @@ public Object execute(final Object[] parameters) { options.fullCount(true); } - final QueryWithCollections queryAndCollection = createQuery(accessor, bindVars, options); + final QueryWithCollections query = createQuery(accessor, bindVars, options); if (options.getStreamTransactionId() == null && transactionBridge != null) { - options.streamTransactionId(transactionBridge.getCurrentTransaction(queryAndCollection.getCollections())); + options.streamTransactionId(transactionBridge.getCurrentTransaction(query.getCollections())); } final ResultProcessor processor = method.getResultProcessor().withDynamicProjection(accessor); final Class typeToRead = getTypeToRead(processor); - final ArangoCursor result = operations.query(queryAndCollection.getQuery(), bindVars, options, typeToRead); + final ArangoCursor result = operations.query(query.getQuery(), bindVars, options, typeToRead); logWarningsIfNecessary(result); return processor.processResult(convertResult(result, accessor)); } From 8875188a3e7e6b2915b238f4136000ca1dac4faa Mon Sep 17 00:00:00 2001 From: Arne Burmeister Date: Thu, 1 Sep 2022 13:20:10 +0200 Subject: [PATCH 29/55] #80 add missing license comment --- .../resolver/DefaultResolverFactory.java | 20 +++++++++++++++++++ .../query/QueryTransactionBridge.java | 20 +++++++++++++++++++ .../query/QueryWithCollections.java | 20 +++++++++++++++++++ ...ArangoTransactionManagementConfigurer.java | 20 +++++++++++++++++++ .../transaction/ArangoTransactionManager.java | 20 +++++++++++++++++++ .../transaction/ArangoTransactionObject.java | 20 +++++++++++++++++++ .../ArangoTransactionResource.java | 20 +++++++++++++++++++ .../TransactionAttributeTemplate.java | 20 +++++++++++++++++++ 8 files changed, 160 insertions(+) diff --git a/src/main/java/com/arangodb/springframework/core/convert/resolver/DefaultResolverFactory.java b/src/main/java/com/arangodb/springframework/core/convert/resolver/DefaultResolverFactory.java index 77df50ed5..a286f7cab 100644 --- a/src/main/java/com/arangodb/springframework/core/convert/resolver/DefaultResolverFactory.java +++ b/src/main/java/com/arangodb/springframework/core/convert/resolver/DefaultResolverFactory.java @@ -1,3 +1,23 @@ +/* + * DISCLAIMER + * + * Copyright 2017 ArangoDB GmbH, Cologne, Germany + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Copyright holder is ArangoDB GmbH, Cologne, Germany + */ + package com.arangodb.springframework.core.convert.resolver; import java.lang.annotation.Annotation; diff --git a/src/main/java/com/arangodb/springframework/repository/query/QueryTransactionBridge.java b/src/main/java/com/arangodb/springframework/repository/query/QueryTransactionBridge.java index 80a418001..1b6667790 100644 --- a/src/main/java/com/arangodb/springframework/repository/query/QueryTransactionBridge.java +++ b/src/main/java/com/arangodb/springframework/repository/query/QueryTransactionBridge.java @@ -1,3 +1,23 @@ +/* + * DISCLAIMER + * + * Copyright 2017 ArangoDB GmbH, Cologne, Germany + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Copyright holder is ArangoDB GmbH, Cologne, Germany + */ + package com.arangodb.springframework.repository.query; import org.springframework.core.NamedInheritableThreadLocal; diff --git a/src/main/java/com/arangodb/springframework/repository/query/QueryWithCollections.java b/src/main/java/com/arangodb/springframework/repository/query/QueryWithCollections.java index 8300feab9..19c249ece 100644 --- a/src/main/java/com/arangodb/springframework/repository/query/QueryWithCollections.java +++ b/src/main/java/com/arangodb/springframework/repository/query/QueryWithCollections.java @@ -1,3 +1,23 @@ +/* + * DISCLAIMER + * + * Copyright 2017 ArangoDB GmbH, Cologne, Germany + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Copyright holder is ArangoDB GmbH, Cologne, Germany + */ + package com.arangodb.springframework.repository.query; import java.util.Collection; diff --git a/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManagementConfigurer.java b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManagementConfigurer.java index 1d6550b9a..b3624c934 100644 --- a/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManagementConfigurer.java +++ b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManagementConfigurer.java @@ -1,3 +1,23 @@ +/* + * DISCLAIMER + * + * Copyright 2017 ArangoDB GmbH, Cologne, Germany + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Copyright holder is ArangoDB GmbH, Cologne, Germany + */ + package com.arangodb.springframework.transaction; import org.springframework.beans.factory.annotation.Autowired; diff --git a/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManager.java b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManager.java index 0b577745e..cc9e00181 100644 --- a/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManager.java +++ b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManager.java @@ -1,3 +1,23 @@ +/* + * DISCLAIMER + * + * Copyright 2017 ArangoDB GmbH, Cologne, Germany + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Copyright holder is ArangoDB GmbH, Cologne, Germany + */ + package com.arangodb.springframework.transaction; import com.arangodb.ArangoDBException; diff --git a/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionObject.java b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionObject.java index 0dcab98f3..13c5d4cea 100644 --- a/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionObject.java +++ b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionObject.java @@ -1,3 +1,23 @@ +/* + * DISCLAIMER + * + * Copyright 2017 ArangoDB GmbH, Cologne, Germany + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Copyright holder is ArangoDB GmbH, Cologne, Germany + */ + package com.arangodb.springframework.transaction; import com.arangodb.ArangoDBException; diff --git a/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionResource.java b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionResource.java index 99a329b5b..8cb908bf8 100644 --- a/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionResource.java +++ b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionResource.java @@ -1,3 +1,23 @@ +/* + * DISCLAIMER + * + * Copyright 2017 ArangoDB GmbH, Cologne, Germany + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Copyright holder is ArangoDB GmbH, Cologne, Germany + */ + package com.arangodb.springframework.transaction; import org.springframework.lang.Nullable; diff --git a/src/main/java/com/arangodb/springframework/transaction/TransactionAttributeTemplate.java b/src/main/java/com/arangodb/springframework/transaction/TransactionAttributeTemplate.java index 08ad506a8..2abc783ca 100644 --- a/src/main/java/com/arangodb/springframework/transaction/TransactionAttributeTemplate.java +++ b/src/main/java/com/arangodb/springframework/transaction/TransactionAttributeTemplate.java @@ -1,3 +1,23 @@ +/* + * DISCLAIMER + * + * Copyright 2017 ArangoDB GmbH, Cologne, Germany + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Copyright holder is ArangoDB GmbH, Cologne, Germany + */ + package com.arangodb.springframework.transaction; import org.springframework.transaction.PlatformTransactionManager; From 31e461b5da1574b87c84e6521fde756aa4c31b98 Mon Sep 17 00:00:00 2001 From: Arne Burmeister Date: Thu, 1 Sep 2022 17:55:34 +0200 Subject: [PATCH 30/55] #80 fix resolver setup --- .../springframework/config/ArangoConfiguration.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/arangodb/springframework/config/ArangoConfiguration.java b/src/main/java/com/arangodb/springframework/config/ArangoConfiguration.java index 6a4c81d34..724d79d37 100644 --- a/src/main/java/com/arangodb/springframework/config/ArangoConfiguration.java +++ b/src/main/java/com/arangodb/springframework/config/ArangoConfiguration.java @@ -142,9 +142,6 @@ default ArangoTypeMapper arangoTypeMapper() throws Exception { @Bean default ResolverFactory resolverFactory() { - return RESOLVER_FACTORY_INSTANCE; - } - - ResolverFactory RESOLVER_FACTORY_INSTANCE = new DefaultResolverFactory(); - + return new DefaultResolverFactory(); + } } \ No newline at end of file From a471901104cc625e819f88e9e06d20de7223a012 Mon Sep 17 00:00:00 2001 From: Arne Burmeister Date: Thu, 1 Sep 2022 18:08:31 +0200 Subject: [PATCH 31/55] #80 fix test setup --- .../springframework/ArangoTransactionalTestConfiguration.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/com/arangodb/springframework/ArangoTransactionalTestConfiguration.java b/src/test/java/com/arangodb/springframework/ArangoTransactionalTestConfiguration.java index 5b14b8610..29b0216bd 100644 --- a/src/test/java/com/arangodb/springframework/ArangoTransactionalTestConfiguration.java +++ b/src/test/java/com/arangodb/springframework/ArangoTransactionalTestConfiguration.java @@ -12,6 +12,6 @@ @EnableTransactionManagement @TestExecutionListeners(TransactionalTestExecutionListener.class) @Import(ArangoTransactionManagementConfigurer.class) -public class ArangoTransactionalTestConfiguration extends ArangoTestConfiguration { +public class ArangoTransactionalTestConfiguration { } From 77e9e55ee5664c14b7d6c5b71708636f0363a8df Mon Sep 17 00:00:00 2001 From: Arne Burmeister Date: Tue, 14 Mar 2023 12:52:24 +0100 Subject: [PATCH 32/55] #80 fix handling of nested transactions --- .../query/QueryTransactionBridge.java | 32 ++++- ...urce.java => ArangoTransactionHolder.java} | 51 +++----- .../transaction/ArangoTransactionManager.java | 121 ++++++++++++------ .../transaction/ArangoTransactionObject.java | 96 +++++--------- .../ArangoTransactionManagerTest.java | 60 ++++++--- 5 files changed, 206 insertions(+), 154 deletions(-) rename src/main/java/com/arangodb/springframework/transaction/{ArangoTransactionResource.java => ArangoTransactionHolder.java} (54%) diff --git a/src/main/java/com/arangodb/springframework/repository/query/QueryTransactionBridge.java b/src/main/java/com/arangodb/springframework/repository/query/QueryTransactionBridge.java index 1b6667790..11efe8760 100644 --- a/src/main/java/com/arangodb/springframework/repository/query/QueryTransactionBridge.java +++ b/src/main/java/com/arangodb/springframework/repository/query/QueryTransactionBridge.java @@ -29,21 +29,41 @@ * Bridge to postpone late transaction start to be able to inject collections from query side. */ public class QueryTransactionBridge { - private static final Function, String> NO_TRANSACTION = any -> null; - private static final ThreadLocal, String>> CURRENT_TRANSACTION = new NamedInheritableThreadLocal<>("ArangoTransactionBegin"); - public QueryTransactionBridge() { - CURRENT_TRANSACTION.set(NO_TRANSACTION); - } + private static final ThreadLocal, String>> CURRENT_TRANSACTION = new NamedInheritableThreadLocal, String>>("ArangoTransactionBegin") { + @Override + protected Function, String> initialValue() { + return any -> null; + } + }; + /** + * Prepare the bridge for accepting transaction begin. + * @param begin a function accepting collection names and returning a stream transaction id + * + * @see com.arangodb.springframework.transaction.ArangoTransactionManager + */ public void setCurrentTransaction(Function, String> begin) { CURRENT_TRANSACTION.set(begin); } + /** + * Reset the bridge ignoring transaction begin. + * + * @see com.arangodb.springframework.transaction.ArangoTransactionManager + */ public void clearCurrentTransaction() { - CURRENT_TRANSACTION.set(NO_TRANSACTION); + CURRENT_TRANSACTION.remove(); } + /** + * Applies the collection names to any current transaction. + * @param collections additional collection names + * @return the stream transaction id or {@code null} without transaction + * + * @see AbstractArangoQuery + * @see com.arangodb.springframework.repository.SimpleArangoRepository + */ public String getCurrentTransaction(Collection collections) { return CURRENT_TRANSACTION.get().apply(collections); } diff --git a/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionResource.java b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionHolder.java similarity index 54% rename from src/main/java/com/arangodb/springframework/transaction/ArangoTransactionResource.java rename to src/main/java/com/arangodb/springframework/transaction/ArangoTransactionHolder.java index 8cb908bf8..4a47dec7f 100644 --- a/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionResource.java +++ b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionHolder.java @@ -20,9 +20,12 @@ package com.arangodb.springframework.transaction; +import com.arangodb.entity.StreamTransactionEntity; +import com.arangodb.entity.StreamTransactionStatus; import org.springframework.lang.Nullable; import org.springframework.transaction.support.TransactionSynchronizationManager; +import java.util.Collection; import java.util.HashSet; import java.util.Set; @@ -32,53 +35,41 @@ * @see TransactionSynchronizationManager#bindResource(Object, Object) * @see ArangoTransactionObject */ -class ArangoTransactionResource { +class ArangoTransactionHolder { - private String streamTransactionId; - private Set collectionNames; - - private boolean rollbackOnly; - int references = 0; - - ArangoTransactionResource(@Nullable String streamTransactionId, Set collectionNames, boolean rollbackOnly) { - this.streamTransactionId = streamTransactionId; - setCollectionNames(collectionNames); - this.rollbackOnly = rollbackOnly; - } + private final Set collectionNames = new HashSet<>(); + private StreamTransactionEntity transaction = null; + private boolean rollbackOnly = false; + @Nullable String getStreamTransactionId() { - return streamTransactionId; + return transaction == null ? null : transaction.getId(); } - void setStreamTransactionId(String streamTransactionId) { - this.streamTransactionId = streamTransactionId; + void setStreamTransaction(StreamTransactionEntity transaction) { + this.transaction = transaction; } Set getCollectionNames() { return collectionNames; } - void setCollectionNames(Set collectionNames) { - this.collectionNames = new HashSet<>(collectionNames); + void addCollectionNames(Collection collectionNames) { + if (transaction != null) { + throw new IllegalStateException("Collections must not be added after stream transaction begun"); + } + this.collectionNames.addAll(collectionNames); } boolean isRollbackOnly() { - return rollbackOnly; - } - - void setRollbackOnly(boolean rollbackOnly) { - this.rollbackOnly = rollbackOnly; - } - - void increaseReferences() { - ++references; + return rollbackOnly || isStatus(StreamTransactionStatus.aborted); } - boolean isSingleReference() { - return references <= 1; + void setRollbackOnly() { + rollbackOnly = true; } - void decreasedReferences() { - --references; + public boolean isStatus(StreamTransactionStatus status) { + return transaction != null && transaction.getStatus() == status; } } diff --git a/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManager.java b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManager.java index cc9e00181..862aaffc8 100644 --- a/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManager.java +++ b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManager.java @@ -23,20 +23,30 @@ import com.arangodb.ArangoDBException; import com.arangodb.ArangoDatabase; import com.arangodb.DbName; -import com.arangodb.model.StreamTransactionOptions; import com.arangodb.springframework.core.ArangoOperations; import com.arangodb.springframework.repository.query.QueryTransactionBridge; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.lang.Nullable; import org.springframework.transaction.*; import org.springframework.transaction.support.AbstractPlatformTransactionManager; import org.springframework.transaction.support.DefaultTransactionStatus; import org.springframework.transaction.support.TransactionSynchronizationManager; +import java.util.function.Function; + /** * Transaction manager using ArangoDB stream transactions on the - * {@linkplain ArangoOperations#getDatabaseName()} current database} of the template. - * Isolation level {@linkplain TransactionDefinition#ISOLATION_SERIALIZABLE serializable} is not supported. + * {@linkplain ArangoOperations#getDatabaseName() current database} of the + * template. A {@linkplain ArangoTransactionObject transaction object} using + * a shared {@linkplain ArangoTransactionHolder holder} is used for the + * {@link DefaultTransactionStatus}. Neither + * {@linkplain TransactionDefinition#getPropagationBehavior() propagation} + * {@linkplain TransactionDefinition#PROPAGATION_NESTED nested} nor + * {@linkplain TransactionDefinition#getIsolationLevel() isolation} + * {@linkplain TransactionDefinition#ISOLATION_SERIALIZABLE serializable} are + * supported. */ -public class ArangoTransactionManager extends AbstractPlatformTransactionManager { +public class ArangoTransactionManager extends AbstractPlatformTransactionManager implements InitializingBean { private final ArangoOperations operations; private final QueryTransactionBridge bridge; @@ -44,40 +54,54 @@ public class ArangoTransactionManager extends AbstractPlatformTransactionManager public ArangoTransactionManager(ArangoOperations operations, QueryTransactionBridge bridge) { this.operations = operations; this.bridge = bridge; - setValidateExistingTransaction(true); + super.setGlobalRollbackOnParticipationFailure(true); + super.setTransactionSynchronization(SYNCHRONIZATION_ON_ACTUAL_TRANSACTION); + } + + /** + * Check for supported property settings. + */ + @Override + public void afterPropertiesSet() { + if (isNestedTransactionAllowed()) { + throw new IllegalStateException("Nested transactions must not be allowed"); + } + if (!isGlobalRollbackOnParticipationFailure()) { + throw new IllegalStateException("Global rollback on participating failure is needed"); + } + if (getTransactionSynchronization() == SYNCHRONIZATION_NEVER) { + throw new IllegalStateException("Transaction synchronization is needed always"); + } } /** - * Creates a new transaction object. Any synchronized resource will be reused. + * Creates a new transaction object. Any holder bound will be reused. */ @Override protected ArangoTransactionObject doGetTransaction() { DbName database = operations.getDatabaseName(); - if (logger.isDebugEnabled()) { - logger.debug("Create new transaction for database " + database); - } + ArangoTransactionHolder holder = (ArangoTransactionHolder) TransactionSynchronizationManager.getResource(database); try { - ArangoTransactionResource resource = (ArangoTransactionResource) TransactionSynchronizationManager.getResource(database); - return new ArangoTransactionObject(operations.driver().db(database), getDefaultTimeout(), resource); + return new ArangoTransactionObject(operations.driver().db(database), getDefaultTimeout(), holder); } catch (ArangoDBException error) { throw new TransactionSystemException("Cannot create transaction object", error); } } /** - * Configures the new transaction object. The resulting resource will be synchronized and the bridge will be initialized. + * Connect the new transaction object to the query bridge. * - * @see ArangoDatabase#beginStreamTransaction(StreamTransactionOptions) - * @see QueryTransactionBridge + * @see QueryTransactionBridge#setCurrentTransaction(Function) + * @see #prepareSynchronization(DefaultTransactionStatus, TransactionDefinition) + * @throws InvalidIsolationLevelException for {@link TransactionDefinition#ISOLATION_SERIALIZABLE} */ @Override - protected void doBegin(Object transaction, TransactionDefinition definition) throws TransactionUsageException { + protected void doBegin(Object transaction, TransactionDefinition definition) throws InvalidIsolationLevelException { int isolationLevel = definition.getIsolationLevel(); - if (isolationLevel != -1 && (isolationLevel & TransactionDefinition.ISOLATION_SERIALIZABLE) != 0) { + if (isolationLevel != TransactionDefinition.ISOLATION_DEFAULT && (isolationLevel & TransactionDefinition.ISOLATION_SERIALIZABLE) != 0) { throw new InvalidIsolationLevelException("ArangoDB does not support isolation level serializable"); } ArangoTransactionObject tx = (ArangoTransactionObject) transaction; - tx.configure(definition); bridge.setCurrentTransaction(collections -> { try { return tx.getOrBegin(collections).getStreamTransactionId(); @@ -88,9 +112,11 @@ protected void doBegin(Object transaction, TransactionDefinition definition) thr } /** - * Commit the current stream transaction iff any. The bridge is cleared afterwards. + * Commit the current stream transaction. The query bridge is cleared + * afterwards. * * @see ArangoDatabase#commitStreamTransaction(String) + * @see QueryTransactionBridge#clearCurrentTransaction() */ @Override protected void doCommit(DefaultTransactionStatus status) throws TransactionException { @@ -100,16 +126,19 @@ protected void doCommit(DefaultTransactionStatus status) throws TransactionExcep } try { tx.commit(); - bridge.clearCurrentTransaction(); } catch (ArangoDBException error) { throw new TransactionSystemException("Cannot commit transaction " + tx, error); + } finally { + bridge.clearCurrentTransaction(); } } /** - * Roll back the current stream transaction iff any. The bridge is cleared afterwards. + * Roll back the current stream transaction. The query bridge is cleared + * afterwards. * * @see ArangoDatabase#abortStreamTransaction(String) + * @see QueryTransactionBridge#clearCurrentTransaction() */ @Override protected void doRollback(DefaultTransactionStatus status) throws TransactionException { @@ -119,58 +148,68 @@ protected void doRollback(DefaultTransactionStatus status) throws TransactionExc } try { tx.rollback(); - bridge.clearCurrentTransaction(); } catch (ArangoDBException error) { throw new TransactionSystemException("Cannot roll back transaction " + tx, error); + } finally { + bridge.clearCurrentTransaction(); } } /** - * Check if the transaction objects has an underlying stream transaction. - * - * @see ArangoDatabase#getStreamTransaction(String) + * Check if the transaction object has the bound holder. For new + * transactions the holder will be bound afterwards. */ @Override protected boolean isExistingTransaction(Object transaction) throws TransactionException { - return ((ArangoTransactionObject) transaction).exists(); + ArangoTransactionHolder holder = ((ArangoTransactionObject) transaction).getHolder(); + return holder == TransactionSynchronizationManager.getResource(operations.getDatabaseName()); } + /** + * Mark the transaction as global rollback only. + * + * @see #isGlobalRollbackOnParticipationFailure() + */ @Override protected void doSetRollbackOnly(DefaultTransactionStatus status) throws TransactionException { ArangoTransactionObject tx = (ArangoTransactionObject) status.getTransaction(); - tx.setRollbackOnly(); + tx.getHolder().setRollbackOnly(); } + /** + * Any transaction object is configured according to the definition upfront. + * + * @see ArangoTransactionObject#configure(TransactionDefinition) + */ @Override - protected DefaultTransactionStatus newTransactionStatus(TransactionDefinition definition, Object transaction, boolean newTransaction, boolean newSynchronization, boolean debug, Object suspendedResources) { + protected DefaultTransactionStatus newTransactionStatus(TransactionDefinition definition, @Nullable Object transaction, boolean newTransaction, boolean newSynchronization, boolean debug, @Nullable Object suspendedResources) { + if (transaction instanceof ArangoTransactionObject) { + ((ArangoTransactionObject) transaction).configure(definition); + } return super.newTransactionStatus(definition, transaction, newTransaction, newSynchronization, debug, suspendedResources); } /** - * Bind the resource for the first new transaction created. + * Bind the holder for the first new transaction created. + * + * @see ArangoTransactionHolder */ @Override protected void prepareSynchronization(DefaultTransactionStatus status, TransactionDefinition definition) { super.prepareSynchronization(status, definition); - if (status.isNewTransaction()) { - ArangoTransactionResource resource = ((ArangoTransactionObject) status.getTransaction()).getResource(); - resource.increaseReferences(); - if (resource.isSingleReference()) { - TransactionSynchronizationManager.bindResource(operations.getDatabaseName(), resource); - } + if (status.isNewSynchronization()) { + ArangoTransactionHolder holder = ((ArangoTransactionObject) status.getTransaction()).getHolder(); + TransactionSynchronizationManager.bindResource(operations.getDatabaseName(), holder); } } /** - * Unbind the resource for the last transaction completed. + * Unbind the holder from the last transaction completed. + * + * @see ArangoTransactionHolder */ @Override protected void doCleanupAfterCompletion(Object transaction) { - ArangoTransactionResource resource = ((ArangoTransactionObject) transaction).getResource(); - if (resource.isSingleReference()) { - TransactionSynchronizationManager.unbindResource(operations.getDatabaseName()); - } - resource.decreasedReferences(); + TransactionSynchronizationManager.unbindResource(operations.getDatabaseName()); } - } diff --git a/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionObject.java b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionObject.java index 13c5d4cea..9566b7690 100644 --- a/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionObject.java +++ b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionObject.java @@ -22,7 +22,6 @@ import com.arangodb.ArangoDBException; import com.arangodb.ArangoDatabase; -import com.arangodb.entity.StreamTransactionEntity; import com.arangodb.entity.StreamTransactionStatus; import com.arangodb.model.StreamTransactionOptions; import org.apache.commons.logging.Log; @@ -31,41 +30,37 @@ import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.interceptor.TransactionAttribute; import org.springframework.transaction.support.SmartTransactionObject; -import org.springframework.transaction.support.TransactionSynchronizationUtils; import java.util.Collection; -import java.util.Collections; import java.util.HashSet; import java.util.Set; +import static org.springframework.transaction.TransactionDefinition.TIMEOUT_DEFAULT; + /** - * Transaction object created by {@link ArangoTransactionManager}. + * Transaction object created by + * {@link ArangoTransactionManager#doGetTransaction()}. */ class ArangoTransactionObject implements SmartTransactionObject { private static final Log logger = LogFactory.getLog(ArangoTransactionObject.class); private final ArangoDatabase database; - private final ArangoTransactionResource resource; + private final ArangoTransactionHolder holder; private int timeout; - private StreamTransactionEntity transaction; - ArangoTransactionObject(ArangoDatabase database, int defaultTimeout, @Nullable ArangoTransactionResource resource) { + ArangoTransactionObject(ArangoDatabase database, int defaultTimeout, @Nullable ArangoTransactionHolder holder) { this.database = database; - this.resource = resource == null ? new ArangoTransactionResource(null, Collections.emptySet(), false) : resource; + this.holder = holder == null ? new ArangoTransactionHolder() : holder; this.timeout = defaultTimeout; } - ArangoTransactionResource getResource() { - return resource; - } - - boolean exists() { - return getStreamTransaction() != null; + ArangoTransactionHolder getHolder() { + return holder; } void configure(TransactionDefinition definition) { - if (definition.getTimeout() != -1) { + if (definition.getTimeout() != TIMEOUT_DEFAULT) { this.timeout = definition.getTimeout(); } if (definition instanceof TransactionAttribute) { @@ -73,78 +68,55 @@ void configure(TransactionDefinition definition) { } } - ArangoTransactionResource getOrBegin(Collection collections) throws ArangoDBException { + ArangoTransactionHolder getOrBegin(Collection collections) throws ArangoDBException { addCollections(collections); - if (resource.getStreamTransactionId() != null) { - return getResource(); - } - StreamTransactionOptions options = new StreamTransactionOptions() - .allowImplicit(true) - .writeCollections(resource.getCollectionNames().toArray(new String[0])) - .lockTimeout(Math.max(timeout, 0)); - transaction = database.beginStreamTransaction(options); - resource.setStreamTransactionId(transaction.getId()); - if (logger.isDebugEnabled()) { - logger.debug("Began stream transaction " + resource.getStreamTransactionId() + " writing collections " + resource.getCollectionNames()); + if (holder.getStreamTransactionId() == null) { + StreamTransactionOptions options = new StreamTransactionOptions() + .allowImplicit(true) + .writeCollections(holder.getCollectionNames().toArray(new String[0])) + .lockTimeout(Math.max(timeout, 0)); + holder.setStreamTransaction(database.beginStreamTransaction(options)); + if (logger.isDebugEnabled()) { + logger.debug("Began stream transaction " + holder.getStreamTransactionId() + " writing collections " + holder.getCollectionNames()); + } } - return getResource(); + return getHolder(); } void commit() throws ArangoDBException { - if (isStatus(StreamTransactionStatus.running)) { - database.commitStreamTransaction(resource.getStreamTransactionId()); + if (holder.isStatus(StreamTransactionStatus.running)) { + holder.setStreamTransaction(database.commitStreamTransaction(holder.getStreamTransactionId())); } } void rollback() throws ArangoDBException { - if (isStatus(StreamTransactionStatus.running)) { - database.abortStreamTransaction(resource.getStreamTransactionId()); + holder.setRollbackOnly(); + if (holder.isStatus(StreamTransactionStatus.running)) { + holder.setStreamTransaction(database.abortStreamTransaction(holder.getStreamTransactionId())); } - setRollbackOnly(); } @Override public boolean isRollbackOnly() { - return resource.isRollbackOnly() || isStatus(StreamTransactionStatus.aborted); - } - - public void setRollbackOnly() { - resource.setRollbackOnly(true); + return holder.isRollbackOnly(); } @Override public void flush() { - TransactionSynchronizationUtils.triggerFlush(); } @Override public String toString() { - return resource.getStreamTransactionId() == null ? "(not begun)" : resource.getStreamTransactionId(); + return holder.getStreamTransactionId() == null ? "(not begun)" : holder.getStreamTransactionId(); } private void addCollections(Collection collections) { - if (resource.getStreamTransactionId() != null) { - if (!resource.getCollectionNames().containsAll(collections) && logger.isDebugEnabled()) { - Set additional = new HashSet<>(collections); - additional.removeAll(resource.getCollectionNames()); - logger.debug("Stream transaction already started on collections " + resource.getCollectionNames() + ", assuming additional collections are read only: " + additional); - } - } else { - Set all = new HashSet<>(resource.getCollectionNames()); - all.addAll(collections); - resource.setCollectionNames(all); - } - } - - private boolean isStatus(StreamTransactionStatus status) { - getStreamTransaction(); - return transaction != null && transaction.getStatus() == status; - } - - private StreamTransactionEntity getStreamTransaction() { - if (transaction == null && resource.getStreamTransactionId() != null) { - transaction = database.getStreamTransaction(resource.getStreamTransactionId()); + if (holder.getStreamTransactionId() == null) { + holder.addCollectionNames(collections); + } else if (logger.isDebugEnabled() && !holder.getCollectionNames().containsAll(collections)) { + Set additional = new HashSet<>(collections); + additional.removeAll(holder.getCollectionNames()); + logger.debug("Stream transaction already started on collections " + holder.getCollectionNames() + ", assuming additional collections are read only: " + additional); } - return transaction; } } diff --git a/src/test/java/com/arangodb/springframework/transaction/ArangoTransactionManagerTest.java b/src/test/java/com/arangodb/springframework/transaction/ArangoTransactionManagerTest.java index ec0b3163f..d3b044def 100644 --- a/src/test/java/com/arangodb/springframework/transaction/ArangoTransactionManagerTest.java +++ b/src/test/java/com/arangodb/springframework/transaction/ArangoTransactionManagerTest.java @@ -37,9 +37,7 @@ import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.nullValue; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.when; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.*; import static org.springframework.beans.PropertyAccessorFactory.forDirectFieldAccess; @RunWith(MockitoJUnitRunner.class) @@ -86,7 +84,7 @@ public void getTransactionReturnsNewTransactionWithoutStreamTransaction() { assertThat(status.isNewTransaction(), is(true)); verify(driver).db(DATABASE_NAME); verify(bridge).setCurrentTransaction(any()); - ArangoTransactionResource resource = (ArangoTransactionResource) TransactionSynchronizationManager.getResource(DATABASE_NAME); + ArangoTransactionHolder resource = (ArangoTransactionHolder) TransactionSynchronizationManager.getResource(DATABASE_NAME); assertThat(resource.getStreamTransactionId(), nullValue()); assertThat(resource.getCollectionNames(), empty()); assertThat(resource.isRollbackOnly(), is(false)); @@ -94,16 +92,16 @@ public void getTransactionReturnsNewTransactionWithoutStreamTransaction() { } @Test - public void nestedGetTransactionReturnsNewTransactionWithFormerCollections() { + public void innerGetTransactionIsNotNewTransactionIncludingFormerCollections() { DefaultTransactionAttribute first = new DefaultTransactionAttribute(); first.setLabels(Collections.singleton("foo")); TransactionStatus outer = underTest.getTransaction(first); DefaultTransactionAttribute second = new DefaultTransactionAttribute(); second.setLabels(Collections.singleton("bar")); TransactionStatus inner = underTest.getTransaction(second); - assertThat(inner.isNewTransaction(), is(true)); + assertThat(inner.isNewTransaction(), is(false)); ArangoTransactionObject transactionObject = getTransactionObject(inner); - assertThat(transactionObject.getResource().getCollectionNames(), hasItems("foo", "bar")); + assertThat(transactionObject.getHolder().getCollectionNames(), hasItems("foo", "bar")); verifyNoInteractions(database); } @@ -112,7 +110,11 @@ public void innerRollbackCausesUnexpectedRollbackOnOuterCommit() { TransactionStatus outer = underTest.getTransaction(new DefaultTransactionAttribute()); TransactionStatus inner = underTest.getTransaction(new DefaultTransactionAttribute()); underTest.rollback(inner); - underTest.commit(outer); + try { + underTest.commit(outer); + } finally { + assertThat(TransactionSynchronizationManager.getResource(DATABASE_NAME), nullValue()); + } } @Test @@ -140,24 +142,54 @@ public void nestedGetTransactionReturnsExistingTransactionWithFormerCollections( assertThat(outer.isNewTransaction(), is(true)); beginTransaction("123", "foo", "bar"); + when(streamTransaction.getStatus()) + .thenReturn(StreamTransactionStatus.running); DefaultTransactionAttribute second = new DefaultTransactionAttribute(); second.setLabels(Collections.singleton("bar")); TransactionStatus inner1 = underTest.getTransaction(second); assertThat(inner1.isNewTransaction(), is(false)); ArangoTransactionObject tx1 = getTransactionObject(inner1); - assertThat(tx1.getResource().getStreamTransactionId(), is("123")); + assertThat(tx1.getHolder().getStreamTransactionId(), is("123")); underTest.commit(inner1); TransactionStatus inner2 = underTest.getTransaction(second); assertThat(inner2.isNewTransaction(), is(false)); ArangoTransactionObject tx2 = getTransactionObject(inner1); - assertThat(tx2.getResource().getStreamTransactionId(), is("123")); + assertThat(tx2.getHolder().getStreamTransactionId(), is("123")); underTest.commit(inner2); underTest.commit(outer); verify(database).commitStreamTransaction("123"); assertThat(TransactionSynchronizationManager.getResource(DATABASE_NAME), nullValue()); } + @Test + public void getTransactionForPropagationSupportsWithoutExistingCreatesDummyTransaction() { + DefaultTransactionAttribute supports = new DefaultTransactionAttribute(); + supports.setPropagationBehavior(TransactionDefinition.PROPAGATION_SUPPORTS); + supports.setLabels(Collections.singleton("foo")); + TransactionStatus empty = underTest.getTransaction(supports); + assertThat(empty.isNewTransaction(), is(false)); + underTest.commit(empty); + verifyNoInteractions(database); + assertThat(TransactionSynchronizationManager.getResource(DATABASE_NAME), nullValue()); + } + + @Test + public void getTransactionForPropagationSupportsWithExistingCreatesInner() { + DefaultTransactionAttribute first = new DefaultTransactionAttribute(); + first.setLabels(Collections.singleton("foo")); + TransactionStatus outer = underTest.getTransaction(first); + DefaultTransactionAttribute supports = new DefaultTransactionAttribute(); + supports.setPropagationBehavior(TransactionDefinition.PROPAGATION_SUPPORTS); + supports.setLabels(Collections.singleton("bar")); + TransactionStatus inner = underTest.getTransaction(supports); + assertThat(inner.isNewTransaction(), is(false)); + underTest.commit(inner); + ArangoTransactionObject transactionObject = getTransactionObject(inner); + assertThat(TransactionSynchronizationManager.getResource(DATABASE_NAME), is(transactionObject.getHolder())); + verifyNoInteractions(database); + } + @Test public void getTransactionWithMultipleBridgeCallsWorksForKnownCollections() { DefaultTransactionAttribute definition = new DefaultTransactionAttribute(); @@ -179,8 +211,8 @@ public void getTransactionWithMultipleBridgeCallsIgnoresAdditionalCollections() TransactionStatus state = underTest.getTransaction(definition); beginTransaction("123", "foo"); beginPassed.getValue().apply(Collections.singletonList("baz")); - assertThat(getTransactionObject(state).getResource().getCollectionNames(), hasItems("foo", "bar")); - assertThat(getTransactionObject(state).getResource().getCollectionNames(), not(hasItem("baz"))); + assertThat(getTransactionObject(state).getHolder().getCollectionNames(), hasItems("foo", "bar")); + assertThat(getTransactionObject(state).getHolder().getCollectionNames(), not(hasItem("baz"))); } @Test(expected = InvalidIsolationLevelException.class) @@ -195,10 +227,8 @@ private void beginTransaction(String id, String... collectionNames) { .thenReturn(id); when(database.beginStreamTransaction(any())) .thenReturn(streamTransaction); - when(database.getStreamTransaction(any())) + lenient().when(database.getStreamTransaction(any())) .thenReturn(streamTransaction); - when(streamTransaction.getStatus()) - .thenReturn(StreamTransactionStatus.running); verify(bridge).setCurrentTransaction(beginPassed.capture()); beginPassed.getValue().apply(Arrays.asList(collectionNames)); } From 7268cb10fe747364dd9470d953ea7605ca53f7bb Mon Sep 17 00:00:00 2001 From: Arne Burmeister Date: Thu, 16 Mar 2023 16:38:50 +0100 Subject: [PATCH 33/55] #80 allow sync callbacks after completion --- .../transaction/ArangoTransactionManager.java | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManager.java b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManager.java index 862aaffc8..ca96b3cc4 100644 --- a/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManager.java +++ b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManager.java @@ -54,8 +54,8 @@ public class ArangoTransactionManager extends AbstractPlatformTransactionManager public ArangoTransactionManager(ArangoOperations operations, QueryTransactionBridge bridge) { this.operations = operations; this.bridge = bridge; - super.setGlobalRollbackOnParticipationFailure(true); - super.setTransactionSynchronization(SYNCHRONIZATION_ON_ACTUAL_TRANSACTION); + setGlobalRollbackOnParticipationFailure(true); + setTransactionSynchronization(SYNCHRONIZATION_ON_ACTUAL_TRANSACTION); } /** @@ -126,10 +126,21 @@ protected void doCommit(DefaultTransactionStatus status) throws TransactionExcep } try { tx.commit(); + afterCompletion(); } catch (ArangoDBException error) { + if (!isRollbackOnCommitFailure()) { + try { + tx.rollback(); + } catch (ArangoDBException noRollback) { + if (logger.isDebugEnabled()) { + logger.debug("Cannot rollback after commit " + tx, noRollback); + } + // expose commit exception instead + } finally { + afterCompletion(); + } + } throw new TransactionSystemException("Cannot commit transaction " + tx, error); - } finally { - bridge.clearCurrentTransaction(); } } @@ -151,7 +162,7 @@ protected void doRollback(DefaultTransactionStatus status) throws TransactionExc } catch (ArangoDBException error) { throw new TransactionSystemException("Cannot roll back transaction " + tx, error); } finally { - bridge.clearCurrentTransaction(); + afterCompletion(); } } @@ -203,13 +214,8 @@ protected void prepareSynchronization(DefaultTransactionStatus status, Transacti } } - /** - * Unbind the holder from the last transaction completed. - * - * @see ArangoTransactionHolder - */ - @Override - protected void doCleanupAfterCompletion(Object transaction) { + private void afterCompletion() { + bridge.clearCurrentTransaction(); TransactionSynchronizationManager.unbindResource(operations.getDatabaseName()); } } From e55a7b079488e6e2efc40a5e4f305ac83b8bf881 Mon Sep 17 00:00:00 2001 From: Arne Burmeister Date: Wed, 31 May 2023 09:30:50 +0200 Subject: [PATCH 34/55] #80 fix merge error --- .../springframework/repository/query/DerivedArangoQuery.java | 5 +++-- .../repository/query/StringBasedArangoQuery.java | 4 +++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/arangodb/springframework/repository/query/DerivedArangoQuery.java b/src/main/java/com/arangodb/springframework/repository/query/DerivedArangoQuery.java index 7baf3c0e8..f36dfb936 100644 --- a/src/main/java/com/arangodb/springframework/repository/query/DerivedArangoQuery.java +++ b/src/main/java/com/arangodb/springframework/repository/query/DerivedArangoQuery.java @@ -22,11 +22,11 @@ import com.arangodb.entity.IndexEntity; import com.arangodb.entity.IndexType; +import com.arangodb.model.AqlQueryOptions; import com.arangodb.springframework.core.ArangoOperations; import com.arangodb.springframework.repository.query.derived.BindParameterBinding; import com.arangodb.springframework.repository.query.derived.DerivedQueryCreator; import org.springframework.data.repository.query.parser.PartTree; -import org.springframework.data.util.Pair; import java.util.Collection; import java.util.LinkedList; @@ -55,7 +55,8 @@ public DerivedArangoQuery(final ArangoQueryMethod method, final ArangoOperations @Override protected QueryWithCollections createQuery( final ArangoParameterAccessor accessor, - final Map bindVars) { + final Map bindVars, + final AqlQueryOptions options) { return new DerivedQueryCreator(mappingContext, domainClass, tree, accessor, new BindParameterBinding(bindVars), geoFields).createQuery(); diff --git a/src/main/java/com/arangodb/springframework/repository/query/StringBasedArangoQuery.java b/src/main/java/com/arangodb/springframework/repository/query/StringBasedArangoQuery.java index 3a8440e69..9effe5e5b 100644 --- a/src/main/java/com/arangodb/springframework/repository/query/StringBasedArangoQuery.java +++ b/src/main/java/com/arangodb/springframework/repository/query/StringBasedArangoQuery.java @@ -20,6 +20,7 @@ package com.arangodb.springframework.repository.query; +import com.arangodb.model.AqlQueryOptions; import com.arangodb.springframework.annotation.Document; import com.arangodb.springframework.annotation.Edge; import com.arangodb.springframework.core.ArangoOperations; @@ -94,7 +95,8 @@ public StringBasedArangoQuery(final String query, final ArangoQueryMethod method @Override protected QueryWithCollections createQuery( final ArangoParameterAccessor accessor, - final Map bindVars) { + final Map bindVars, + final AqlQueryOptions options) { extractBindVars(accessor, bindVars); From ba7d206f56b2d997dc449237c52f50920d556eee Mon Sep 17 00:00:00 2001 From: Arne Burmeister Date: Wed, 23 Aug 2023 08:46:19 +0200 Subject: [PATCH 35/55] #80 replace dbname by database itself --- .../core/ArangoOperations.java | 9 +++++++- .../core/template/ArangoTemplate.java | 16 ++++++------- .../transaction/ArangoTransactionManager.java | 13 +++++------ .../core/template/ArangoTemplateTest.java | 2 +- .../ArangoTransactionManagerTest.java | 23 ++++++------------- 5 files changed, 30 insertions(+), 33 deletions(-) diff --git a/src/main/java/com/arangodb/springframework/core/ArangoOperations.java b/src/main/java/com/arangodb/springframework/core/ArangoOperations.java index 9398b9a38..82fc9b3a1 100644 --- a/src/main/java/com/arangodb/springframework/core/ArangoOperations.java +++ b/src/main/java/com/arangodb/springframework/core/ArangoOperations.java @@ -22,6 +22,7 @@ import com.arangodb.ArangoCursor; import com.arangodb.ArangoDB; +import com.arangodb.ArangoDatabase; import com.arangodb.entity.*; import com.arangodb.model.*; import com.arangodb.springframework.core.convert.ArangoConverter; @@ -54,7 +55,13 @@ public interface ArangoOperations { */ ArangoDBVersion getVersion() throws DataAccessException; - DbName getDatabaseName(); + /** + * Returns the underlying database. The database will be created if it does not exist. + * + * @return the database object + * @throws DataAccessException + */ + ArangoDatabase db() throws DataAccessException; /** * Performs a database query using the given {@code query} and {@code bindVars}, then returns a new diff --git a/src/main/java/com/arangodb/springframework/core/template/ArangoTemplate.java b/src/main/java/com/arangodb/springframework/core/template/ArangoTemplate.java index 798102032..2798f5c65 100644 --- a/src/main/java/com/arangodb/springframework/core/template/ArangoTemplate.java +++ b/src/main/java/com/arangodb/springframework/core/template/ArangoTemplate.java @@ -113,14 +113,20 @@ public ArangoTemplate(final ArangoDB arango, final String database, final Arango version = null; } - ArangoDatabase db() { - final String key = getDatabaseName().get(); + @Override + public ArangoDatabase db() throws DataAccessException { + final String key = databaseExpression != null ? databaseExpression.getValue(context, String.class) + : databaseName; return databaseCache.computeIfAbsent(key, name -> { final ArangoDatabase db = arango.db(name); + try { if (!db.exists()) { db.create(); } return db; + } catch (ArangoDBException error) { + throw DataAccessUtils.translateIfNecessary(error, exceptionTranslator); + } }); } @@ -259,12 +265,6 @@ public ArangoDBVersion getVersion() throws DataAccessException { } } - @Override - public DbName getDatabaseName() { - return DbName.of(databaseExpression != null ? databaseExpression.getValue(context, String.class) - : databaseName); - } - @Override public ArangoCursor query(final String query, final Map bindVars, final AqlQueryOptions options, final Class entityClass) throws DataAccessException { diff --git a/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManager.java b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManager.java index ca96b3cc4..7d4a45ec1 100644 --- a/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManager.java +++ b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManager.java @@ -36,7 +36,7 @@ /** * Transaction manager using ArangoDB stream transactions on the - * {@linkplain ArangoOperations#getDatabaseName() current database} of the + * {@linkplain ArangoOperations#db() current database} of the * template. A {@linkplain ArangoTransactionObject transaction object} using * a shared {@linkplain ArangoTransactionHolder holder} is used for the * {@link DefaultTransactionStatus}. Neither @@ -79,10 +79,9 @@ public void afterPropertiesSet() { */ @Override protected ArangoTransactionObject doGetTransaction() { - DbName database = operations.getDatabaseName(); - ArangoTransactionHolder holder = (ArangoTransactionHolder) TransactionSynchronizationManager.getResource(database); + ArangoTransactionHolder holder = (ArangoTransactionHolder) TransactionSynchronizationManager.getResource(this); try { - return new ArangoTransactionObject(operations.driver().db(database), getDefaultTimeout(), holder); + return new ArangoTransactionObject(operations.db(), getDefaultTimeout(), holder); } catch (ArangoDBException error) { throw new TransactionSystemException("Cannot create transaction object", error); } @@ -173,7 +172,7 @@ protected void doRollback(DefaultTransactionStatus status) throws TransactionExc @Override protected boolean isExistingTransaction(Object transaction) throws TransactionException { ArangoTransactionHolder holder = ((ArangoTransactionObject) transaction).getHolder(); - return holder == TransactionSynchronizationManager.getResource(operations.getDatabaseName()); + return holder == TransactionSynchronizationManager.getResource(this); } /** @@ -210,12 +209,12 @@ protected void prepareSynchronization(DefaultTransactionStatus status, Transacti super.prepareSynchronization(status, definition); if (status.isNewSynchronization()) { ArangoTransactionHolder holder = ((ArangoTransactionObject) status.getTransaction()).getHolder(); - TransactionSynchronizationManager.bindResource(operations.getDatabaseName(), holder); + TransactionSynchronizationManager.bindResource(this, holder); } } private void afterCompletion() { bridge.clearCurrentTransaction(); - TransactionSynchronizationManager.unbindResource(operations.getDatabaseName()); + TransactionSynchronizationManager.unbindResource(this); } } diff --git a/src/test/java/com/arangodb/springframework/core/template/ArangoTemplateTest.java b/src/test/java/com/arangodb/springframework/core/template/ArangoTemplateTest.java index 5544a2c76..ae9c49bb9 100644 --- a/src/test/java/com/arangodb/springframework/core/template/ArangoTemplateTest.java +++ b/src/test/java/com/arangodb/springframework/core/template/ArangoTemplateTest.java @@ -62,7 +62,7 @@ public void template() { assertThat(version.getLicense(), is(notNullValue())); assertThat(version.getServer(), is(notNullValue())); assertThat(version.getVersion(), is(notNullValue())); - assertThat(template.getDatabaseName(), is(notNullValue())); + assertThat(template.db(), is(notNullValue())); } @Test diff --git a/src/test/java/com/arangodb/springframework/transaction/ArangoTransactionManagerTest.java b/src/test/java/com/arangodb/springframework/transaction/ArangoTransactionManagerTest.java index d3b044def..90b286773 100644 --- a/src/test/java/com/arangodb/springframework/transaction/ArangoTransactionManagerTest.java +++ b/src/test/java/com/arangodb/springframework/transaction/ArangoTransactionManagerTest.java @@ -43,8 +43,6 @@ @RunWith(MockitoJUnitRunner.class) public class ArangoTransactionManagerTest { - private static final DbName DATABASE_NAME = DbName.of("test"); - @Mock private ArangoOperations operations; @Mock @@ -52,8 +50,6 @@ public class ArangoTransactionManagerTest { @InjectMocks private ArangoTransactionManager underTest; @Mock - private ArangoDB driver; - @Mock private ArangoDatabase database; @Mock private StreamTransactionEntity streamTransaction; @@ -64,17 +60,13 @@ public class ArangoTransactionManagerTest { @Before public void setupMocks() { - when(operations.getDatabaseName()) - .thenReturn(DATABASE_NAME); - when(operations.driver()) - .thenReturn(driver); - when(driver.db(any(DbName.class))) + when(operations.db()) .thenReturn(database); } @After public void cleanupSync() { - TransactionSynchronizationManager.unbindResourceIfPossible(DATABASE_NAME); + TransactionSynchronizationManager.unbindResourceIfPossible(underTest); TransactionSynchronizationManager.clear(); } @@ -82,9 +74,8 @@ public void cleanupSync() { public void getTransactionReturnsNewTransactionWithoutStreamTransaction() { TransactionStatus status = underTest.getTransaction(new DefaultTransactionAttribute()); assertThat(status.isNewTransaction(), is(true)); - verify(driver).db(DATABASE_NAME); verify(bridge).setCurrentTransaction(any()); - ArangoTransactionHolder resource = (ArangoTransactionHolder) TransactionSynchronizationManager.getResource(DATABASE_NAME); + ArangoTransactionHolder resource = (ArangoTransactionHolder) TransactionSynchronizationManager.getResource(underTest); assertThat(resource.getStreamTransactionId(), nullValue()); assertThat(resource.getCollectionNames(), empty()); assertThat(resource.isRollbackOnly(), is(false)); @@ -113,7 +104,7 @@ public void innerRollbackCausesUnexpectedRollbackOnOuterCommit() { try { underTest.commit(outer); } finally { - assertThat(TransactionSynchronizationManager.getResource(DATABASE_NAME), nullValue()); + assertThat(TransactionSynchronizationManager.getResource(underTest), nullValue()); } } @@ -159,7 +150,7 @@ public void nestedGetTransactionReturnsExistingTransactionWithFormerCollections( underTest.commit(inner2); underTest.commit(outer); verify(database).commitStreamTransaction("123"); - assertThat(TransactionSynchronizationManager.getResource(DATABASE_NAME), nullValue()); + assertThat(TransactionSynchronizationManager.getResource(underTest), nullValue()); } @Test @@ -171,7 +162,7 @@ public void getTransactionForPropagationSupportsWithoutExistingCreatesDummyTrans assertThat(empty.isNewTransaction(), is(false)); underTest.commit(empty); verifyNoInteractions(database); - assertThat(TransactionSynchronizationManager.getResource(DATABASE_NAME), nullValue()); + assertThat(TransactionSynchronizationManager.getResource(underTest), nullValue()); } @Test @@ -186,7 +177,7 @@ public void getTransactionForPropagationSupportsWithExistingCreatesInner() { assertThat(inner.isNewTransaction(), is(false)); underTest.commit(inner); ArangoTransactionObject transactionObject = getTransactionObject(inner); - assertThat(TransactionSynchronizationManager.getResource(DATABASE_NAME), is(transactionObject.getHolder())); + assertThat(TransactionSynchronizationManager.getResource(underTest), is(transactionObject.getHolder())); verifyNoInteractions(database); } From 170acaae3cc2d0cd79fbd1f02b7e030212b911f3 Mon Sep 17 00:00:00 2001 From: Arne Burmeister Date: Wed, 23 Aug 2023 09:17:14 +0200 Subject: [PATCH 36/55] #80 avoid collection creation or index creation inside transaction --- .../core/template/ArangoTemplate.java | 57 ++++++++++--------- 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/src/main/java/com/arangodb/springframework/core/template/ArangoTemplate.java b/src/main/java/com/arangodb/springframework/core/template/ArangoTemplate.java index 2798f5c65..a561c62a2 100644 --- a/src/main/java/com/arangodb/springframework/core/template/ArangoTemplate.java +++ b/src/main/java/com/arangodb/springframework/core/template/ArangoTemplate.java @@ -46,6 +46,7 @@ import org.springframework.context.expression.BeanFactoryAccessor; import org.springframework.context.expression.BeanFactoryResolver; import org.springframework.dao.DataAccessException; +import org.springframework.dao.InvalidDataAccessResourceUsageException; import org.springframework.dao.support.DataAccessUtils; import org.springframework.dao.support.PersistenceExceptionTranslator; import org.springframework.data.mapping.PersistentPropertyAccessor; @@ -130,23 +131,23 @@ public ArangoDatabase db() throws DataAccessException { }); } - private ArangoCollection _collection(final String name) { - return _collection(name, null, null); + private ArangoCollection _collection(final String name, boolean transactional) { + return _collection(name, null, null, transactional); } - private ArangoCollection _collection(final Class entityClass) { - return _collection(entityClass, null); + private ArangoCollection _collection(final Class entityClass, boolean transactional) { + return _collection(entityClass, null, transactional); } - private ArangoCollection _collection(final Class entityClass, final Object id) { + private ArangoCollection _collection(final Class entityClass, final Object id, boolean transactional) { final ArangoPersistentEntity persistentEntity = converter.getMappingContext() .getRequiredPersistentEntity(entityClass); final String name = determineCollectionFromId(id).orElse(persistentEntity.getCollection()); - return _collection(name, persistentEntity, persistentEntity.getCollectionOptions()); + return _collection(name, persistentEntity, persistentEntity.getCollectionOptions(), transactional); } private ArangoCollection _collection(final String name, final ArangoPersistentEntity persistentEntity, - final CollectionCreateOptions options) { + final CollectionCreateOptions options, boolean transactional) { final ArangoDatabase db = db(); final Class entityClass = persistentEntity != null ? persistentEntity.getType() : null; @@ -154,7 +155,9 @@ private ArangoCollection _collection(final String name, final ArangoPersistentEn key -> { final ArangoCollection collection = db.collection(name); if (!collection.exists()) { - + if (transactional) { + throw new InvalidDataAccessResourceUsageException("Missing collection cannot be created during transaction"); + } collection.create(options); } return new CollectionCacheValue(collection); @@ -163,8 +166,10 @@ private ArangoCollection _collection(final String name, final ArangoPersistentEn final ArangoCollection collection = value.getCollection(); if (persistentEntity != null && !entities.contains(entityClass)) { value.addEntityClass(entityClass); + if (!transactional) { ensureCollectionIndexes(collection(collection), persistentEntity); } + } return collection; } @@ -269,18 +274,18 @@ public ArangoDBVersion getVersion() throws DataAccessException { public ArangoCursor query(final String query, final Map bindVars, final AqlQueryOptions options, final Class entityClass) throws DataAccessException { try { - ArangoCursor cursor = db().query(query, entityClass, bindVars == null ? null : prepareBindVars(bindVars), options); + ArangoCursor cursor = db().query(query, entityClass, bindVars == null ? null : prepareBindVars(bindVars, false), options); return new ArangoExtCursor<>(cursor, entityClass, eventPublisher); } catch (final ArangoDBException e) { throw translateException(e); } } - private Map prepareBindVars(final Map bindVars) { + private Map prepareBindVars(final Map bindVars, boolean transactional) { final Map prepared = new HashMap<>(bindVars.size()); for (final Entry entry : bindVars.entrySet()) { if (entry.getKey().startsWith("@") && entry.getValue() instanceof Class clazz) { - prepared.put(entry.getKey(), _collection(clazz).name()); + prepared.put(entry.getKey(), _collection(clazz, transactional).name()); } else { prepared.put(entry.getKey(), entry.getValue()); } @@ -299,7 +304,7 @@ public MultiDocumentEntity> deleteAll( MultiDocumentEntity> result; try { - result = _collection(entityClass).deleteDocuments(toList(values), options, entityClass); + result = _collection(entityClass, options.getStreamTransactionId() != null).deleteDocuments(toList(values), options, entityClass); } catch (final ArangoDBException e) { throw translateException(e); } @@ -331,7 +336,7 @@ public DocumentDeleteEntity delete(final Object id, final DocumentDeleteO DocumentDeleteEntity result; try { - result = _collection(entityClass, id).deleteDocument(determineDocumentKeyFromId(id), options, entityClass); + result = _collection(entityClass, id, options.getStreamTransactionId() != null).deleteDocument(determineDocumentKeyFromId(id), options, entityClass); } catch (final ArangoDBException e) { throw translateException(e); } @@ -351,7 +356,7 @@ public MultiDocumentEntity> updateAll( MultiDocumentEntity> result; try { - result = _collection(entityClass).updateDocuments(toList(values), options, entityClass); + result = _collection(entityClass, options.getStreamTransactionId() != null).updateDocuments(toList(values), options, entityClass); } catch (final ArangoDBException e) { throw translateException(e); } @@ -369,7 +374,7 @@ public DocumentUpdateEntity update(final Object id, final T value, final DocumentUpdateEntity result; try { - result = _collection(value.getClass(), id).updateDocument(determineDocumentKeyFromId(id), value, options); + result = _collection(value.getClass(), id, options.getStreamTransactionId() != null).updateDocument(determineDocumentKeyFromId(id), value, options); } catch (final ArangoDBException e) { throw translateException(e); } @@ -390,7 +395,7 @@ public MultiDocumentEntity> replaceAll( MultiDocumentEntity> result; try { - result = _collection(entityClass).replaceDocuments(toList(values), options, entityClass); + result = _collection(entityClass, options.getStreamTransactionId() != null).replaceDocuments(toList(values), options, entityClass); } catch (final ArangoDBException e) { throw translateException(e); } @@ -407,7 +412,7 @@ public DocumentUpdateEntity replace(final Object id, final T value, final DocumentUpdateEntity result; try { - result = _collection(value.getClass(), id).replaceDocument(determineDocumentKeyFromId(id), value, options); + result = _collection(value.getClass(), id, false).replaceDocument(determineDocumentKeyFromId(id), value, options); } catch (final ArangoDBException e) { throw translateException(e); } @@ -421,7 +426,7 @@ public DocumentUpdateEntity replace(final Object id, final T value, final public Optional find(final Object id, final Class entityClass, final DocumentReadOptions options) throws DataAccessException { try { - T res = _collection(entityClass, id).getDocument(determineDocumentKeyFromId(id), entityClass, options); + T res = _collection(entityClass, id, options.getStreamTransactionId() != null).getDocument(determineDocumentKeyFromId(id), entityClass, options); if (res != null) { potentiallyEmitEvent(new AfterLoadEvent<>(res)); } @@ -444,7 +449,7 @@ public Iterable findAll(final Iterable ids, final Class entityClass try { final Collection keys = new ArrayList<>(); ids.forEach(id -> keys.add(determineDocumentKeyFromId(id))); - Collection docs = _collection(entityClass).getDocuments(keys, entityClass).getDocuments(); + Collection docs = _collection(entityClass, options.getStreamTransactionId() != null).getDocuments(keys, entityClass).getDocuments(); for (T doc : docs) { if (doc != null) { potentiallyEmitEvent(new AfterLoadEvent<>(doc)); @@ -464,7 +469,7 @@ public MultiDocumentEntity> insertAll( MultiDocumentEntity> result; try { - result = _collection(entityClass).insertDocuments(toList(values), options, entityClass); + result = _collection(entityClass, options.getStreamTransactionId() != null).insertDocuments(toList(values), options, entityClass); } catch (final ArangoDBException e) { throw translateException(e); } @@ -480,7 +485,7 @@ public DocumentCreateEntity insert(final T value, final DocumentCreateOpt DocumentCreateEntity result; try { - result = _collection(value.getClass()).insertDocument(value, options); + result = _collection(value.getClass(), options.getStreamTransactionId() != null).insertDocument(value, options); } catch (final ArangoDBException e) { throw translateException(e); } @@ -493,7 +498,7 @@ public DocumentCreateEntity insert(final T value, final DocumentCreateOpt @Override public T repsert(final T value, AqlQueryOptions options) throws DataAccessException { @SuppressWarnings("unchecked") final Class clazz = (Class) value.getClass(); - final String collectionName = _collection(clazz).name(); + final String collectionName = _collection(clazz, options.getStreamTransactionId() != null).name(); potentiallyEmitEvent(new BeforeSaveEvent<>(value)); @@ -525,7 +530,7 @@ public Iterable repsertAll(final Iterable values, final Class bindVars = new HashMap<>(); @@ -626,7 +631,7 @@ private void updateDBFields(final Object value, final DocumentEntity documentEnt @Override public boolean exists(final Object id, final Class entityClass, DocumentExistsOptions options) throws DataAccessException { try { - return _collection(entityClass).documentExists(determineDocumentKeyFromId(id), options); + return _collection(entityClass, options.getStreamTransactionId() != null).documentExists(determineDocumentKeyFromId(id), options); } catch (final ArangoDBException e) { throw translateException(e); } @@ -647,13 +652,13 @@ public void dropDatabase() throws DataAccessException { @Override public CollectionOperations collection(final Class entityClass) throws DataAccessException { - return collection(_collection(entityClass)); + return collection(_collection(entityClass, false)); } @Override public CollectionOperations collection(final String name, final CollectionCreateOptions options) throws DataAccessException { - return collection(_collection(name, null, options)); + return collection(_collection(name, null, options, false)); } private CollectionOperations collection(final ArangoCollection collection) { From 2f4229cd4f8875ba51c27be5503fec193e4b1bec Mon Sep 17 00:00:00 2001 From: Arne Burmeister Date: Wed, 23 Aug 2023 11:55:37 +0200 Subject: [PATCH 37/55] #80 extract collection callback, improve implementation --- .../core/template/ArangoTemplate.java | 11 +++- .../core/template/CollectionCallback.java | 61 +++++++++++++++++++ .../core/template/DefaultUserOperation.java | 7 --- 3 files changed, 69 insertions(+), 10 deletions(-) create mode 100644 src/main/java/com/arangodb/springframework/core/template/CollectionCallback.java diff --git a/src/main/java/com/arangodb/springframework/core/template/ArangoTemplate.java b/src/main/java/com/arangodb/springframework/core/template/ArangoTemplate.java index a561c62a2..42a9c0d4d 100644 --- a/src/main/java/com/arangodb/springframework/core/template/ArangoTemplate.java +++ b/src/main/java/com/arangodb/springframework/core/template/ArangoTemplate.java @@ -39,6 +39,8 @@ import com.arangodb.springframework.core.util.ArangoExceptionTranslator; import com.arangodb.springframework.core.util.MetadataUtils; import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; @@ -77,6 +79,7 @@ public class ArangoTemplate implements ArangoOperations, CollectionCallback, App private static final String REPSERT_QUERY = "LET doc = @doc " + REPSERT_QUERY_BODY; private static final String REPSERT_MANY_QUERY = "FOR doc IN @docs " + REPSERT_QUERY_BODY; + private static final Logger LOGGER = LoggerFactory.getLogger(ArangoTemplate.class); private static final SpelExpressionParser PARSER = new SpelExpressionParser(); private volatile ArangoDBVersion version; @@ -166,9 +169,11 @@ private ArangoCollection _collection(final String name, final ArangoPersistentEn final ArangoCollection collection = value.getCollection(); if (persistentEntity != null && !entities.contains(entityClass)) { value.addEntityClass(entityClass); - if (!transactional) { - ensureCollectionIndexes(collection(collection), persistentEntity); - } + if (transactional) { + LOGGER.debug("Not ensuring any indexes of collection {} for {} during transaction", collection.name(), entityClass); + } else { + ensureCollectionIndexes(collection(collection), persistentEntity); + } } return collection; } diff --git a/src/main/java/com/arangodb/springframework/core/template/CollectionCallback.java b/src/main/java/com/arangodb/springframework/core/template/CollectionCallback.java new file mode 100644 index 000000000..34c9bbc88 --- /dev/null +++ b/src/main/java/com/arangodb/springframework/core/template/CollectionCallback.java @@ -0,0 +1,61 @@ +/* + * DISCLAIMER + * + * Copyright 2017 ArangoDB GmbH, Cologne, Germany + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Copyright holder is ArangoDB GmbH, Cologne, Germany + */ + +package com.arangodb.springframework.core.template; + +import com.arangodb.springframework.core.ArangoOperations; +import com.arangodb.springframework.core.CollectionOperations; +import org.springframework.dao.DataAccessException; + +/** + * Internal interface to handle collection operations. + * Typically implemented by same class as {@link com.arangodb.springframework.core.ArangoOperations}. + */ +public interface CollectionCallback { + + /** + * @see com.arangodb.springframework.core.ArangoOperations#collection(Class) + */ + CollectionOperations collection(Class type) throws DataAccessException; + + /** + * @see com.arangodb.springframework.core.ArangoOperations#collection(String) + */ + CollectionOperations collection(String name) throws DataAccessException; + + + static CollectionCallback fromOperations(ArangoOperations operations) { + if (operations instanceof CollectionCallback) { + return (CollectionCallback) operations; + } + return new CollectionCallback() { + @Override + public CollectionOperations collection(Class type) { + return operations.collection(type); + } + + @Override + public CollectionOperations collection(String name) { + return operations.collection(name); + } + }; + } + +} diff --git a/src/main/java/com/arangodb/springframework/core/template/DefaultUserOperation.java b/src/main/java/com/arangodb/springframework/core/template/DefaultUserOperation.java index 0b88c6583..e7e750505 100644 --- a/src/main/java/com/arangodb/springframework/core/template/DefaultUserOperation.java +++ b/src/main/java/com/arangodb/springframework/core/template/DefaultUserOperation.java @@ -30,7 +30,6 @@ import com.arangodb.entity.UserEntity; import com.arangodb.model.UserCreateOptions; import com.arangodb.model.UserUpdateOptions; -import com.arangodb.springframework.core.CollectionOperations; import com.arangodb.springframework.core.UserOperations; /** @@ -39,12 +38,6 @@ */ public class DefaultUserOperation implements UserOperations { - public interface CollectionCallback { - CollectionOperations collection(Class type); - - CollectionOperations collection(String name); - } - private final ArangoDatabase db; private final String username; private final PersistenceExceptionTranslator exceptionTranslator; From b7118cc7775fc087faccf2ddcb26b7a6030e7a3e Mon Sep 17 00:00:00 2001 From: Arne Burmeister Date: Wed, 23 Aug 2023 11:55:49 +0200 Subject: [PATCH 38/55] #80 ensure collections before transaction start --- .../transaction/ArangoTransactionManager.java | 8 +-- .../transaction/ArangoTransactionObject.java | 6 +- .../springframework/testdata/Actor.java | 2 + ...rangoTransactionManagerRepositoryTest.java | 9 ++- .../ArangoTransactionManagerTest.java | 55 +++++++------------ src/test/resources/logback-test.xml | 2 +- 6 files changed, 40 insertions(+), 42 deletions(-) diff --git a/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManager.java b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManager.java index 7d4a45ec1..3ebf667c8 100644 --- a/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManager.java +++ b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManager.java @@ -22,8 +22,8 @@ import com.arangodb.ArangoDBException; import com.arangodb.ArangoDatabase; -import com.arangodb.DbName; import com.arangodb.springframework.core.ArangoOperations; +import com.arangodb.springframework.core.template.CollectionCallback; import com.arangodb.springframework.repository.query.QueryTransactionBridge; import org.springframework.beans.factory.InitializingBean; import org.springframework.lang.Nullable; @@ -67,10 +67,10 @@ public void afterPropertiesSet() { throw new IllegalStateException("Nested transactions must not be allowed"); } if (!isGlobalRollbackOnParticipationFailure()) { - throw new IllegalStateException("Global rollback on participating failure is needed"); + throw new IllegalStateException("Global rollback on participating failure is required"); } if (getTransactionSynchronization() == SYNCHRONIZATION_NEVER) { - throw new IllegalStateException("Transaction synchronization is needed always"); + throw new IllegalStateException("Transaction synchronization must not be disabled"); } } @@ -81,7 +81,7 @@ public void afterPropertiesSet() { protected ArangoTransactionObject doGetTransaction() { ArangoTransactionHolder holder = (ArangoTransactionHolder) TransactionSynchronizationManager.getResource(this); try { - return new ArangoTransactionObject(operations.db(), getDefaultTimeout(), holder); + return new ArangoTransactionObject(operations.db(), CollectionCallback.fromOperations(operations), getDefaultTimeout(), holder); } catch (ArangoDBException error) { throw new TransactionSystemException("Cannot create transaction object", error); } diff --git a/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionObject.java b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionObject.java index 9566b7690..077be4936 100644 --- a/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionObject.java +++ b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionObject.java @@ -24,6 +24,7 @@ import com.arangodb.ArangoDatabase; import com.arangodb.entity.StreamTransactionStatus; import com.arangodb.model.StreamTransactionOptions; +import com.arangodb.springframework.core.template.CollectionCallback; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.lang.Nullable; @@ -46,11 +47,13 @@ class ArangoTransactionObject implements SmartTransactionObject { private static final Log logger = LogFactory.getLog(ArangoTransactionObject.class); private final ArangoDatabase database; + private final CollectionCallback collectionCallback; private final ArangoTransactionHolder holder; private int timeout; - ArangoTransactionObject(ArangoDatabase database, int defaultTimeout, @Nullable ArangoTransactionHolder holder) { + ArangoTransactionObject(ArangoDatabase database, CollectionCallback collectionCallback, int defaultTimeout, @Nullable ArangoTransactionHolder holder) { this.database = database; + this.collectionCallback = collectionCallback; this.holder = holder == null ? new ArangoTransactionHolder() : holder; this.timeout = defaultTimeout; } @@ -71,6 +74,7 @@ void configure(TransactionDefinition definition) { ArangoTransactionHolder getOrBegin(Collection collections) throws ArangoDBException { addCollections(collections); if (holder.getStreamTransactionId() == null) { + holder.getCollectionNames().forEach(collectionCallback::collection); StreamTransactionOptions options = new StreamTransactionOptions() .allowImplicit(true) .writeCollections(holder.getCollectionNames().toArray(new String[0])) diff --git a/src/test/java/com/arangodb/springframework/testdata/Actor.java b/src/test/java/com/arangodb/springframework/testdata/Actor.java index 3d4e1f1d9..4dc5f667b 100644 --- a/src/test/java/com/arangodb/springframework/testdata/Actor.java +++ b/src/test/java/com/arangodb/springframework/testdata/Actor.java @@ -22,6 +22,7 @@ import java.util.List; +import com.arangodb.springframework.annotation.PersistentIndex; import org.springframework.data.annotation.Id; import com.arangodb.springframework.annotation.Document; @@ -33,6 +34,7 @@ * @author Christian Lechner */ @Document("actors") +@PersistentIndex(fields = "name") public class Actor { @Id diff --git a/src/test/java/com/arangodb/springframework/transaction/ArangoTransactionManagerRepositoryTest.java b/src/test/java/com/arangodb/springframework/transaction/ArangoTransactionManagerRepositoryTest.java index 5c0d9b90f..8117b9743 100644 --- a/src/test/java/com/arangodb/springframework/transaction/ArangoTransactionManagerRepositoryTest.java +++ b/src/test/java/com/arangodb/springframework/transaction/ArangoTransactionManagerRepositoryTest.java @@ -4,7 +4,6 @@ import com.arangodb.springframework.ArangoTransactionalTestConfiguration; import com.arangodb.springframework.repository.ActorRepository; import com.arangodb.springframework.repository.MovieRepository; -import com.arangodb.springframework.testdata.Actor; import com.arangodb.springframework.testdata.Movie; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -18,7 +17,7 @@ public class ArangoTransactionManagerRepositoryTest extends AbstractArangoTest { public ArangoTransactionManagerRepositoryTest() { - super(Movie.class, Actor.class); + super(Movie.class); } @Autowired @@ -66,4 +65,10 @@ public void shouldRollbackWithinTransaction() { assertThat(movieRepository.findById(starWars.getId())).isNotPresent(); } + + @Test + @Transactional(label = "actors") + public void shouldCreateCollectionsBeforeTransaction() { + actorRepository.findAll(); + } } diff --git a/src/test/java/com/arangodb/springframework/transaction/ArangoTransactionManagerTest.java b/src/test/java/com/arangodb/springframework/transaction/ArangoTransactionManagerTest.java index 90b286773..dcd587702 100644 --- a/src/test/java/com/arangodb/springframework/transaction/ArangoTransactionManagerTest.java +++ b/src/test/java/com/arangodb/springframework/transaction/ArangoTransactionManagerTest.java @@ -1,8 +1,6 @@ package com.arangodb.springframework.transaction; -import com.arangodb.ArangoDB; import com.arangodb.ArangoDatabase; -import com.arangodb.DbName; import com.arangodb.entity.StreamTransactionEntity; import com.arangodb.entity.StreamTransactionStatus; import com.arangodb.model.StreamTransactionOptions; @@ -72,7 +70,7 @@ public void cleanupSync() { @Test public void getTransactionReturnsNewTransactionWithoutStreamTransaction() { - TransactionStatus status = underTest.getTransaction(new DefaultTransactionAttribute()); + TransactionStatus status = underTest.getTransaction(createTransactionAttribute("test")); assertThat(status.isNewTransaction(), is(true)); verify(bridge).setCurrentTransaction(any()); ArangoTransactionHolder resource = (ArangoTransactionHolder) TransactionSynchronizationManager.getResource(underTest); @@ -84,12 +82,8 @@ public void getTransactionReturnsNewTransactionWithoutStreamTransaction() { @Test public void innerGetTransactionIsNotNewTransactionIncludingFormerCollections() { - DefaultTransactionAttribute first = new DefaultTransactionAttribute(); - first.setLabels(Collections.singleton("foo")); - TransactionStatus outer = underTest.getTransaction(first); - DefaultTransactionAttribute second = new DefaultTransactionAttribute(); - second.setLabels(Collections.singleton("bar")); - TransactionStatus inner = underTest.getTransaction(second); + TransactionStatus outer = underTest.getTransaction(createTransactionAttribute("outer", "foo")); + TransactionStatus inner = underTest.getTransaction(createTransactionAttribute("inner", "bar")); assertThat(inner.isNewTransaction(), is(false)); ArangoTransactionObject transactionObject = getTransactionObject(inner); assertThat(transactionObject.getHolder().getCollectionNames(), hasItems("foo", "bar")); @@ -98,8 +92,8 @@ public void innerGetTransactionIsNotNewTransactionIncludingFormerCollections() { @Test(expected = UnexpectedRollbackException.class) public void innerRollbackCausesUnexpectedRollbackOnOuterCommit() { - TransactionStatus outer = underTest.getTransaction(new DefaultTransactionAttribute()); - TransactionStatus inner = underTest.getTransaction(new DefaultTransactionAttribute()); + TransactionStatus outer = underTest.getTransaction(createTransactionAttribute("outer")); + TransactionStatus inner = underTest.getTransaction(createTransactionAttribute("inner")); underTest.rollback(inner); try { underTest.commit(outer); @@ -110,8 +104,7 @@ public void innerRollbackCausesUnexpectedRollbackOnOuterCommit() { @Test public void getTransactionReturnsTransactionCreatesStreamTransactionWithAllCollectionsOnBridgeBeginCall() { - DefaultTransactionAttribute definition = new DefaultTransactionAttribute(); - definition.setLabels(Collections.singleton("baz")); + DefaultTransactionAttribute definition = createTransactionAttribute("timeout", "baz"); definition.setTimeout(20); TransactionStatus status = underTest.getTransaction(definition); beginTransaction("123", "foo", "bar"); @@ -127,17 +120,14 @@ public void getTransactionReturnsTransactionCreatesStreamTransactionWithAllColle @Test public void nestedGetTransactionReturnsExistingTransactionWithFormerCollections() { - DefaultTransactionAttribute first = new DefaultTransactionAttribute(); - first.setLabels(Collections.singleton("foo")); - TransactionStatus outer = underTest.getTransaction(first); + TransactionStatus outer = underTest.getTransaction(createTransactionAttribute("outer", "foo")); assertThat(outer.isNewTransaction(), is(true)); beginTransaction("123", "foo", "bar"); when(streamTransaction.getStatus()) .thenReturn(StreamTransactionStatus.running); - DefaultTransactionAttribute second = new DefaultTransactionAttribute(); - second.setLabels(Collections.singleton("bar")); + DefaultTransactionAttribute second = createTransactionAttribute("inner", "bar"); TransactionStatus inner1 = underTest.getTransaction(second); assertThat(inner1.isNewTransaction(), is(false)); ArangoTransactionObject tx1 = getTransactionObject(inner1); @@ -155,9 +145,8 @@ public void nestedGetTransactionReturnsExistingTransactionWithFormerCollections( @Test public void getTransactionForPropagationSupportsWithoutExistingCreatesDummyTransaction() { - DefaultTransactionAttribute supports = new DefaultTransactionAttribute(); + DefaultTransactionAttribute supports = createTransactionAttribute("test", "foo"); supports.setPropagationBehavior(TransactionDefinition.PROPAGATION_SUPPORTS); - supports.setLabels(Collections.singleton("foo")); TransactionStatus empty = underTest.getTransaction(supports); assertThat(empty.isNewTransaction(), is(false)); underTest.commit(empty); @@ -167,12 +156,9 @@ public void getTransactionForPropagationSupportsWithoutExistingCreatesDummyTrans @Test public void getTransactionForPropagationSupportsWithExistingCreatesInner() { - DefaultTransactionAttribute first = new DefaultTransactionAttribute(); - first.setLabels(Collections.singleton("foo")); - TransactionStatus outer = underTest.getTransaction(first); - DefaultTransactionAttribute supports = new DefaultTransactionAttribute(); + TransactionStatus outer = underTest.getTransaction(createTransactionAttribute("outer", "foo")); + DefaultTransactionAttribute supports = createTransactionAttribute("supports", "bar"); supports.setPropagationBehavior(TransactionDefinition.PROPAGATION_SUPPORTS); - supports.setLabels(Collections.singleton("bar")); TransactionStatus inner = underTest.getTransaction(supports); assertThat(inner.isNewTransaction(), is(false)); underTest.commit(inner); @@ -183,10 +169,7 @@ public void getTransactionForPropagationSupportsWithExistingCreatesInner() { @Test public void getTransactionWithMultipleBridgeCallsWorksForKnownCollections() { - DefaultTransactionAttribute definition = new DefaultTransactionAttribute(); - definition.setLabels(Collections.singleton("baz")); - definition.setTimeout(20); - underTest.getTransaction(definition); + underTest.getTransaction(createTransactionAttribute("test", "baz")); beginTransaction("123", "foo"); beginPassed.getValue().apply(Arrays.asList("foo", "baz")); verify(database).beginStreamTransaction(optionsPassed.capture()); @@ -196,10 +179,7 @@ public void getTransactionWithMultipleBridgeCallsWorksForKnownCollections() { @Test public void getTransactionWithMultipleBridgeCallsIgnoresAdditionalCollections() { - DefaultTransactionAttribute definition = new DefaultTransactionAttribute(); - definition.setLabels(Collections.singleton("bar")); - definition.setTimeout(20); - TransactionStatus state = underTest.getTransaction(definition); + TransactionStatus state = underTest.getTransaction(createTransactionAttribute("test", "bar")); beginTransaction("123", "foo"); beginPassed.getValue().apply(Collections.singletonList("baz")); assertThat(getTransactionObject(state).getHolder().getCollectionNames(), hasItems("foo", "bar")); @@ -208,7 +188,7 @@ public void getTransactionWithMultipleBridgeCallsIgnoresAdditionalCollections() @Test(expected = InvalidIsolationLevelException.class) public void getTransactionThrowsInvalidIsolationLevelExceptionForIsolationSerializable() { - DefaultTransactionAttribute definition = new DefaultTransactionAttribute(); + DefaultTransactionAttribute definition = createTransactionAttribute("serializable"); definition.setIsolationLevel(TransactionDefinition.ISOLATION_SERIALIZABLE); underTest.getTransaction(definition); } @@ -235,4 +215,11 @@ private ArangoTransactionObject getTransactionObject(TransactionStatus status) { private TransactionCollectionOptions getCollections(StreamTransactionOptions options) { return (TransactionCollectionOptions) forDirectFieldAccess(options).getPropertyValue("collections"); } + + private static DefaultTransactionAttribute createTransactionAttribute(String name, String... collections) { + DefaultTransactionAttribute transactionAttribute = new DefaultTransactionAttribute(); + transactionAttribute.setName(name); + transactionAttribute.setLabels(Arrays.asList(collections)); + return transactionAttribute; + } } diff --git a/src/test/resources/logback-test.xml b/src/test/resources/logback-test.xml index c4237c3f5..fd30e27b5 100644 --- a/src/test/resources/logback-test.xml +++ b/src/test/resources/logback-test.xml @@ -8,7 +8,7 @@ - + From 7410798816afda48b7dd03708451e1a8daae76da Mon Sep 17 00:00:00 2001 From: Arne Burmeister Date: Wed, 23 Aug 2023 15:25:06 +0200 Subject: [PATCH 39/55] #80 reuse collection name from persistent entity, escape collection names --- .../query/StringBasedArangoQuery.java | 20 ++++++------------- .../transaction/ArangoTransactionObject.java | 6 +++++- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/src/main/java/com/arangodb/springframework/repository/query/StringBasedArangoQuery.java b/src/main/java/com/arangodb/springframework/repository/query/StringBasedArangoQuery.java index 9effe5e5b..6b1e2806f 100644 --- a/src/main/java/com/arangodb/springframework/repository/query/StringBasedArangoQuery.java +++ b/src/main/java/com/arangodb/springframework/repository/query/StringBasedArangoQuery.java @@ -21,15 +21,12 @@ package com.arangodb.springframework.repository.query; import com.arangodb.model.AqlQueryOptions; -import com.arangodb.springframework.annotation.Document; -import com.arangodb.springframework.annotation.Edge; import com.arangodb.springframework.core.ArangoOperations; import com.arangodb.springframework.core.util.AqlUtils; import com.arangodb.springframework.repository.query.ArangoParameters.ArangoParameter; import org.springframework.context.ApplicationContext; import org.springframework.context.expression.BeanFactoryAccessor; import org.springframework.context.expression.BeanFactoryResolver; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.expression.Expression; import org.springframework.expression.ParserContext; import org.springframework.expression.spel.standard.SpelExpressionParser; @@ -111,22 +108,17 @@ private Collection allCollectionNames(Map bindVars) { bindVars.entrySet().stream() .filter(entry -> entry.getKey().startsWith("@")) .map(Map.Entry::getValue) - .map(value -> value instanceof Class ? getCollectionName((Class) value): value.toString()) - .filter(Objects::nonNull) + .map(this::asCollectionName) + .map(AqlUtils::buildCollectionName) .forEach(allCollections::add); return allCollections; } - private String getCollectionName(Class value) { - Document document = AnnotationUtils.findAnnotation(value, Document.class); - if (document != null) { - return document.value(); + private String asCollectionName(Object value) { + if (value instanceof Class) { + return mappingContext.getRequiredPersistentEntity((Class) value).getCollection(); } - Edge edge = AnnotationUtils.findAnnotation(value, Edge.class); - if (edge != null) { - return edge.value(); - } - return null; + return value.toString(); } @Override diff --git a/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionObject.java b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionObject.java index 077be4936..57bca3894 100644 --- a/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionObject.java +++ b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionObject.java @@ -25,6 +25,7 @@ import com.arangodb.entity.StreamTransactionStatus; import com.arangodb.model.StreamTransactionOptions; import com.arangodb.springframework.core.template.CollectionCallback; +import com.arangodb.springframework.core.util.AqlUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.lang.Nullable; @@ -35,6 +36,7 @@ import java.util.Collection; import java.util.HashSet; import java.util.Set; +import java.util.stream.Collectors; import static org.springframework.transaction.TransactionDefinition.TIMEOUT_DEFAULT; @@ -67,7 +69,9 @@ void configure(TransactionDefinition definition) { this.timeout = definition.getTimeout(); } if (definition instanceof TransactionAttribute) { - addCollections(((TransactionAttribute) definition).getLabels()); + addCollections(((TransactionAttribute) definition).getLabels().stream() + .map(AqlUtils::buildCollectionName) + .collect(Collectors.toList())); } } From f5773ac0d72aea81f0683862da94f6a6293c3aa5 Mon Sep 17 00:00:00 2001 From: Arne Burmeister Date: Wed, 23 Aug 2023 15:36:38 +0200 Subject: [PATCH 40/55] #80 improve docs --- .../repository/query/QueryTransactionBridge.java | 4 ++++ .../repository/query/StringBasedArangoQuery.java | 1 + .../transaction/ArangoTransactionHolder.java | 1 + .../transaction/ArangoTransactionManagementConfigurer.java | 4 +++- .../transaction/ArangoTransactionManager.java | 2 ++ .../transaction/ArangoTransactionObject.java | 6 ++++-- .../transaction/TransactionAttributeTemplate.java | 5 ++++- .../arangodb/springframework/transaction/package-info.java | 4 ++++ 8 files changed, 23 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/arangodb/springframework/transaction/package-info.java diff --git a/src/main/java/com/arangodb/springframework/repository/query/QueryTransactionBridge.java b/src/main/java/com/arangodb/springframework/repository/query/QueryTransactionBridge.java index 11efe8760..621b9e297 100644 --- a/src/main/java/com/arangodb/springframework/repository/query/QueryTransactionBridge.java +++ b/src/main/java/com/arangodb/springframework/repository/query/QueryTransactionBridge.java @@ -21,12 +21,15 @@ package com.arangodb.springframework.repository.query; import org.springframework.core.NamedInheritableThreadLocal; +import org.springframework.lang.Nullable; import java.util.Collection; import java.util.function.Function; /** * Bridge to postpone late transaction start to be able to inject collections from query side. + * + * @author Arne Burmeister */ public class QueryTransactionBridge { @@ -64,6 +67,7 @@ public void clearCurrentTransaction() { * @see AbstractArangoQuery * @see com.arangodb.springframework.repository.SimpleArangoRepository */ + @Nullable public String getCurrentTransaction(Collection collections) { return CURRENT_TRANSACTION.get().apply(collections); } diff --git a/src/main/java/com/arangodb/springframework/repository/query/StringBasedArangoQuery.java b/src/main/java/com/arangodb/springframework/repository/query/StringBasedArangoQuery.java index 6b1e2806f..07cd9b501 100644 --- a/src/main/java/com/arangodb/springframework/repository/query/StringBasedArangoQuery.java +++ b/src/main/java/com/arangodb/springframework/repository/query/StringBasedArangoQuery.java @@ -45,6 +45,7 @@ * @author Mark Vollmary * @author Christian Lechner * @author Michele Rastelli + * @author Arne Burmeister */ public class StringBasedArangoQuery extends AbstractArangoQuery { private static final SpelExpressionParser PARSER = new SpelExpressionParser(); diff --git a/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionHolder.java b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionHolder.java index 4a47dec7f..0d42db32c 100644 --- a/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionHolder.java +++ b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionHolder.java @@ -34,6 +34,7 @@ * * @see TransactionSynchronizationManager#bindResource(Object, Object) * @see ArangoTransactionObject + * @author Arne Burmeister */ class ArangoTransactionHolder { diff --git a/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManagementConfigurer.java b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManagementConfigurer.java index b3624c934..a9188a7e7 100644 --- a/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManagementConfigurer.java +++ b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManagementConfigurer.java @@ -32,7 +32,9 @@ * To enable stream transactions for Arango Spring Data, create a * {@link org.springframework.context.annotation.Configuration} class annotated with * {@link org.springframework.transaction.annotation.EnableTransactionManagement} and - * {@link org.springframework.context.annotation.Import} this one. + *{@link org.springframework.context.annotation.Import} this one. + * + * @author Arne Burmeister */ public class ArangoTransactionManagementConfigurer implements TransactionManagementConfigurer { diff --git a/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManager.java b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManager.java index 3ebf667c8..80a19833f 100644 --- a/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManager.java +++ b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManager.java @@ -45,6 +45,8 @@ * {@linkplain TransactionDefinition#getIsolationLevel() isolation} * {@linkplain TransactionDefinition#ISOLATION_SERIALIZABLE serializable} are * supported. + * + * @author Arne Burmeister */ public class ArangoTransactionManager extends AbstractPlatformTransactionManager implements InitializingBean { diff --git a/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionObject.java b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionObject.java index 57bca3894..d4f176cb6 100644 --- a/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionObject.java +++ b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionObject.java @@ -41,8 +41,10 @@ import static org.springframework.transaction.TransactionDefinition.TIMEOUT_DEFAULT; /** - * Transaction object created by - * {@link ArangoTransactionManager#doGetTransaction()}. + * Transaction object used for {@link org.springframework.transaction.support.DefaultTransactionStatus#getTransaction()}. + * + * @see ArangoTransactionManager#doGetTransaction() + * @author Arne Burmeister */ class ArangoTransactionObject implements SmartTransactionObject { diff --git a/src/main/java/com/arangodb/springframework/transaction/TransactionAttributeTemplate.java b/src/main/java/com/arangodb/springframework/transaction/TransactionAttributeTemplate.java index 2abc783ca..4f75c851e 100644 --- a/src/main/java/com/arangodb/springframework/transaction/TransactionAttributeTemplate.java +++ b/src/main/java/com/arangodb/springframework/transaction/TransactionAttributeTemplate.java @@ -20,6 +20,7 @@ package com.arangodb.springframework.transaction; +import org.springframework.lang.Nullable; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.interceptor.TransactionAttribute; @@ -34,6 +35,7 @@ * transaction exception handling in combination with a transaction manager using labels. * * @see ArangoTransactionManager + * @author Arne Burmeister */ public class TransactionAttributeTemplate extends TransactionTemplate implements TransactionAttribute { @@ -61,11 +63,12 @@ public TransactionAttributeTemplate(PlatformTransactionManager transactionManage } @Override + @Nullable public String getQualifier() { return qualifier; } - public void setQualifier(String qualifier) { + public void setQualifier(@Nullable String qualifier) { this.qualifier = qualifier; } diff --git a/src/main/java/com/arangodb/springframework/transaction/package-info.java b/src/main/java/com/arangodb/springframework/transaction/package-info.java new file mode 100644 index 000000000..67fc95c5a --- /dev/null +++ b/src/main/java/com/arangodb/springframework/transaction/package-info.java @@ -0,0 +1,4 @@ +@NonNullApi +package com.arangodb.springframework.transaction; + +import org.springframework.lang.NonNullApi; \ No newline at end of file From 1d60c4009b4c313abfd20c14ce57a18a08892d18 Mon Sep 17 00:00:00 2001 From: Arne Burmeister Date: Wed, 23 Aug 2023 18:00:57 +0200 Subject: [PATCH 41/55] #80 reduce ensure index calls (ignore hash and skiplist index as they will be removed with driver 7) --- .../core/template/ArangoTemplate.java | 128 +++++++++++++----- 1 file changed, 97 insertions(+), 31 deletions(-) diff --git a/src/main/java/com/arangodb/springframework/core/template/ArangoTemplate.java b/src/main/java/com/arangodb/springframework/core/template/ArangoTemplate.java index 42a9c0d4d..b64154a8c 100644 --- a/src/main/java/com/arangodb/springframework/core/template/ArangoTemplate.java +++ b/src/main/java/com/arangodb/springframework/core/template/ArangoTemplate.java @@ -56,6 +56,8 @@ import org.springframework.expression.ParserContext; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.lang.Nullable; +import org.springframework.util.ReflectionUtils; import java.util.*; import java.util.Map.Entry; @@ -181,68 +183,132 @@ private ArangoCollection _collection(final String name, final ArangoPersistentEn @SuppressWarnings("deprecation") private static void ensureCollectionIndexes(final CollectionOperations collection, final ArangoPersistentEntity persistentEntity) { - persistentEntity.getPersistentIndexes().forEach(index -> ensurePersistentIndex(collection, index)); - persistentEntity.getPersistentIndexedProperties().forEach(p -> ensurePersistentIndex(collection, p)); - persistentEntity.getGeoIndexes().forEach(index -> ensureGeoIndex(collection, index)); - persistentEntity.getGeoIndexedProperties().forEach(p -> ensureGeoIndex(collection, p)); - persistentEntity.getFulltextIndexes().forEach(index -> ensureFulltextIndex(collection, index)); - persistentEntity.getFulltextIndexedProperties().forEach(p -> ensureFulltextIndex(collection, p)); - persistentEntity.getTtlIndex().ifPresent(index -> ensureTtlIndex(collection, index)); - persistentEntity.getTtlIndexedProperty().ifPresent(p -> ensureTtlIndex(collection, p)); - } - - private static void ensurePersistentIndex(final CollectionOperations collection, final PersistentIndex annotation) { - collection.ensurePersistentIndex(Arrays.asList(annotation.fields()), - new PersistentIndexOptions() + Collection existing = collection.getIndexes(); + persistentEntity.getPersistentIndexes().forEach(index -> ensurePersistentIndex(collection, index, existing)); + persistentEntity.getPersistentIndexedProperties().forEach(p -> ensurePersistentIndex(collection, p, existing)); + persistentEntity.getGeoIndexes().forEach(index -> ensureGeoIndex(collection, index, existing)); + persistentEntity.getGeoIndexedProperties().forEach(p -> ensureGeoIndex(collection, p, existing)); + persistentEntity.getFulltextIndexes().forEach(index -> ensureFulltextIndex(collection, index, existing)); + persistentEntity.getFulltextIndexedProperties().forEach(p -> ensureFulltextIndex(collection, p, existing)); + persistentEntity.getTtlIndex().ifPresent(index -> ensureTtlIndex(collection, index, existing)); + persistentEntity.getTtlIndexedProperty().ifPresent(p -> ensureTtlIndex(collection, p, existing)); + } + + private static void ensurePersistentIndex(final CollectionOperations collection, final PersistentIndex annotation, Collection existing) { + PersistentIndexOptions options = new PersistentIndexOptions() .unique(annotation.unique()) .sparse(annotation.sparse()) - .deduplicate(annotation.deduplicate())); + .deduplicate(annotation.deduplicate()); + Collection fields = Arrays.asList(annotation.fields()); + if (existing.stream().noneMatch(index -> equalPersistentIndex(index, options, fields))) { + collection.ensurePersistentIndex(fields, options); + } } private static void ensurePersistentIndex(final CollectionOperations collection, - final ArangoPersistentProperty value) { + final ArangoPersistentProperty value, Collection existing) { final PersistentIndexOptions options = new PersistentIndexOptions(); value.getPersistentIndexed().ifPresent(i -> options .unique(i.unique()) .sparse(i.sparse()) .deduplicate(i.deduplicate())); - collection.ensurePersistentIndex(Collections.singleton(value.getFieldName()), options); + Collection fields = Collections.singleton(value.getFieldName()); + if (existing.stream().noneMatch(index -> equalPersistentIndex(index, options, fields))) { + collection.ensurePersistentIndex(fields, options); + } + } + + private static boolean equalPersistentIndex(IndexEntity index, PersistentIndexOptions options, Collection fields) { + return isIndexWithTypeAndFields(index, IndexType.persistent, fields) + && isEqualOption(index.getUnique(), options.getUnique(), false) + && isEqualOption(index.getSparse(), options.getSparse(), false) + && isEqualOption(index.getDeduplicate(), options.getDeduplicate(), false); } - private static void ensureGeoIndex(final CollectionOperations collection, final GeoIndex annotation) { - collection.ensureGeoIndex(Arrays.asList(annotation.fields()), - new GeoIndexOptions().geoJson(annotation.geoJson())); + private static void ensureGeoIndex(final CollectionOperations collection, final GeoIndex annotation, Collection existing) { + GeoIndexOptions options = new GeoIndexOptions().geoJson(annotation.geoJson()); + Collection fields = Arrays.asList(annotation.fields()); + if (existing.stream().noneMatch(index -> equalGeoIndex(index, options, fields))) { + collection.ensureGeoIndex(fields, options); + } } - private static void ensureGeoIndex(final CollectionOperations collection, final ArangoPersistentProperty value) { + private static void ensureGeoIndex(final CollectionOperations collection, final ArangoPersistentProperty value, Collection existing) { final GeoIndexOptions options = new GeoIndexOptions(); value.getGeoIndexed().ifPresent(i -> options.geoJson(i.geoJson())); - collection.ensureGeoIndex(Collections.singleton(value.getFieldName()), options); + Collection fields = Collections.singleton(value.getFieldName()); + if (existing.stream().noneMatch(index -> equalGeoIndex(index, options, fields))) { + collection.ensureGeoIndex(fields, options); + } + } + + private static boolean equalGeoIndex(IndexEntity index, GeoIndexOptions options, Collection fields) { + return isIndexWithTypeAndFields(index, IndexType.geo, fields) + && isEqualOption(index.getGeoJson(), options.getGeoJson(), false); } @SuppressWarnings("deprecation") - private static void ensureFulltextIndex(final CollectionOperations collection, final FulltextIndex annotation) { - collection.ensureFulltextIndex(Collections.singleton(annotation.field()), - new FulltextIndexOptions().minLength(annotation.minLength() > -1 ? annotation.minLength() : null)); + private static void ensureFulltextIndex(final CollectionOperations collection, final FulltextIndex annotation, Collection existing) { + Collection fields = Collections.singleton(annotation.field()); + FulltextIndexOptions options = new FulltextIndexOptions().minLength(annotation.minLength() > -1 ? annotation.minLength() : null); + if (existing.stream().noneMatch(index -> equalFulltextIndex(index, options, fields))) { + collection.ensureFulltextIndex(fields, options); + } } @SuppressWarnings("deprecation") private static void ensureFulltextIndex(final CollectionOperations collection, - final ArangoPersistentProperty value) { + final ArangoPersistentProperty value, Collection existing) { final FulltextIndexOptions options = new FulltextIndexOptions(); value.getFulltextIndexed().ifPresent(i -> options.minLength(i.minLength() > -1 ? i.minLength() : null)); - collection.ensureFulltextIndex(Collections.singleton(value.getFieldName()), options); + Collection fields = Collections.singleton(value.getFieldName()); + if (existing.stream().noneMatch(index -> equalFulltextIndex(index, options, fields))) { + collection.ensureFulltextIndex(fields, options); + } + } + + private static boolean equalFulltextIndex(IndexEntity index, FulltextIndexOptions options, Collection fields) { + return isIndexWithTypeAndFields(index, IndexType.fulltext, fields) + && isEqualOption(index.getMinLength(), options.getMinLength(), 0); } - private static void ensureTtlIndex(final CollectionOperations collection, final TtlIndex annotation) { - collection.ensureTtlIndex(Collections.singleton(annotation.field()), - new TtlIndexOptions().expireAfter(annotation.expireAfter())); + private static void ensureTtlIndex(final CollectionOperations collection, final TtlIndex annotation, Collection existing) { + TtlIndexOptions options = new TtlIndexOptions().expireAfter(annotation.expireAfter()); + Collection fields = Collections.singleton(annotation.field()); + if (existing.stream().noneMatch(index -> equalTtlIndex(index, options, fields))) { + collection.ensureTtlIndex(fields, options); + } } - private static void ensureTtlIndex(final CollectionOperations collection, final ArangoPersistentProperty value) { + private static void ensureTtlIndex(final CollectionOperations collection, final ArangoPersistentProperty value, Collection existing) { final TtlIndexOptions options = new TtlIndexOptions(); value.getTtlIndexed().ifPresent(i -> options.expireAfter(i.expireAfter())); - collection.ensureTtlIndex(Collections.singleton(value.getFieldName()), options); + Collection fields = Collections.singleton(value.getFieldName()); + if (existing.stream().noneMatch(index -> equalTtlIndex(index, options, fields))) { + collection.ensureTtlIndex(fields, options); + } + } + + private static boolean equalTtlIndex(IndexEntity index, TtlIndexOptions options, Collection fields) { + return isIndexWithTypeAndFields(index, IndexType.ttl, fields) + && isEqualOption(index.getExpireAfter(), getProtected(options, "getExpireAfter"), 0); + } + + private static boolean isIndexWithTypeAndFields(IndexEntity index, IndexType type, Collection fields) { + return index.getType() == type && index.getFields().size() == fields.size() && index.getFields().containsAll(fields); + } + + private static boolean isEqualOption(T value, @Nullable T optionalValue, T defaultValue) { + return value.equals(optionalValue == null ? defaultValue : optionalValue); + } + + @SuppressWarnings("unchecked") + @Deprecated + // can be removed for driver 7 as all option getters are visible + private static T getProtected(Object options, String getterName) { + Method getter = ReflectionUtils.findMethod(options.getClass(), getterName); + ReflectionUtils.makeAccessible(getter); + return (T) ReflectionUtils.invokeMethod(getter, options); } private Optional determineCollectionFromId(final Object id) { From 44afc3c8432fd8cd4e2b58edf0174b00de1a1d13 Mon Sep 17 00:00:00 2001 From: Arne Burmeister Date: Wed, 23 Aug 2023 18:16:41 +0200 Subject: [PATCH 42/55] #80 handle missing options --- .../core/template/ArangoTemplate.java | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/main/java/com/arangodb/springframework/core/template/ArangoTemplate.java b/src/main/java/com/arangodb/springframework/core/template/ArangoTemplate.java index b64154a8c..ad5170e73 100644 --- a/src/main/java/com/arangodb/springframework/core/template/ArangoTemplate.java +++ b/src/main/java/com/arangodb/springframework/core/template/ArangoTemplate.java @@ -345,7 +345,7 @@ public ArangoDBVersion getVersion() throws DataAccessException { public ArangoCursor query(final String query, final Map bindVars, final AqlQueryOptions options, final Class entityClass) throws DataAccessException { try { - ArangoCursor cursor = db().query(query, entityClass, bindVars == null ? null : prepareBindVars(bindVars, false), options); + ArangoCursor cursor = db().query(query, entityClass, bindVars == null ? null : prepareBindVars(bindVars, options != null && options.getStreamTransactionId() != null), options); return new ArangoExtCursor<>(cursor, entityClass, eventPublisher); } catch (final ArangoDBException e) { throw translateException(e); @@ -375,7 +375,7 @@ public MultiDocumentEntity> deleteAll( MultiDocumentEntity> result; try { - result = _collection(entityClass, options.getStreamTransactionId() != null).deleteDocuments(toList(values), options, entityClass); + result = _collection(entityClass, options != null && options.getStreamTransactionId() != null).deleteDocuments(toList(values), options, entityClass); } catch (final ArangoDBException e) { throw translateException(e); } @@ -407,7 +407,7 @@ public DocumentDeleteEntity delete(final Object id, final DocumentDeleteO DocumentDeleteEntity result; try { - result = _collection(entityClass, id, options.getStreamTransactionId() != null).deleteDocument(determineDocumentKeyFromId(id), options, entityClass); + result = _collection(entityClass, id, options != null && options.getStreamTransactionId() != null).deleteDocument(determineDocumentKeyFromId(id), options, entityClass); } catch (final ArangoDBException e) { throw translateException(e); } @@ -427,7 +427,7 @@ public MultiDocumentEntity> updateAll( MultiDocumentEntity> result; try { - result = _collection(entityClass, options.getStreamTransactionId() != null).updateDocuments(toList(values), options, entityClass); + result = _collection(entityClass, options != null && options.getStreamTransactionId() != null).updateDocuments(toList(values), options, entityClass); } catch (final ArangoDBException e) { throw translateException(e); } @@ -445,7 +445,7 @@ public DocumentUpdateEntity update(final Object id, final T value, final DocumentUpdateEntity result; try { - result = _collection(value.getClass(), id, options.getStreamTransactionId() != null).updateDocument(determineDocumentKeyFromId(id), value, options); + result = _collection(value.getClass(), id, options != null && options.getStreamTransactionId() != null).updateDocument(determineDocumentKeyFromId(id), value, options); } catch (final ArangoDBException e) { throw translateException(e); } @@ -466,7 +466,7 @@ public MultiDocumentEntity> replaceAll( MultiDocumentEntity> result; try { - result = _collection(entityClass, options.getStreamTransactionId() != null).replaceDocuments(toList(values), options, entityClass); + result = _collection(entityClass, options != null && options.getStreamTransactionId() != null).replaceDocuments(toList(values), options, entityClass); } catch (final ArangoDBException e) { throw translateException(e); } @@ -483,7 +483,7 @@ public DocumentUpdateEntity replace(final Object id, final T value, final DocumentUpdateEntity result; try { - result = _collection(value.getClass(), id, false).replaceDocument(determineDocumentKeyFromId(id), value, options); + result = _collection(value.getClass(), id, options != null && options.getStreamTransactionId() != null).replaceDocument(determineDocumentKeyFromId(id), value, options); } catch (final ArangoDBException e) { throw translateException(e); } @@ -497,7 +497,7 @@ public DocumentUpdateEntity replace(final Object id, final T value, final public Optional find(final Object id, final Class entityClass, final DocumentReadOptions options) throws DataAccessException { try { - T res = _collection(entityClass, id, options.getStreamTransactionId() != null).getDocument(determineDocumentKeyFromId(id), entityClass, options); + T res = _collection(entityClass, id, options != null && options.getStreamTransactionId() != null).getDocument(determineDocumentKeyFromId(id), entityClass, options); if (res != null) { potentiallyEmitEvent(new AfterLoadEvent<>(res)); } @@ -520,7 +520,7 @@ public Iterable findAll(final Iterable ids, final Class entityClass try { final Collection keys = new ArrayList<>(); ids.forEach(id -> keys.add(determineDocumentKeyFromId(id))); - Collection docs = _collection(entityClass, options.getStreamTransactionId() != null).getDocuments(keys, entityClass).getDocuments(); + Collection docs = _collection(entityClass, options != null && options.getStreamTransactionId() != null).getDocuments(keys, entityClass).getDocuments(); for (T doc : docs) { if (doc != null) { potentiallyEmitEvent(new AfterLoadEvent<>(doc)); @@ -540,7 +540,7 @@ public MultiDocumentEntity> insertAll( MultiDocumentEntity> result; try { - result = _collection(entityClass, options.getStreamTransactionId() != null).insertDocuments(toList(values), options, entityClass); + result = _collection(entityClass, options != null && options.getStreamTransactionId() != null).insertDocuments(toList(values), options, entityClass); } catch (final ArangoDBException e) { throw translateException(e); } @@ -556,7 +556,7 @@ public DocumentCreateEntity insert(final T value, final DocumentCreateOpt DocumentCreateEntity result; try { - result = _collection(value.getClass(), options.getStreamTransactionId() != null).insertDocument(value, options); + result = _collection(value.getClass(), options != null && options.getStreamTransactionId() != null).insertDocument(value, options); } catch (final ArangoDBException e) { throw translateException(e); } @@ -569,7 +569,7 @@ public DocumentCreateEntity insert(final T value, final DocumentCreateOpt @Override public T repsert(final T value, AqlQueryOptions options) throws DataAccessException { @SuppressWarnings("unchecked") final Class clazz = (Class) value.getClass(); - final String collectionName = _collection(clazz, options.getStreamTransactionId() != null).name(); + final String collectionName = _collection(clazz, options != null && options.getStreamTransactionId() != null).name(); potentiallyEmitEvent(new BeforeSaveEvent<>(value)); @@ -601,7 +601,7 @@ public Iterable repsertAll(final Iterable values, final Class bindVars = new HashMap<>(); @@ -702,7 +702,7 @@ private void updateDBFields(final Object value, final DocumentEntity documentEnt @Override public boolean exists(final Object id, final Class entityClass, DocumentExistsOptions options) throws DataAccessException { try { - return _collection(entityClass, options.getStreamTransactionId() != null).documentExists(determineDocumentKeyFromId(id), options); + return _collection(entityClass, options != null && options.getStreamTransactionId() != null).documentExists(determineDocumentKeyFromId(id), options); } catch (final ArangoDBException e) { throw translateException(e); } @@ -822,7 +822,7 @@ private List toList(Iterable it) { return l; } - private AqlQueryOptions asQueryOptions(DocumentReadOptions source) { + private static AqlQueryOptions asQueryOptions(DocumentReadOptions source) { return new AqlQueryOptions().streamTransactionId(source.getStreamTransactionId()).allowDirtyRead(source.getAllowDirtyRead()); } } From d2e72ea127a544b2142e386ea2d4d2f39e413844 Mon Sep 17 00:00:00 2001 From: Arne Burmeister Date: Thu, 24 Aug 2023 11:19:47 +0200 Subject: [PATCH 43/55] #80 log collection creation or index creation inside transaction --- .../core/template/ArangoTemplate.java | 47 ++++++++++++------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/src/main/java/com/arangodb/springframework/core/template/ArangoTemplate.java b/src/main/java/com/arangodb/springframework/core/template/ArangoTemplate.java index ad5170e73..54cd930f9 100644 --- a/src/main/java/com/arangodb/springframework/core/template/ArangoTemplate.java +++ b/src/main/java/com/arangodb/springframework/core/template/ArangoTemplate.java @@ -48,7 +48,6 @@ import org.springframework.context.expression.BeanFactoryAccessor; import org.springframework.context.expression.BeanFactoryResolver; import org.springframework.dao.DataAccessException; -import org.springframework.dao.InvalidDataAccessResourceUsageException; import org.springframework.dao.support.DataAccessUtils; import org.springframework.dao.support.PersistenceExceptionTranslator; import org.springframework.data.mapping.PersistentPropertyAccessor; @@ -161,7 +160,7 @@ private ArangoCollection _collection(final String name, final ArangoPersistentEn final ArangoCollection collection = db.collection(name); if (!collection.exists()) { if (transactional) { - throw new InvalidDataAccessResourceUsageException("Missing collection cannot be created during transaction"); + LOGGER.debug("Creating collection {} during transaction", name); } collection.create(options); } @@ -172,7 +171,7 @@ private ArangoCollection _collection(final String name, final ArangoPersistentEn if (persistentEntity != null && !entities.contains(entityClass)) { value.addEntityClass(entityClass); if (transactional) { - LOGGER.debug("Not ensuring any indexes of collection {} for {} during transaction", collection.name(), entityClass); + LOGGER.debug("Not ensuring any indexes of collection {} for {} during transaction", name, entityClass); } else { ensureCollectionIndexes(collection(collection), persistentEntity); } @@ -345,7 +344,8 @@ public ArangoDBVersion getVersion() throws DataAccessException { public ArangoCursor query(final String query, final Map bindVars, final AqlQueryOptions options, final Class entityClass) throws DataAccessException { try { - ArangoCursor cursor = db().query(query, entityClass, bindVars == null ? null : prepareBindVars(bindVars, options != null && options.getStreamTransactionId() != null), options); + boolean transactional = options != null && options.getStreamTransactionId() != null; + ArangoCursor cursor = db().query(query, entityClass, bindVars == null ? null : prepareBindVars(bindVars, transactional), options); return new ArangoExtCursor<>(cursor, entityClass, eventPublisher); } catch (final ArangoDBException e) { throw translateException(e); @@ -375,7 +375,8 @@ public MultiDocumentEntity> deleteAll( MultiDocumentEntity> result; try { - result = _collection(entityClass, options != null && options.getStreamTransactionId() != null).deleteDocuments(toList(values), options, entityClass); + boolean transactional = options != null && options.getStreamTransactionId() != null; + result = _collection(entityClass, transactional).deleteDocuments(toList(values), options, entityClass); } catch (final ArangoDBException e) { throw translateException(e); } @@ -407,7 +408,8 @@ public DocumentDeleteEntity delete(final Object id, final DocumentDeleteO DocumentDeleteEntity result; try { - result = _collection(entityClass, id, options != null && options.getStreamTransactionId() != null).deleteDocument(determineDocumentKeyFromId(id), options, entityClass); + boolean transactional = options != null && options.getStreamTransactionId() != null; + result = _collection(entityClass, id, transactional).deleteDocument(determineDocumentKeyFromId(id), options, entityClass); } catch (final ArangoDBException e) { throw translateException(e); } @@ -427,7 +429,8 @@ public MultiDocumentEntity> updateAll( MultiDocumentEntity> result; try { - result = _collection(entityClass, options != null && options.getStreamTransactionId() != null).updateDocuments(toList(values), options, entityClass); + boolean transactional = options != null && options.getStreamTransactionId() != null; + result = _collection(entityClass, transactional).updateDocuments(toList(values), options, entityClass); } catch (final ArangoDBException e) { throw translateException(e); } @@ -445,7 +448,8 @@ public DocumentUpdateEntity update(final Object id, final T value, final DocumentUpdateEntity result; try { - result = _collection(value.getClass(), id, options != null && options.getStreamTransactionId() != null).updateDocument(determineDocumentKeyFromId(id), value, options); + boolean transactional = options != null && options.getStreamTransactionId() != null; + result = _collection(value.getClass(), id, transactional).updateDocument(determineDocumentKeyFromId(id), value, options); } catch (final ArangoDBException e) { throw translateException(e); } @@ -466,7 +470,8 @@ public MultiDocumentEntity> replaceAll( MultiDocumentEntity> result; try { - result = _collection(entityClass, options != null && options.getStreamTransactionId() != null).replaceDocuments(toList(values), options, entityClass); + boolean transactional = options != null && options.getStreamTransactionId() != null; + result = _collection(entityClass, transactional).replaceDocuments(toList(values), options, entityClass); } catch (final ArangoDBException e) { throw translateException(e); } @@ -483,7 +488,8 @@ public DocumentUpdateEntity replace(final Object id, final T value, final DocumentUpdateEntity result; try { - result = _collection(value.getClass(), id, options != null && options.getStreamTransactionId() != null).replaceDocument(determineDocumentKeyFromId(id), value, options); + boolean transactional = options != null && options.getStreamTransactionId() != null; + result = _collection(value.getClass(), id, transactional).replaceDocument(determineDocumentKeyFromId(id), value, options); } catch (final ArangoDBException e) { throw translateException(e); } @@ -497,7 +503,8 @@ public DocumentUpdateEntity replace(final Object id, final T value, final public Optional find(final Object id, final Class entityClass, final DocumentReadOptions options) throws DataAccessException { try { - T res = _collection(entityClass, id, options != null && options.getStreamTransactionId() != null).getDocument(determineDocumentKeyFromId(id), entityClass, options); + boolean transactional = options != null && options.getStreamTransactionId() != null; + T res = _collection(entityClass, id, transactional).getDocument(determineDocumentKeyFromId(id), entityClass, options); if (res != null) { potentiallyEmitEvent(new AfterLoadEvent<>(res)); } @@ -520,7 +527,8 @@ public Iterable findAll(final Iterable ids, final Class entityClass try { final Collection keys = new ArrayList<>(); ids.forEach(id -> keys.add(determineDocumentKeyFromId(id))); - Collection docs = _collection(entityClass, options != null && options.getStreamTransactionId() != null).getDocuments(keys, entityClass).getDocuments(); + boolean transactional = options != null && options.getStreamTransactionId() != null; + Collection docs = _collection(entityClass, transactional).getDocuments(keys, entityClass).getDocuments(); for (T doc : docs) { if (doc != null) { potentiallyEmitEvent(new AfterLoadEvent<>(doc)); @@ -540,7 +548,8 @@ public MultiDocumentEntity> insertAll( MultiDocumentEntity> result; try { - result = _collection(entityClass, options != null && options.getStreamTransactionId() != null).insertDocuments(toList(values), options, entityClass); + boolean transactional = options != null && options.getStreamTransactionId() != null; + result = _collection(entityClass, transactional).insertDocuments(toList(values), options, entityClass); } catch (final ArangoDBException e) { throw translateException(e); } @@ -556,7 +565,8 @@ public DocumentCreateEntity insert(final T value, final DocumentCreateOpt DocumentCreateEntity result; try { - result = _collection(value.getClass(), options != null && options.getStreamTransactionId() != null).insertDocument(value, options); + boolean transactional = options != null && options.getStreamTransactionId() != null; + result = _collection(value.getClass(), transactional).insertDocument(value, options); } catch (final ArangoDBException e) { throw translateException(e); } @@ -569,7 +579,8 @@ public DocumentCreateEntity insert(final T value, final DocumentCreateOpt @Override public T repsert(final T value, AqlQueryOptions options) throws DataAccessException { @SuppressWarnings("unchecked") final Class clazz = (Class) value.getClass(); - final String collectionName = _collection(clazz, options != null && options.getStreamTransactionId() != null).name(); + boolean transactional = options != null && options.getStreamTransactionId() != null; + final String collectionName = _collection(clazz, transactional).name(); potentiallyEmitEvent(new BeforeSaveEvent<>(value)); @@ -601,7 +612,8 @@ public Iterable repsertAll(final Iterable values, final Class bindVars = new HashMap<>(); @@ -702,7 +714,8 @@ private void updateDBFields(final Object value, final DocumentEntity documentEnt @Override public boolean exists(final Object id, final Class entityClass, DocumentExistsOptions options) throws DataAccessException { try { - return _collection(entityClass, options != null && options.getStreamTransactionId() != null).documentExists(determineDocumentKeyFromId(id), options); + boolean transactional = options != null && options.getStreamTransactionId() != null; + return _collection(entityClass, transactional).documentExists(determineDocumentKeyFromId(id), options); } catch (final ArangoDBException e) { throw translateException(e); } From 90d47ad91c157b2c925ec00c03b2fda6b6a39ceb Mon Sep 17 00:00:00 2001 From: Arne Burmeister Date: Thu, 24 Aug 2023 17:14:32 +0200 Subject: [PATCH 44/55] #80 create only missing indexes, skip existing --- .../core/template/ArangoTemplate.java | 82 ++++++------------- .../core/template/CollectionCacheValue.java | 12 ++- 2 files changed, 33 insertions(+), 61 deletions(-) diff --git a/src/main/java/com/arangodb/springframework/core/template/ArangoTemplate.java b/src/main/java/com/arangodb/springframework/core/template/ArangoTemplate.java index 54cd930f9..72d027829 100644 --- a/src/main/java/com/arangodb/springframework/core/template/ArangoTemplate.java +++ b/src/main/java/com/arangodb/springframework/core/template/ArangoTemplate.java @@ -55,8 +55,6 @@ import org.springframework.expression.ParserContext; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.StandardEvaluationContext; -import org.springframework.lang.Nullable; -import org.springframework.util.ReflectionUtils; import java.util.*; import java.util.Map.Entry; @@ -66,6 +64,7 @@ * @author Mark Vollmary * @author Christian Lechner * @author ReÅŸat SABIQ + * @author Arne Burmeister */ public class ArangoTemplate implements ArangoOperations, CollectionCallback, ApplicationContextAware { @@ -164,16 +163,14 @@ private ArangoCollection _collection(final String name, final ArangoPersistentEn } collection.create(options); } - return new CollectionCacheValue(collection); + return new CollectionCacheValue(collection, collection.getIndexes()); }); - final Collection> entities = value.getEntities(); final ArangoCollection collection = value.getCollection(); - if (persistentEntity != null && !entities.contains(entityClass)) { - value.addEntityClass(entityClass); + if (persistentEntity != null && value.addEntityClass(entityClass)) { if (transactional) { LOGGER.debug("Not ensuring any indexes of collection {} for {} during transaction", name, entityClass); } else { - ensureCollectionIndexes(collection(collection), persistentEntity); + ensureCollectionIndexes(collection(collection), persistentEntity, value.getIndexes()); } } return collection; @@ -181,8 +178,7 @@ private ArangoCollection _collection(final String name, final ArangoPersistentEn @SuppressWarnings("deprecation") private static void ensureCollectionIndexes(final CollectionOperations collection, - final ArangoPersistentEntity persistentEntity) { - Collection existing = collection.getIndexes(); + final ArangoPersistentEntity persistentEntity, Collection existing) { persistentEntity.getPersistentIndexes().forEach(index -> ensurePersistentIndex(collection, index, existing)); persistentEntity.getPersistentIndexedProperties().forEach(p -> ensurePersistentIndex(collection, p, existing)); persistentEntity.getGeoIndexes().forEach(index -> ensureGeoIndex(collection, index, existing)); @@ -199,8 +195,8 @@ private static void ensurePersistentIndex(final CollectionOperations collection, .sparse(annotation.sparse()) .deduplicate(annotation.deduplicate()); Collection fields = Arrays.asList(annotation.fields()); - if (existing.stream().noneMatch(index -> equalPersistentIndex(index, options, fields))) { - collection.ensurePersistentIndex(fields, options); + if (createNewIndex(IndexType.persistent, fields, existing)) { + existing.add(collection.ensurePersistentIndex(fields, options)); } } @@ -212,23 +208,16 @@ private static void ensurePersistentIndex(final CollectionOperations collection, .sparse(i.sparse()) .deduplicate(i.deduplicate())); Collection fields = Collections.singleton(value.getFieldName()); - if (existing.stream().noneMatch(index -> equalPersistentIndex(index, options, fields))) { - collection.ensurePersistentIndex(fields, options); + if (createNewIndex(IndexType.persistent, fields, existing)) { + existing.add(collection.ensurePersistentIndex(fields, options)); } } - private static boolean equalPersistentIndex(IndexEntity index, PersistentIndexOptions options, Collection fields) { - return isIndexWithTypeAndFields(index, IndexType.persistent, fields) - && isEqualOption(index.getUnique(), options.getUnique(), false) - && isEqualOption(index.getSparse(), options.getSparse(), false) - && isEqualOption(index.getDeduplicate(), options.getDeduplicate(), false); - } - private static void ensureGeoIndex(final CollectionOperations collection, final GeoIndex annotation, Collection existing) { GeoIndexOptions options = new GeoIndexOptions().geoJson(annotation.geoJson()); Collection fields = Arrays.asList(annotation.fields()); - if (existing.stream().noneMatch(index -> equalGeoIndex(index, options, fields))) { - collection.ensureGeoIndex(fields, options); + if (createNewIndex(IndexType.geo, fields, existing)) { + existing.add(collection.ensureGeoIndex(fields, options)); } } @@ -236,22 +225,17 @@ private static void ensureGeoIndex(final CollectionOperations collection, final final GeoIndexOptions options = new GeoIndexOptions(); value.getGeoIndexed().ifPresent(i -> options.geoJson(i.geoJson())); Collection fields = Collections.singleton(value.getFieldName()); - if (existing.stream().noneMatch(index -> equalGeoIndex(index, options, fields))) { - collection.ensureGeoIndex(fields, options); + if (createNewIndex(IndexType.geo, fields, existing)) { + existing.add(collection.ensureGeoIndex(fields, options)); } } - private static boolean equalGeoIndex(IndexEntity index, GeoIndexOptions options, Collection fields) { - return isIndexWithTypeAndFields(index, IndexType.geo, fields) - && isEqualOption(index.getGeoJson(), options.getGeoJson(), false); - } - @SuppressWarnings("deprecation") private static void ensureFulltextIndex(final CollectionOperations collection, final FulltextIndex annotation, Collection existing) { Collection fields = Collections.singleton(annotation.field()); FulltextIndexOptions options = new FulltextIndexOptions().minLength(annotation.minLength() > -1 ? annotation.minLength() : null); - if (existing.stream().noneMatch(index -> equalFulltextIndex(index, options, fields))) { - collection.ensureFulltextIndex(fields, options); + if (createNewIndex(IndexType.fulltext, fields, existing)) { + existing.add(collection.ensureFulltextIndex(fields, options)); } } @@ -261,21 +245,16 @@ private static void ensureFulltextIndex(final CollectionOperations collection, final FulltextIndexOptions options = new FulltextIndexOptions(); value.getFulltextIndexed().ifPresent(i -> options.minLength(i.minLength() > -1 ? i.minLength() : null)); Collection fields = Collections.singleton(value.getFieldName()); - if (existing.stream().noneMatch(index -> equalFulltextIndex(index, options, fields))) { - collection.ensureFulltextIndex(fields, options); + if (createNewIndex(IndexType.fulltext, fields, existing)) { + existing.add(collection.ensureFulltextIndex(fields, options)); } } - private static boolean equalFulltextIndex(IndexEntity index, FulltextIndexOptions options, Collection fields) { - return isIndexWithTypeAndFields(index, IndexType.fulltext, fields) - && isEqualOption(index.getMinLength(), options.getMinLength(), 0); - } - private static void ensureTtlIndex(final CollectionOperations collection, final TtlIndex annotation, Collection existing) { TtlIndexOptions options = new TtlIndexOptions().expireAfter(annotation.expireAfter()); Collection fields = Collections.singleton(annotation.field()); - if (existing.stream().noneMatch(index -> equalTtlIndex(index, options, fields))) { - collection.ensureTtlIndex(fields, options); + if (createNewIndex(IndexType.ttl, fields, existing)) { + existing.add(collection.ensureTtlIndex(fields, options)); } } @@ -283,33 +262,20 @@ private static void ensureTtlIndex(final CollectionOperations collection, final final TtlIndexOptions options = new TtlIndexOptions(); value.getTtlIndexed().ifPresent(i -> options.expireAfter(i.expireAfter())); Collection fields = Collections.singleton(value.getFieldName()); - if (existing.stream().noneMatch(index -> equalTtlIndex(index, options, fields))) { - collection.ensureTtlIndex(fields, options); + if (createNewIndex(IndexType.ttl, fields, existing)) { + existing.add(collection.ensureTtlIndex(fields, options)); } } - private static boolean equalTtlIndex(IndexEntity index, TtlIndexOptions options, Collection fields) { - return isIndexWithTypeAndFields(index, IndexType.ttl, fields) - && isEqualOption(index.getExpireAfter(), getProtected(options, "getExpireAfter"), 0); + private static boolean createNewIndex(IndexType type, Collection fields, Collection existing) { + return existing.stream() + .noneMatch(index -> isIndexWithTypeAndFields(index, type, fields)); } private static boolean isIndexWithTypeAndFields(IndexEntity index, IndexType type, Collection fields) { return index.getType() == type && index.getFields().size() == fields.size() && index.getFields().containsAll(fields); } - private static boolean isEqualOption(T value, @Nullable T optionalValue, T defaultValue) { - return value.equals(optionalValue == null ? defaultValue : optionalValue); - } - - @SuppressWarnings("unchecked") - @Deprecated - // can be removed for driver 7 as all option getters are visible - private static T getProtected(Object options, String getterName) { - Method getter = ReflectionUtils.findMethod(options.getClass(), getterName); - ReflectionUtils.makeAccessible(getter); - return (T) ReflectionUtils.invokeMethod(getter, options); - } - private Optional determineCollectionFromId(final Object id) { return id != null ? Optional.ofNullable(MetadataUtils.determineCollectionFromId(converter.convertId(id))) : Optional.empty(); diff --git a/src/main/java/com/arangodb/springframework/core/template/CollectionCacheValue.java b/src/main/java/com/arangodb/springframework/core/template/CollectionCacheValue.java index 51e88983d..bbaece017 100644 --- a/src/main/java/com/arangodb/springframework/core/template/CollectionCacheValue.java +++ b/src/main/java/com/arangodb/springframework/core/template/CollectionCacheValue.java @@ -4,16 +4,19 @@ import java.util.Collection; import com.arangodb.ArangoCollection; +import com.arangodb.entity.IndexEntity; class CollectionCacheValue { private final ArangoCollection collection; private final Collection> entities; + private final Collection indexes; - public CollectionCacheValue(final ArangoCollection collection) { + public CollectionCacheValue(final ArangoCollection collection, Collection indexes) { super(); this.collection = collection; this.entities = new ArrayList<>(); + this.indexes = indexes; } public ArangoCollection getCollection() { @@ -24,8 +27,11 @@ public Collection> getEntities() { return entities; } - public void addEntityClass(final Class entityClass) { - entities.add(entityClass); + public Collection getIndexes() { + return indexes; + } + public boolean addEntityClass(final Class entityClass) { + return entities.add(entityClass); } } \ No newline at end of file From b84931fd019f5f4ab727221f62aefa08285dd936 Mon Sep 17 00:00:00 2001 From: Arne Burmeister Date: Thu, 31 Aug 2023 19:23:20 +0200 Subject: [PATCH 45/55] #80 fix merge errors --- .../transaction/ArangoTransactionManagerTest.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/test/java/com/arangodb/springframework/transaction/ArangoTransactionManagerTest.java b/src/test/java/com/arangodb/springframework/transaction/ArangoTransactionManagerTest.java index dcd587702..ae8fce035 100644 --- a/src/test/java/com/arangodb/springframework/transaction/ArangoTransactionManagerTest.java +++ b/src/test/java/com/arangodb/springframework/transaction/ArangoTransactionManagerTest.java @@ -17,6 +17,7 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.lang.Nullable; +import org.springframework.test.util.ReflectionTestUtils; import org.springframework.transaction.*; import org.springframework.transaction.interceptor.DefaultTransactionAttribute; import org.springframework.transaction.support.DefaultTransactionStatus; @@ -49,7 +50,6 @@ public class ArangoTransactionManagerTest { private ArangoTransactionManager underTest; @Mock private ArangoDatabase database; - @Mock private StreamTransactionEntity streamTransaction; @Captor private ArgumentCaptor, String>> beginPassed; @@ -58,6 +58,7 @@ public class ArangoTransactionManagerTest { @Before public void setupMocks() { + streamTransaction = new StreamTransactionEntity(); when(operations.db()) .thenReturn(database); } @@ -124,8 +125,6 @@ public void nestedGetTransactionReturnsExistingTransactionWithFormerCollections( assertThat(outer.isNewTransaction(), is(true)); beginTransaction("123", "foo", "bar"); - when(streamTransaction.getStatus()) - .thenReturn(StreamTransactionStatus.running); DefaultTransactionAttribute second = createTransactionAttribute("inner", "bar"); TransactionStatus inner1 = underTest.getTransaction(second); @@ -194,8 +193,8 @@ public void getTransactionThrowsInvalidIsolationLevelExceptionForIsolationSerial } private void beginTransaction(String id, String... collectionNames) { - when(streamTransaction.getId()) - .thenReturn(id); + ReflectionTestUtils.setField(streamTransaction, "id", id); + ReflectionTestUtils.setField(streamTransaction, "status", StreamTransactionStatus.running); when(database.beginStreamTransaction(any())) .thenReturn(streamTransaction); lenient().when(database.getStreamTransaction(any())) From 40c96b7e6537ed28705c8fafc5bf01f15ae31e7f Mon Sep 17 00:00:00 2001 From: Arne Burmeister Date: Tue, 5 Sep 2023 14:37:21 +0200 Subject: [PATCH 46/55] #80 move new test to junit5 --- .../query/QueryTransactionBridgeTest.java | 6 ++-- ...rangoTransactionManagerRepositoryTest.java | 2 +- .../ArangoTransactionManagerTest.java | 30 +++++++++---------- 3 files changed, 18 insertions(+), 20 deletions(-) diff --git a/src/test/java/com/arangodb/springframework/repository/query/QueryTransactionBridgeTest.java b/src/test/java/com/arangodb/springframework/repository/query/QueryTransactionBridgeTest.java index d7735c506..48474f38a 100644 --- a/src/test/java/com/arangodb/springframework/repository/query/QueryTransactionBridgeTest.java +++ b/src/test/java/com/arangodb/springframework/repository/query/QueryTransactionBridgeTest.java @@ -1,8 +1,8 @@ package com.arangodb.springframework.repository.query; import org.hamcrest.Matchers; -import org.junit.After; -import org.junit.Test; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; import java.util.Collections; @@ -23,7 +23,7 @@ public void setCurrentTransactionIsAppliedOnGetCurrentTransaction() { assertThat(underTest.getCurrentTransaction(Collections.singleton("test")), Matchers.is("test")); } - @After + @AfterEach public void cleanup() { underTest.clearCurrentTransaction(); } diff --git a/src/test/java/com/arangodb/springframework/transaction/ArangoTransactionManagerRepositoryTest.java b/src/test/java/com/arangodb/springframework/transaction/ArangoTransactionManagerRepositoryTest.java index 8117b9743..53af05b8b 100644 --- a/src/test/java/com/arangodb/springframework/transaction/ArangoTransactionManagerRepositoryTest.java +++ b/src/test/java/com/arangodb/springframework/transaction/ArangoTransactionManagerRepositoryTest.java @@ -5,7 +5,7 @@ import com.arangodb.springframework.repository.ActorRepository; import com.arangodb.springframework.repository.MovieRepository; import com.arangodb.springframework.testdata.Movie; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.transaction.TestTransaction; diff --git a/src/test/java/com/arangodb/springframework/transaction/ArangoTransactionManagerTest.java b/src/test/java/com/arangodb/springframework/transaction/ArangoTransactionManagerTest.java index ae8fce035..6c68c1eea 100644 --- a/src/test/java/com/arangodb/springframework/transaction/ArangoTransactionManagerTest.java +++ b/src/test/java/com/arangodb/springframework/transaction/ArangoTransactionManagerTest.java @@ -7,15 +7,15 @@ import com.arangodb.model.TransactionCollectionOptions; import com.arangodb.springframework.core.ArangoOperations; import com.arangodb.springframework.repository.query.QueryTransactionBridge; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.lang.Nullable; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.transaction.*; @@ -35,11 +35,12 @@ import static org.hamcrest.Matchers.hasItems; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.nullValue; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; import static org.springframework.beans.PropertyAccessorFactory.forDirectFieldAccess; -@RunWith(MockitoJUnitRunner.class) +@ExtendWith(MockitoExtension.class) public class ArangoTransactionManagerTest { @Mock @@ -56,14 +57,14 @@ public class ArangoTransactionManagerTest { @Captor private ArgumentCaptor optionsPassed; - @Before + @BeforeEach public void setupMocks() { streamTransaction = new StreamTransactionEntity(); when(operations.db()) .thenReturn(database); } - @After + @AfterEach public void cleanupSync() { TransactionSynchronizationManager.unbindResourceIfPossible(underTest); TransactionSynchronizationManager.clear(); @@ -91,16 +92,13 @@ public void innerGetTransactionIsNotNewTransactionIncludingFormerCollections() { verifyNoInteractions(database); } - @Test(expected = UnexpectedRollbackException.class) + @Test public void innerRollbackCausesUnexpectedRollbackOnOuterCommit() { TransactionStatus outer = underTest.getTransaction(createTransactionAttribute("outer")); TransactionStatus inner = underTest.getTransaction(createTransactionAttribute("inner")); underTest.rollback(inner); - try { - underTest.commit(outer); - } finally { - assertThat(TransactionSynchronizationManager.getResource(underTest), nullValue()); - } + assertThrows(UnexpectedRollbackException.class, () -> underTest.commit(outer)); + assertThat(TransactionSynchronizationManager.getResource(underTest), nullValue()); } @Test @@ -185,11 +183,11 @@ public void getTransactionWithMultipleBridgeCallsIgnoresAdditionalCollections() assertThat(getTransactionObject(state).getHolder().getCollectionNames(), not(hasItem("baz"))); } - @Test(expected = InvalidIsolationLevelException.class) + @Test public void getTransactionThrowsInvalidIsolationLevelExceptionForIsolationSerializable() { DefaultTransactionAttribute definition = createTransactionAttribute("serializable"); definition.setIsolationLevel(TransactionDefinition.ISOLATION_SERIALIZABLE); - underTest.getTransaction(definition); + assertThrows(InvalidIsolationLevelException.class, () -> underTest.getTransaction(definition)); } private void beginTransaction(String id, String... collectionNames) { From 93b28b7e62f9a9d4a0e3ea75bca9a2b3c468bff6 Mon Sep 17 00:00:00 2001 From: Arne Burmeister Date: Tue, 12 Sep 2023 14:48:42 +0200 Subject: [PATCH 47/55] fix merge error --- .../core/ArangoOperations.java | 10 ++--- .../resolver/DefaultResolverFactory.java | 42 +++++++++++-------- .../core/template/ArangoTemplate.java | 6 ++- .../repository/SimpleArangoRepository.java | 2 +- 4 files changed, 36 insertions(+), 24 deletions(-) diff --git a/src/main/java/com/arangodb/springframework/core/ArangoOperations.java b/src/main/java/com/arangodb/springframework/core/ArangoOperations.java index 82fc9b3a1..aebfd69a8 100644 --- a/src/main/java/com/arangodb/springframework/core/ArangoOperations.java +++ b/src/main/java/com/arangodb/springframework/core/ArangoOperations.java @@ -451,7 +451,7 @@ MultiDocumentEntity> insertAll( * @return information about the documents * @throws DataAccessException */ - default MultiDocumentEntity> insertAll(Iterable values, Class entityClass) + default MultiDocumentEntity> insertAll(Iterable values, Class entityClass) throws DataAccessException { return insertAll(values, new DocumentCreateOptions(), entityClass); } @@ -488,10 +488,10 @@ default DocumentCreateEntity insert(T value) throws DataAccessException { * @throws DataAccessException * @since ArangoDB 3.4 */ - void repsert(T value, AqlQueryOptions options) throws DataAccessException; + T repsert(T value, AqlQueryOptions options) throws DataAccessException; default T repsert(T value) throws DataAccessException { - repsert(value, new AqlQueryOptions()); + return repsert(value, new AqlQueryOptions()); } /** @@ -505,9 +505,9 @@ default T repsert(T value) throws DataAccessException { * @throws DataAccessException * @since ArangoDB 3.4 */ - Iterable repsertAll(Iterable values, Class entityClass, AqlQueryOptions options) throws DataAccessException; + Iterable repsertAll(Iterable values, Class entityClass, AqlQueryOptions options) throws DataAccessException; - default Iterable repsertAll(Iterable values, Class entityClass) throws DataAccessException { + default Iterable repsertAll(Iterable values, Class entityClass) throws DataAccessException { return repsertAll(values, entityClass, new AqlQueryOptions()); } diff --git a/src/main/java/com/arangodb/springframework/core/convert/resolver/DefaultResolverFactory.java b/src/main/java/com/arangodb/springframework/core/convert/resolver/DefaultResolverFactory.java index a286f7cab..b99d3413c 100644 --- a/src/main/java/com/arangodb/springframework/core/convert/resolver/DefaultResolverFactory.java +++ b/src/main/java/com/arangodb/springframework/core/convert/resolver/DefaultResolverFactory.java @@ -23,6 +23,7 @@ import java.lang.annotation.Annotation; import java.util.Optional; +import com.arangodb.ArangoDBException; import org.springframework.beans.BeansException; import org.springframework.beans.factory.ObjectProvider; import org.springframework.context.ApplicationContext; @@ -45,11 +46,14 @@ public class DefaultResolverFactory implements ResolverFactory, ApplicationConte @SuppressWarnings("unchecked") @Override public Optional> getReferenceResolver(final A annotation) { - ReferenceResolver resolver = null; - if (annotation instanceof Ref) { - resolver = (ReferenceResolver) new RefResolver(template.getObject(), transactionBridge.getIfUnique()); + try { + if (annotation instanceof Ref) { + return Optional.of((ReferenceResolver) new RefResolver(template.getObject(), transactionBridge.getIfUnique())); + } + } catch (final Exception e) { + throw new ArangoDBException(e); } - return Optional.ofNullable(resolver); + return Optional.empty(); } @SuppressWarnings("unchecked") @@ -57,20 +61,24 @@ public Optional> getReferenceResolve public Optional> getRelationResolver(final A annotation, final Class collectionType) { RelationResolver resolver = null; - if (annotation instanceof From) { - if (collectionType == Edge.class) { - resolver = (RelationResolver) new EdgeFromResolver(template.getObject(), transactionBridge.getIfUnique()); - } else if (collectionType == Document.class) { - resolver = (RelationResolver) new DocumentFromResolver(template.getObject(), transactionBridge.getIfUnique()); - } - } else if (annotation instanceof To) { - if (collectionType == Edge.class) { - resolver = (RelationResolver) new EdgeToResolver(template.getObject(), transactionBridge.getIfUnique()); - } else if (collectionType == Document.class) { - resolver = (RelationResolver) new DocumentToResolver(template.getObject(), transactionBridge.getIfUnique()); + try { + if (annotation instanceof From) { + if (collectionType == Edge.class) { + resolver = (RelationResolver) new EdgeFromResolver(template.getObject(), null); + } else if (collectionType == Document.class) { + resolver = (RelationResolver) new DocumentFromResolver(template.getObject(), null); + } + } else if (annotation instanceof To) { + if (collectionType == Edge.class) { + resolver = (RelationResolver) new EdgeToResolver(template.getObject(), null); + } else if (collectionType == Document.class) { + resolver = (RelationResolver) new DocumentToResolver(template.getObject(), null); + } + } else if (annotation instanceof Relations) { + resolver = (RelationResolver) new RelationsResolver(template.getObject(), null); } - } else if (annotation instanceof Relations) { - resolver = (RelationResolver) new RelationsResolver(template.getObject(), transactionBridge.getIfUnique()); + } catch (final Exception e) { + throw new ArangoDBException(e); } return Optional.ofNullable(resolver); } diff --git a/src/main/java/com/arangodb/springframework/core/template/ArangoTemplate.java b/src/main/java/com/arangodb/springframework/core/template/ArangoTemplate.java index 72d027829..4b7e114af 100644 --- a/src/main/java/com/arangodb/springframework/core/template/ArangoTemplate.java +++ b/src/main/java/com/arangodb/springframework/core/template/ArangoTemplate.java @@ -35,7 +35,6 @@ import com.arangodb.springframework.core.mapping.ArangoPersistentEntity; import com.arangodb.springframework.core.mapping.ArangoPersistentProperty; import com.arangodb.springframework.core.mapping.event.*; -import com.arangodb.springframework.core.template.DefaultUserOperation.CollectionCallback; import com.arangodb.springframework.core.util.ArangoExceptionTranslator; import com.arangodb.springframework.core.util.MetadataUtils; import com.fasterxml.jackson.databind.node.JsonNodeFactory; @@ -705,6 +704,11 @@ public CollectionOperations collection(final Class entityClass) throws DataAc return collection(_collection(entityClass, false)); } + @Override + public CollectionOperations collection(String name) throws DataAccessException { + return ArangoOperations.super.collection(name); + } + @Override public CollectionOperations collection(final String name, final CollectionCreateOptions options) throws DataAccessException { diff --git a/src/main/java/com/arangodb/springframework/repository/SimpleArangoRepository.java b/src/main/java/com/arangodb/springframework/repository/SimpleArangoRepository.java index 9dcdfa722..3fd2eaa2d 100644 --- a/src/main/java/com/arangodb/springframework/repository/SimpleArangoRepository.java +++ b/src/main/java/com/arangodb/springframework/repository/SimpleArangoRepository.java @@ -301,7 +301,7 @@ public Optional findOne(final Example example) { */ @Override public Iterable findAll(final Example example) { - return (ArangoCursor) findAllInternal((Pageable) null, example, new HashMap<>()); + return (ArangoCursor) findAllInternal((Pageable) null, example, new HashMap<>()); } /** From c6912ab6f7121a0203a087ab7d1ebffdca7e628d Mon Sep 17 00:00:00 2001 From: Arne Burmeister Date: Tue, 19 Sep 2023 11:58:52 +0200 Subject: [PATCH 48/55] no deprecation warning in tests --- .../springframework/core/template/ArangoIndexTest.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/test/java/com/arangodb/springframework/core/template/ArangoIndexTest.java b/src/test/java/com/arangodb/springframework/core/template/ArangoIndexTest.java index 99d663739..bdcb68b44 100644 --- a/src/test/java/com/arangodb/springframework/core/template/ArangoIndexTest.java +++ b/src/test/java/com/arangodb/springframework/core/template/ArangoIndexTest.java @@ -52,7 +52,7 @@ private IndexType geo2() { } private IndexType geoType(final IndexType type) { - return Integer.valueOf(template.getVersion().getVersion().split("\\.")[1]) >= 4 ? IndexType.geo : type; + return Integer.parseInt(template.getVersion().getVersion().split("\\.")[1]) >= 4 ? IndexType.geo : type; } public static class PersistentIndexedSingleFieldTestEntity { @@ -309,6 +309,7 @@ public void multipleSingleFieldFulltextIndexed() { hasItems(IndexType.primary, IndexType.fulltext)); } + @SuppressWarnings("deprecation") @FulltextIndex(field = "a") public static class FulltextIndexWithSingleFieldTestEntity { } @@ -326,6 +327,7 @@ public void singleFieldFulltextIndex() { hasItems("a")); } + @SuppressWarnings("deprecation") @FulltextIndex(field = "a") @FulltextIndex(field = "b") public static class FulltextIndexWithMultipleSingleFieldTestEntity { @@ -341,6 +343,7 @@ public void multipleSingleFieldFulltextIndex() { hasItems(IndexType.primary, IndexType.fulltext)); } + @SuppressWarnings("deprecation") @FulltextIndexes({@FulltextIndex(field = "a"), @FulltextIndex(field = "b")}) public static class FulltextIndexWithMultipleIndexesTestEntity { } @@ -374,6 +377,7 @@ public void differentIndexedAnnotationsSameField() { hasItems(IndexType.primary, IndexType.persistent, geo1(), IndexType.fulltext, IndexType.ttl)); } + @SuppressWarnings("deprecation") @PersistentIndex(fields = {"a"}) @GeoIndex(fields = {"a"}) @FulltextIndex(field = "a") @@ -391,6 +395,7 @@ public void differentIndexAnnotations() { hasItems(IndexType.primary, IndexType.persistent, geo1(), IndexType.fulltext)); } + @SuppressWarnings("deprecation") @PersistentIndex(fields = {"a"}) @PersistentIndex(fields = {"b"}) @GeoIndex(fields = {"a"}) From 1991dbc6f86863cf0316ee61f630d4c26d085132 Mon Sep 17 00:00:00 2001 From: Arne Burmeister Date: Tue, 19 Sep 2023 12:04:31 +0200 Subject: [PATCH 49/55] fix generic --- .../springframework/core/template/ArangoTemplateTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/com/arangodb/springframework/core/template/ArangoTemplateTest.java b/src/test/java/com/arangodb/springframework/core/template/ArangoTemplateTest.java index ae9c49bb9..a35ac4289 100644 --- a/src/test/java/com/arangodb/springframework/core/template/ArangoTemplateTest.java +++ b/src/test/java/com/arangodb/springframework/core/template/ArangoTemplateTest.java @@ -410,7 +410,7 @@ public void deleteDocuments() { final Product documentA = template.find(a.getId(), Product.class).get(); final Product documentB = template.find(b.getId(), Product.class).get(); - final MultiDocumentEntity> res = template.deleteAll(Arrays.asList(documentA, documentB), Product.class); + final MultiDocumentEntity> res = template.deleteAll(Arrays.asList(documentA, documentB), Product.class); assertThat(res, is(notNullValue())); assertThat(res.getDocuments().size(), is(2)); From 7202d5b2c0dd0de9908c9eee61cdc4678605757a Mon Sep 17 00:00:00 2001 From: Arne Burmeister Date: Tue, 19 Sep 2023 12:31:27 +0200 Subject: [PATCH 50/55] #80 adjust order of repsertAll to insertAll --- .../springframework/core/ArangoOperations.java | 10 ++++------ .../springframework/core/template/ArangoTemplate.java | 5 +++-- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/arangodb/springframework/core/ArangoOperations.java b/src/main/java/com/arangodb/springframework/core/ArangoOperations.java index aebfd69a8..fd290d279 100644 --- a/src/main/java/com/arangodb/springframework/core/ArangoOperations.java +++ b/src/main/java/com/arangodb/springframework/core/ArangoOperations.java @@ -498,17 +498,15 @@ default T repsert(T value) throws DataAccessException { * Creates new documents from the given documents, unless there already exists. In that case it replaces the * documents. * - * @param values - * A List of documents - * @param entityClass - * The entity class which represents the collection + * @param values A List of documents + * @param entityClass The entity class which represents the collection * @throws DataAccessException * @since ArangoDB 3.4 */ - Iterable repsertAll(Iterable values, Class entityClass, AqlQueryOptions options) throws DataAccessException; + Iterable repsertAll(Iterable values, AqlQueryOptions options, Class entityClass) throws DataAccessException; default Iterable repsertAll(Iterable values, Class entityClass) throws DataAccessException { - return repsertAll(values, entityClass, new AqlQueryOptions()); + return repsertAll(values, new AqlQueryOptions(), entityClass); } /** diff --git a/src/main/java/com/arangodb/springframework/core/template/ArangoTemplate.java b/src/main/java/com/arangodb/springframework/core/template/ArangoTemplate.java index 4b7e114af..086639263 100644 --- a/src/main/java/com/arangodb/springframework/core/template/ArangoTemplate.java +++ b/src/main/java/com/arangodb/springframework/core/template/ArangoTemplate.java @@ -570,9 +570,9 @@ public T repsert(final T value, AqlQueryOptions options) throws DataAccessEx return result; } - @SuppressWarnings({"rawtypes", "unchecked"}) + @SuppressWarnings("unchecked") @Override - public Iterable repsertAll(final Iterable values, final Class entityClass, AqlQueryOptions options) throws DataAccessException { + public Iterable repsertAll(final Iterable values, AqlQueryOptions options, final Class entityClass) throws DataAccessException { if (!values.iterator().hasNext()) { return Collections.emptyList(); } @@ -585,6 +585,7 @@ public Iterable repsertAll(final Iterable values, final Class Date: Tue, 19 Sep 2023 16:47:11 +0200 Subject: [PATCH 51/55] fix tx of relation resolvers --- .../resolver/DefaultResolverFactory.java | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/arangodb/springframework/core/convert/resolver/DefaultResolverFactory.java b/src/main/java/com/arangodb/springframework/core/convert/resolver/DefaultResolverFactory.java index b99d3413c..c08e3321c 100644 --- a/src/main/java/com/arangodb/springframework/core/convert/resolver/DefaultResolverFactory.java +++ b/src/main/java/com/arangodb/springframework/core/convert/resolver/DefaultResolverFactory.java @@ -29,7 +29,6 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; -import com.arangodb.springframework.annotation.Document; import com.arangodb.springframework.annotation.Edge; import com.arangodb.springframework.annotation.From; import com.arangodb.springframework.annotation.Ref; @@ -60,27 +59,26 @@ public Optional> getReferenceResolve @Override public Optional> getRelationResolver(final A annotation, final Class collectionType) { - RelationResolver resolver = null; try { if (annotation instanceof From) { if (collectionType == Edge.class) { - resolver = (RelationResolver) new EdgeFromResolver(template.getObject(), null); - } else if (collectionType == Document.class) { - resolver = (RelationResolver) new DocumentFromResolver(template.getObject(), null); + return Optional.of((RelationResolver) new EdgeFromResolver(template.getObject(), transactionBridge.getIfUnique())); } - } else if (annotation instanceof To) { + return Optional.of((RelationResolver) new DocumentFromResolver(template.getObject(), transactionBridge.getIfUnique())); + } + if (annotation instanceof To) { if (collectionType == Edge.class) { - resolver = (RelationResolver) new EdgeToResolver(template.getObject(), null); - } else if (collectionType == Document.class) { - resolver = (RelationResolver) new DocumentToResolver(template.getObject(), null); + return Optional.of((RelationResolver) new EdgeToResolver(template.getObject(), transactionBridge.getIfUnique())); } - } else if (annotation instanceof Relations) { - resolver = (RelationResolver) new RelationsResolver(template.getObject(), null); + return Optional.of((RelationResolver) new DocumentToResolver(template.getObject(), transactionBridge.getIfUnique())); + } + if (annotation instanceof Relations) { + return Optional.of((RelationResolver) new RelationsResolver(template.getObject(), transactionBridge.getIfUnique())); } } catch (final Exception e) { throw new ArangoDBException(e); } - return Optional.ofNullable(resolver); + return Optional.empty(); } @Override From 37fc779ad32eb7b8beee16d75c4bcb181a9ec348 Mon Sep 17 00:00:00 2001 From: Arne Burmeister Date: Tue, 30 Jan 2024 17:53:47 +0100 Subject: [PATCH 52/55] #80 fix for spring 6.1 --- .../transaction/ArangoTransactionManager.java | 20 ++++++------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManager.java b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManager.java index 80a19833f..52e9ae74f 100644 --- a/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManager.java +++ b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManager.java @@ -190,27 +190,19 @@ protected void doSetRollbackOnly(DefaultTransactionStatus status) throws Transac /** * Any transaction object is configured according to the definition upfront. - * - * @see ArangoTransactionObject#configure(TransactionDefinition) - */ - @Override - protected DefaultTransactionStatus newTransactionStatus(TransactionDefinition definition, @Nullable Object transaction, boolean newTransaction, boolean newSynchronization, boolean debug, @Nullable Object suspendedResources) { - if (transaction instanceof ArangoTransactionObject) { - ((ArangoTransactionObject) transaction).configure(definition); - } - return super.newTransactionStatus(definition, transaction, newTransaction, newSynchronization, debug, suspendedResources); - } - - /** * Bind the holder for the first new transaction created. * * @see ArangoTransactionHolder */ @Override protected void prepareSynchronization(DefaultTransactionStatus status, TransactionDefinition definition) { + ArangoTransactionObject transaction = status.hasTransaction() ? (ArangoTransactionObject) status.getTransaction() : null; + if (transaction != null) { + transaction.configure(definition); + } super.prepareSynchronization(status, definition); - if (status.isNewSynchronization()) { - ArangoTransactionHolder holder = ((ArangoTransactionObject) status.getTransaction()).getHolder(); + if (transaction != null && status.isNewSynchronization()) { + ArangoTransactionHolder holder = transaction.getHolder(); TransactionSynchronizationManager.bindResource(this, holder); } } From 9320edd2b0f26b65e5a1b5cbe72101db12e07a50 Mon Sep 17 00:00:00 2001 From: Arne Burmeister Date: Mon, 26 Feb 2024 15:53:35 +0100 Subject: [PATCH 53/55] fix deprecation, but compatible to 3.0 --- .../core/convert/resolver/DefaultResolverFactory.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/arangodb/springframework/core/convert/resolver/DefaultResolverFactory.java b/src/main/java/com/arangodb/springframework/core/convert/resolver/DefaultResolverFactory.java index c08e3321c..bb789de29 100644 --- a/src/main/java/com/arangodb/springframework/core/convert/resolver/DefaultResolverFactory.java +++ b/src/main/java/com/arangodb/springframework/core/convert/resolver/DefaultResolverFactory.java @@ -50,7 +50,7 @@ public Optional> getReferenceResolve return Optional.of((ReferenceResolver) new RefResolver(template.getObject(), transactionBridge.getIfUnique())); } } catch (final Exception e) { - throw new ArangoDBException(e); + throw ArangoDBException.of(e); } return Optional.empty(); } @@ -76,7 +76,7 @@ public Optional> getRelationResolver( return Optional.of((RelationResolver) new RelationsResolver(template.getObject(), transactionBridge.getIfUnique())); } } catch (final Exception e) { - throw new ArangoDBException(e); + throw ArangoDBException.of(e); } return Optional.empty(); } From 7e6f2f3bc2887b4f294f6b322b5ff652b0ed1a9c Mon Sep 17 00:00:00 2001 From: Arne Burmeister Date: Thu, 18 Jul 2024 10:11:36 +0200 Subject: [PATCH 54/55] fix merge errors and unify parameter order with options --- .../core/ArangoOperations.java | 42 +++++++++---------- .../convert/resolver/EdgeFromResolver.java | 5 ++- .../core/convert/resolver/EdgeToResolver.java | 5 ++- .../core/template/ArangoTemplate.java | 6 +-- .../repository/SimpleArangoRepository.java | 20 ++++----- .../transaction/ArangoTransactionManager.java | 7 ++-- 6 files changed, 41 insertions(+), 44 deletions(-) diff --git a/src/main/java/com/arangodb/springframework/core/ArangoOperations.java b/src/main/java/com/arangodb/springframework/core/ArangoOperations.java index fd290d279..dac51ba1c 100644 --- a/src/main/java/com/arangodb/springframework/core/ArangoOperations.java +++ b/src/main/java/com/arangodb/springframework/core/ArangoOperations.java @@ -105,6 +105,8 @@ default ArangoCursor query(String query, Map bindVars, Cl * * @param query * An AQL query string + * @param options + * Additional options that will be passed to the query API, can be null * @param entityClass * The entity type of the result * @return cursor of the results @@ -189,7 +191,9 @@ MultiDocumentEntity> deleteAllById( * @return information about the documents * @throws DataAccessException */ - MultiDocumentEntity> deleteAllById(Iterable ids, Class entityClass) throws DataAccessException; + default MultiDocumentEntity> deleteAllById(Iterable ids, Class entityClass) throws DataAccessException { + return deleteAllById(ids, new DocumentDeleteOptions(), entityClass); + } /** * Deletes the document with the given {@code id} from a collection. @@ -365,12 +369,9 @@ default DocumentUpdateEntity replace(Object id, T value) throws DataAcces /** * Retrieves the document with the given {@code id} from a collection. * - * @param id - * The id or key of the document - * @param entityClass - * The entity class which represents the collection - * @param options - * Additional options, can be null + * @param id The id or key of the document + * @param entityClass The entity class which represents the collection + * @param options Additional options, can be null * @return the document identified by the id * @throws DataAccessException */ @@ -393,31 +394,28 @@ default Optional find(Object id, Class entityClass) throws DataAccessE /** * Retrieves all documents from a collection. * - * @param entityClass - * The entity class which represents the collection + * @param entityClass The entity class which represents the collection * @return the documents * @throws DataAccessException */ - Iterable findAll(Class entityClass, DocumentReadOptions options) throws DataAccessException; + Iterable findAll(DocumentReadOptions options, Class entityClass) throws DataAccessException; default Iterable findAll(Class entityClass) throws DataAccessException { - return findAll(entityClass, new DocumentReadOptions()); + return findAll(new DocumentReadOptions(), entityClass); } /** * Retrieves multiple documents with the given {@code ids} from a collection. * - * @param ids - * The ids or keys of the documents - * @param entityClass - * The entity class which represents the collection + * @param ids The ids or keys of the documents + * @param entityClass The entity class which represents the collection * @return the documents * @throws DataAccessException */ - Iterable findAll(final Iterable ids, final Class entityClass, DocumentReadOptions options) throws DataAccessException; + Iterable findAll(final Iterable ids, DocumentReadOptions options, final Class entityClass) throws DataAccessException; default Iterable findAll(final Iterable ids, final Class entityClass) throws DataAccessException { - return findAll(ids, entityClass, new DocumentReadOptions()); + return findAll(ids, new DocumentReadOptions(), entityClass); } /** @@ -512,17 +510,15 @@ default Iterable repsertAll(Iterable values, Class entityCl /** * Checks whether the document exists by reading a single document head * - * @param id - * The id or key of the document - * @param entityClass - * The entity type representing the collection + * @param id The id or key of the document + * @param entityClass The entity type representing the collection * @return true if the document exists, false if not * @throws DataAccessException */ - boolean exists(Object id, Class entityClass, DocumentExistsOptions options) throws DataAccessException; + boolean exists(Object id, DocumentExistsOptions options, Class entityClass) throws DataAccessException; default boolean exists(Object id, Class entityClass) throws DataAccessException { - return exists(id, entityClass, new DocumentExistsOptions()); + return exists(id, new DocumentExistsOptions(), entityClass); } /** diff --git a/src/main/java/com/arangodb/springframework/core/convert/resolver/EdgeFromResolver.java b/src/main/java/com/arangodb/springframework/core/convert/resolver/EdgeFromResolver.java index c8157611f..71e9e9f3d 100644 --- a/src/main/java/com/arangodb/springframework/core/convert/resolver/EdgeFromResolver.java +++ b/src/main/java/com/arangodb/springframework/core/convert/resolver/EdgeFromResolver.java @@ -34,8 +34,11 @@ */ public class EdgeFromResolver extends AbstractResolver implements RelationResolver { + private final ArangoOperations template; + public EdgeFromResolver(final ArangoOperations template, QueryTransactionBridge transactionBridge) { - super(template, transactionBridge); + super(template.getConverter().getConversionService(), transactionBridge); + this.template = template; } @Override diff --git a/src/main/java/com/arangodb/springframework/core/convert/resolver/EdgeToResolver.java b/src/main/java/com/arangodb/springframework/core/convert/resolver/EdgeToResolver.java index f5d029d20..03d0578b3 100644 --- a/src/main/java/com/arangodb/springframework/core/convert/resolver/EdgeToResolver.java +++ b/src/main/java/com/arangodb/springframework/core/convert/resolver/EdgeToResolver.java @@ -34,8 +34,11 @@ */ public class EdgeToResolver extends AbstractResolver implements RelationResolver { + private final ArangoOperations template; + public EdgeToResolver(final ArangoOperations template, QueryTransactionBridge transactionBridge) { - super(template, transactionBridge); + super(template.getConverter().getConversionService(), transactionBridge); + this.template = template; } @Override diff --git a/src/main/java/com/arangodb/springframework/core/template/ArangoTemplate.java b/src/main/java/com/arangodb/springframework/core/template/ArangoTemplate.java index 086639263..a55d6c7ff 100644 --- a/src/main/java/com/arangodb/springframework/core/template/ArangoTemplate.java +++ b/src/main/java/com/arangodb/springframework/core/template/ArangoTemplate.java @@ -480,14 +480,14 @@ public Optional find(final Object id, final Class entityClass, final D } @Override - public Iterable findAll(final Class entityClass, DocumentReadOptions options) throws DataAccessException { + public Iterable findAll(DocumentReadOptions options, final Class entityClass) throws DataAccessException { final String query = "FOR entity IN @@col RETURN entity"; final Map bindVars = Collections.singletonMap("@col", entityClass); return query(query, bindVars, asQueryOptions(options), entityClass).asListRemaining(); } @Override - public Iterable findAll(final Iterable ids, final Class entityClass, DocumentReadOptions options) + public Iterable findAll(final Iterable ids, DocumentReadOptions options, final Class entityClass) throws DataAccessException { try { final Collection keys = new ArrayList<>(); @@ -678,7 +678,7 @@ private void updateDBFields(final Object value, final DocumentEntity documentEnt } @Override - public boolean exists(final Object id, final Class entityClass, DocumentExistsOptions options) throws DataAccessException { + public boolean exists(final Object id, DocumentExistsOptions options, final Class entityClass) throws DataAccessException { try { boolean transactional = options != null && options.getStreamTransactionId() != null; return _collection(entityClass, transactional).documentExists(determineDocumentKeyFromId(id), options); diff --git a/src/main/java/com/arangodb/springframework/repository/SimpleArangoRepository.java b/src/main/java/com/arangodb/springframework/repository/SimpleArangoRepository.java index 3fd2eaa2d..87f0b5d6b 100644 --- a/src/main/java/com/arangodb/springframework/repository/SimpleArangoRepository.java +++ b/src/main/java/com/arangodb/springframework/repository/SimpleArangoRepository.java @@ -36,8 +36,6 @@ import com.arangodb.springframework.core.template.ArangoTemplate; import com.arangodb.springframework.core.util.AqlUtils; import com.arangodb.springframework.repository.query.QueryTransactionBridge; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.dao.OptimisticLockingFailureException; import org.springframework.data.domain.*; import org.springframework.data.repository.query.FluentQuery; @@ -56,8 +54,6 @@ @SuppressWarnings({ "rawtypes", "unchecked" }) public class SimpleArangoRepository implements ArangoRepository { - private static final Logger LOGGER = LoggerFactory.getLogger(SimpleArangoRepository.class); - private final ArangoTemplate arangoTemplate; private final ArangoConverter converter; private final ArangoMappingContext mappingContext; @@ -108,7 +104,7 @@ public S save(final S entity) { */ @Override public Iterable saveAll(final Iterable entities) { - Iterable saved = arangoTemplate.repsertAll(entities, domainClass, defaultQueryOptions()); + Iterable saved = arangoTemplate.repsertAll(entities, defaultQueryOptions(), domainClass); return returnOriginalEntities ? entities : saved; } @@ -131,7 +127,7 @@ public Optional findById(final ID id) { */ @Override public boolean existsById(final ID id) { - return arangoTemplate.exists(id, domainClass, defaultExistsOptions()); + return arangoTemplate.exists(id, defaultExistsOptions(), domainClass); } /** @@ -141,7 +137,7 @@ public boolean existsById(final ID id) { */ @Override public Iterable findAll() { - return arangoTemplate.findAll(domainClass, defaultReadOptions()); + return arangoTemplate.findAll(defaultReadOptions(), domainClass); } /** @@ -153,7 +149,7 @@ public Iterable findAll() { */ @Override public Iterable findAllById(final Iterable ids) { - return arangoTemplate.findAll(ids, domainClass, defaultReadOptions()); + return arangoTemplate.findAll(ids, defaultReadOptions(), domainClass); } /** @@ -175,7 +171,7 @@ public long count() { @Override public void deleteById(final ID id) { try { - arangoTemplate.delete(id, domainClass, defaultDeleteOptions()); + arangoTemplate.delete(id, defaultDeleteOptions(), domainClass); } catch (DocumentNotFoundException unknown) { // silently ignored } @@ -190,14 +186,14 @@ public void deleteById(final ID id) { @Override public void delete(final T entity) { Object id = persistentEntity.getIdentifierAccessor(entity).getRequiredIdentifier(); - DocumentDeleteOptions opts = new DocumentDeleteOptions(); + DocumentDeleteOptions opts = defaultDeleteOptions(); persistentEntity.getRevProperty() .map(persistentEntity.getPropertyAccessor(entity)::getProperty) .map(r -> converter.convertIfNecessary(r, String.class)) .ifPresent(opts::ifMatch); try { - arangoTemplate.delete(id, opts, domainClass, defaultDeleteOptions()); + arangoTemplate.delete(id, opts, domainClass); } catch (DocumentNotFoundException e) { throw new OptimisticLockingFailureException(e.getMessage(), e); } @@ -208,7 +204,7 @@ public void delete(final T entity) { * @implNote do not add @Override annotation to keep backwards compatibility with spring-data-commons 2.4 */ public void deleteAllById(Iterable ids) { - MultiDocumentEntity> res = arangoTemplate.deleteAllById(ids, domainClass, defaultDeleteOptions()); + MultiDocumentEntity> res = arangoTemplate.deleteAllById(ids, defaultDeleteOptions(), domainClass); for (ErrorEntity error : res.getErrors()) { // Entities that aren't found in the persistence store are silently ignored. if (error.getErrorNum() != 1202) { diff --git a/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManager.java b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManager.java index 52e9ae74f..8a1fe19f6 100644 --- a/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManager.java +++ b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManager.java @@ -114,7 +114,7 @@ protected void doBegin(Object transaction, TransactionDefinition definition) thr /** * Commit the current stream transaction. The query bridge is cleared - * afterwards. + * afterward. * * @see ArangoDatabase#commitStreamTransaction(String) * @see QueryTransactionBridge#clearCurrentTransaction() @@ -147,7 +147,7 @@ protected void doCommit(DefaultTransactionStatus status) throws TransactionExcep /** * Roll back the current stream transaction. The query bridge is cleared - * afterwards. + * afterward. * * @see ArangoDatabase#abortStreamTransaction(String) * @see QueryTransactionBridge#clearCurrentTransaction() @@ -169,7 +169,7 @@ protected void doRollback(DefaultTransactionStatus status) throws TransactionExc /** * Check if the transaction object has the bound holder. For new - * transactions the holder will be bound afterwards. + * transactions the holder will be bound afterward. */ @Override protected boolean isExistingTransaction(Object transaction) throws TransactionException { @@ -189,7 +189,6 @@ protected void doSetRollbackOnly(DefaultTransactionStatus status) throws Transac } /** - * Any transaction object is configured according to the definition upfront. * Bind the holder for the first new transaction created. * * @see ArangoTransactionHolder From 63b05579bd9df4ece659701185472b2cd7a74e06 Mon Sep 17 00:00:00 2001 From: Arne Burmeister Date: Thu, 18 Jul 2024 10:15:05 +0200 Subject: [PATCH 55/55] cleanup warnings --- .../core/template/ArangoTemplate.java | 4 -- .../core/template/CollectionCacheValue.java | 1 + .../repository/SimpleArangoRepository.java | 6 +- .../repository/query/AbstractArangoQuery.java | 3 +- .../repository/query/DerivedArangoQuery.java | 1 - .../query/QueryTransactionBridge.java | 2 +- .../query/StringBasedArangoQuery.java | 2 +- .../query/derived/DerivedQueryCreator.java | 57 +++++++++---------- .../transaction/ArangoTransactionManager.java | 3 +- .../TransactionAttributeTemplate.java | 3 +- 10 files changed, 34 insertions(+), 48 deletions(-) diff --git a/src/main/java/com/arangodb/springframework/core/template/ArangoTemplate.java b/src/main/java/com/arangodb/springframework/core/template/ArangoTemplate.java index a55d6c7ff..508d6c425 100644 --- a/src/main/java/com/arangodb/springframework/core/template/ArangoTemplate.java +++ b/src/main/java/com/arangodb/springframework/core/template/ArangoTemplate.java @@ -133,10 +133,6 @@ public ArangoDatabase db() throws DataAccessException { }); } - private ArangoCollection _collection(final String name, boolean transactional) { - return _collection(name, null, null, transactional); - } - private ArangoCollection _collection(final Class entityClass, boolean transactional) { return _collection(entityClass, null, transactional); } diff --git a/src/main/java/com/arangodb/springframework/core/template/CollectionCacheValue.java b/src/main/java/com/arangodb/springframework/core/template/CollectionCacheValue.java index bbaece017..5a8e4ebb8 100644 --- a/src/main/java/com/arangodb/springframework/core/template/CollectionCacheValue.java +++ b/src/main/java/com/arangodb/springframework/core/template/CollectionCacheValue.java @@ -30,6 +30,7 @@ public Collection> getEntities() { public Collection getIndexes() { return indexes; } + public boolean addEntityClass(final Class entityClass) { return entities.add(entityClass); } diff --git a/src/main/java/com/arangodb/springframework/repository/SimpleArangoRepository.java b/src/main/java/com/arangodb/springframework/repository/SimpleArangoRepository.java index 87f0b5d6b..ae0902cec 100644 --- a/src/main/java/com/arangodb/springframework/repository/SimpleArangoRepository.java +++ b/src/main/java/com/arangodb/springframework/repository/SimpleArangoRepository.java @@ -257,10 +257,6 @@ public Iterator iterator() { */ @Override public Page findAll(final Pageable pageable) { - if (pageable == null) { - LOGGER.debug("Pageable in findAll(Pageable) is null"); - } - final ArangoCursor result = findAllInternal(pageable, null, new HashMap<>()); final List content = result.asListRemaining(); return new PageImpl<>(content, pageable, ((Number) result.getStats().getFullCount()).longValue()); @@ -342,7 +338,7 @@ public long count(final Example example) { final Map bindVars = new HashMap<>(); bindVars.put("@col", getCollectionName()); final String predicate = exampleConverter.convertExampleToPredicate(example, bindVars); - final String filter = predicate.length() == 0 ? "" : " FILTER " + predicate; + final String filter = predicate.isEmpty() ? "" : " FILTER " + predicate; final String query = String.format("FOR e IN @@col %s COLLECT WITH COUNT INTO length RETURN length", filter); arangoTemplate.collection(domainClass); final ArangoCursor cursor = arangoTemplate.query(query, bindVars, defaultQueryOptions(), Long.class); diff --git a/src/main/java/com/arangodb/springframework/repository/query/AbstractArangoQuery.java b/src/main/java/com/arangodb/springframework/repository/query/AbstractArangoQuery.java index 56647bfd7..01c78f8c6 100644 --- a/src/main/java/com/arangodb/springframework/repository/query/AbstractArangoQuery.java +++ b/src/main/java/com/arangodb/springframework/repository/query/AbstractArangoQuery.java @@ -20,7 +20,6 @@ package com.arangodb.springframework.repository.query; -import java.util.Collection; import java.util.HashMap; import java.util.Map; @@ -94,7 +93,7 @@ public Object execute(final Object[] parameters) { private void logWarningsIfNecessary(final ArangoCursor result) { result.getWarnings().forEach(warning -> { - LOGGER.warn("Query warning at [" + method + "]: " + warning.getCode() + " - " + warning.getMessage()); + LOGGER.warn("Query warning at [{}]: {} - {}", method, warning.getCode(), warning.getMessage()); }); } diff --git a/src/main/java/com/arangodb/springframework/repository/query/DerivedArangoQuery.java b/src/main/java/com/arangodb/springframework/repository/query/DerivedArangoQuery.java index f36dfb936..d970c5d47 100644 --- a/src/main/java/com/arangodb/springframework/repository/query/DerivedArangoQuery.java +++ b/src/main/java/com/arangodb/springframework/repository/query/DerivedArangoQuery.java @@ -28,7 +28,6 @@ import com.arangodb.springframework.repository.query.derived.DerivedQueryCreator; import org.springframework.data.repository.query.parser.PartTree; -import java.util.Collection; import java.util.LinkedList; import java.util.List; import java.util.Map; diff --git a/src/main/java/com/arangodb/springframework/repository/query/QueryTransactionBridge.java b/src/main/java/com/arangodb/springframework/repository/query/QueryTransactionBridge.java index 621b9e297..c22fca964 100644 --- a/src/main/java/com/arangodb/springframework/repository/query/QueryTransactionBridge.java +++ b/src/main/java/com/arangodb/springframework/repository/query/QueryTransactionBridge.java @@ -33,7 +33,7 @@ */ public class QueryTransactionBridge { - private static final ThreadLocal, String>> CURRENT_TRANSACTION = new NamedInheritableThreadLocal, String>>("ArangoTransactionBegin") { + private static final ThreadLocal, String>> CURRENT_TRANSACTION = new NamedInheritableThreadLocal<>("ArangoTransactionBegin") { @Override protected Function, String> initialValue() { return any -> null; diff --git a/src/main/java/com/arangodb/springframework/repository/query/StringBasedArangoQuery.java b/src/main/java/com/arangodb/springframework/repository/query/StringBasedArangoQuery.java index 07cd9b501..bacf9c113 100644 --- a/src/main/java/com/arangodb/springframework/repository/query/StringBasedArangoQuery.java +++ b/src/main/java/com/arangodb/springframework/repository/query/StringBasedArangoQuery.java @@ -171,7 +171,7 @@ private void extractBindVars(final ArangoParameterAccessor accessor, final Map bindVars.put(name, value)); } else { final String key = String.valueOf(param.getIndex()); final String collectionKey = "@" + key; diff --git a/src/main/java/com/arangodb/springframework/repository/query/derived/DerivedQueryCreator.java b/src/main/java/com/arangodb/springframework/repository/query/derived/DerivedQueryCreator.java index 1cc8cfa03..d6e8c8b8b 100644 --- a/src/main/java/com/arangodb/springframework/repository/query/derived/DerivedQueryCreator.java +++ b/src/main/java/com/arangodb/springframework/repository/query/derived/DerivedQueryCreator.java @@ -46,7 +46,6 @@ import org.springframework.util.Assert; import java.util.*; -import java.util.stream.Collectors; /** * Creates a full AQL query from a PartTree and ArangoParameterAccessor @@ -89,7 +88,7 @@ public DerivedQueryCreator( super(tree, accessor); this.context = context; this.domainClass = domainClass; - collectionName = AqlUtils.buildCollectionName(context.getPersistentEntity(domainClass).getCollection()); + collectionName = AqlUtils.buildCollectionName(context.getRequiredPersistentEntity(domainClass).getCollection()); this.tree = tree; this.accessor = accessor; this.geoFields = geoFields; @@ -127,7 +126,7 @@ protected QueryWithCollections complete(final Criteria criteria, final Sort sort } final StringBuilder query = new StringBuilder(); - final String with = withCollections.stream().collect(Collectors.joining(", ")); + final String with = String.join(", ", withCollections); if (!with.isEmpty()) { query.append("WITH ").append(with).append(" "); } @@ -160,7 +159,7 @@ protected QueryWithCollections complete(final Criteria criteria, final Sort sort if (sort.isUnsorted()) { sortString = distanceSortKey; } else { - sortString = distanceSortKey + ", " + sortString.substring(5, sortString.length()); + sortString = distanceSortKey + ", " + sortString.substring(5); } } query.append(sortString); @@ -170,7 +169,7 @@ protected QueryWithCollections complete(final Criteria criteria, final Sort sort } final Pageable pageable = accessor.getPageable(); - if (pageable != null && pageable.isPaged()) { + if (pageable.isPaged()) { query.append(" ").append(AqlUtils.buildLimitClause(pageable)); } if (tree.isDelete()) { @@ -261,7 +260,7 @@ private String[] createPredicateTemplateAndPropertyString(final Part part) { --propertiesLeft; final ArangoPersistentProperty property = (ArangoPersistentProperty) object; if (propertiesLeft == 0) { - simpleProperties.append("." + AqlUtils.buildFieldName(property.getFieldName())); + simpleProperties.append(".").append(AqlUtils.buildFieldName(property.getFieldName())); break; } if (property.getRelations().isPresent()) { @@ -274,11 +273,11 @@ private String[] createPredicateTemplateAndPropertyString(final Part part) { final Class[] edgeClasses = relations.edges(); final StringBuilder edgesBuilder = new StringBuilder(); for (final Class edge : edgeClasses) { - String collection = context.getPersistentEntity(edge).getCollection(); + String collection = context.getRequiredPersistentEntity(edge).getCollection(); if (collection.split("-").length > 1) { collection = "`" + collection + "`"; } - edgesBuilder.append((edgesBuilder.length() == 0 ? "" : ", ") + collection); + edgesBuilder.append(edgesBuilder.isEmpty() ? "" : ", ").append(collection); } final String prevEntity = "e" + (varsUsed == 0 ? "" : Integer.toString(varsUsed)); final String entity = "e" + Integer.toString(++varsUsed); @@ -286,22 +285,22 @@ private String[] createPredicateTemplateAndPropertyString(final Part part) { simpleProperties = new StringBuilder(); final String iteration = format(TEMPLATE, entity, depths, direction, prevEntity, nested, edges); final String predicate = format(PREDICATE_TEMPLATE, iteration); - predicateTemplate = predicateTemplate.length() == 0 ? predicate : format(predicateTemplate, predicate); + predicateTemplate = predicateTemplate.isEmpty() ? predicate : format(predicateTemplate, predicate); } else if (property.isCollectionLike()) { if (property.getRef().isPresent()) { // collection of references final String TEMPLATE = "FOR %s IN %s FILTER %s._id IN %s%s"; final String prevEntity = "e" + (varsUsed == 0 ? "" : Integer.toString(varsUsed)); final String entity = "e" + Integer.toString(++varsUsed); - String collection = context.getPersistentEntity(property.getComponentType()).getCollection(); + String collection = context.getRequiredPersistentEntity(property).getCollection(); if (collection.split("-").length > 1) { collection = "`" + collection + "`"; } - final String name = simpleProperties.toString() + "." + AqlUtils.buildFieldName(property.getFieldName()); + final String name = simpleProperties + "." + AqlUtils.buildFieldName(property.getFieldName()); simpleProperties = new StringBuilder(); final String iteration = format(TEMPLATE, entity, collection, entity, prevEntity, name); final String predicate = format(PREDICATE_TEMPLATE, iteration); - predicateTemplate = predicateTemplate.length() == 0 ? predicate + predicateTemplate = predicateTemplate.isEmpty() ? predicate : format(predicateTemplate, predicate); } else { // collection @@ -312,7 +311,7 @@ private String[] createPredicateTemplateAndPropertyString(final Part part) { simpleProperties = new StringBuilder(); final String iteration = format(TEMPLATE, entity, prevEntity, name); final String predicate = format(PREDICATE_TEMPLATE, iteration); - predicateTemplate = predicateTemplate.length() == 0 ? predicate + predicateTemplate = predicateTemplate.isEmpty() ? predicate : format(predicateTemplate, predicate); } } else { @@ -320,20 +319,20 @@ private String[] createPredicateTemplateAndPropertyString(final Part part) { // single reference final String TEMPLATE = "FOR %s IN %s FILTER %s._id == %s%s"; final String prevEntity = "e" + (varsUsed == 0 ? "" : Integer.toString(varsUsed)); - final String entity = "e" + Integer.toString(++varsUsed); - String collection = context.getPersistentEntity(property.getType()).getCollection(); + final String entity = "e" + ++varsUsed; + String collection = context.getRequiredPersistentEntity(property).getCollection(); if (collection.split("-").length > 1) { collection = "`" + collection + "`"; } - final String name = simpleProperties.toString() + "." + AqlUtils.buildFieldName(property.getFieldName()); + final String name = simpleProperties + "." + AqlUtils.buildFieldName(property.getFieldName()); simpleProperties = new StringBuilder(); final String iteration = format(TEMPLATE, entity, collection, entity, prevEntity, name); final String predicate = format(PREDICATE_TEMPLATE, iteration); - predicateTemplate = predicateTemplate.length() == 0 ? predicate + predicateTemplate = predicateTemplate.isEmpty() ? predicate : format(predicateTemplate, predicate); } else { // simple property - simpleProperties.append("." + AqlUtils.buildFieldName(property.getFieldName())); + simpleProperties.append(".").append(AqlUtils.buildFieldName(property.getFieldName())); } } } @@ -385,7 +384,7 @@ private void checkUniquePoint(final Point point) { * @param part */ private void checkUniqueLocation(final Part part) { - isUnique = isUnique == null ? true : isUnique; + isUnique = isUnique == null || isUnique; isUnique = (uniqueLocation == null || uniqueLocation.equals(ignorePropertyCase(part))) ? isUnique : false; if (!geoFields.isEmpty()) { Assert.isTrue(isUnique, "Different location fields are used - Distance is ambiguous"); @@ -398,8 +397,7 @@ private Criteria createCriteria(final Part part, final Iterator iterator final String[] templateAndProperty = createPredicateTemplateAndPropertyString(part); final String template = templateAndProperty[0]; final String property = templateAndProperty[1]; - Criteria criteria = null; - final boolean checkUnique = part.getProperty().toDotPath().split(".").length <= 1; + final boolean checkUnique = part.getProperty().toDotPath().split("\\.").length <= 1; Class type = part.getProperty().getType(); // whether the current field type is a type encoded as geoJson @@ -410,6 +408,7 @@ private Criteria createCriteria(final Part part, final Iterator iterator hasGeoJsonType = true; } + Criteria criteria = null; switch (part.getType()) { case SIMPLE_PROPERTY: criteria = Criteria.eql(ignorePropertyCase(part, property), bind(part, iterator)); @@ -431,7 +430,7 @@ private Criteria createCriteria(final Part part, final Iterator iterator break; case EXISTS: final String document = property.substring(0, property.lastIndexOf(".")); - final String attribute = property.substring(property.lastIndexOf(".") + 1, property.length()); + final String attribute = property.substring(property.lastIndexOf(".") + 1); criteria = Criteria.exists(document, attribute); break; case BEFORE: @@ -492,10 +491,9 @@ private Criteria createCriteria(final Part part, final Iterator iterator if (nearValue instanceof Point point) { checkUniquePoint(point); } else { - bindingCounter = binding.bind(nearValue, shouldIgnoreCase(part), null, point -> checkUniquePoint(point), + bindingCounter = binding.bind(nearValue, shouldIgnoreCase(part), null, this::checkUniquePoint, bindingCounter); } - criteria = null; break; case WITHIN: if (checkUnique) { @@ -588,24 +586,24 @@ private int bind(final Part part, final Iterator iterator, final Boolean private int bind(final Part part, final Object value, final Boolean borderStatus) { final int index = bindingCounter; - bindingCounter = binding.bind(value, shouldIgnoreCase(part), borderStatus, point -> checkUniquePoint(point), + bindingCounter = binding.bind(value, shouldIgnoreCase(part), borderStatus, this::checkUniquePoint, bindingCounter); return index; } private int bind(final Object value) { final int index = bindingCounter; - bindingCounter = binding.bind(value, false, null, point -> checkUniquePoint(point), bindingCounter); + bindingCounter = binding.bind(value, false, null, this::checkUniquePoint, bindingCounter); return index; } private void bindPoint(final Part part, final Object value, final boolean toGeoJson) { - bindingCounter = binding.bindPoint(value, shouldIgnoreCase(part), point -> checkUniquePoint(point), + bindingCounter = binding.bindPoint(value, shouldIgnoreCase(part), this::checkUniquePoint, bindingCounter, toGeoJson); } private void bindCircle(final Part part, final Object value, final boolean toGeoJson) { - bindingCounter = binding.bindCircle(value, shouldIgnoreCase(part), point -> checkUniquePoint(point), + bindingCounter = binding.bindCircle(value, shouldIgnoreCase(part), this::checkUniquePoint, bindingCounter, toGeoJson); } @@ -614,7 +612,7 @@ private void bindRange(final Part part, final Object value) { } private void bindRing(final Part part, final Object value, final boolean toGeoJson) { - bindingCounter = binding.bindRing(value, shouldIgnoreCase(part), point -> checkUniquePoint(point), + bindingCounter = binding.bindRing(value, shouldIgnoreCase(part), this::checkUniquePoint, bindingCounter, toGeoJson); } @@ -632,7 +630,6 @@ private void collectWithCollections(final PropertyPath propertyPath) { propertyPath.stream() .filter(property -> { ArangoPersistentProperty p = context.getPersistentPropertyPath(property).getBaseProperty(); - if (p == null) return false; Optional ref = p.getRef(); Optional rels = p.getRelations(); return ref.isPresent() || rels.isPresent(); diff --git a/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManager.java b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManager.java index 8a1fe19f6..324be60d6 100644 --- a/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManager.java +++ b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManager.java @@ -26,7 +26,6 @@ import com.arangodb.springframework.core.template.CollectionCallback; import com.arangodb.springframework.repository.query.QueryTransactionBridge; import org.springframework.beans.factory.InitializingBean; -import org.springframework.lang.Nullable; import org.springframework.transaction.*; import org.springframework.transaction.support.AbstractPlatformTransactionManager; import org.springframework.transaction.support.DefaultTransactionStatus; @@ -80,7 +79,7 @@ public void afterPropertiesSet() { * Creates a new transaction object. Any holder bound will be reused. */ @Override - protected ArangoTransactionObject doGetTransaction() { + protected Object doGetTransaction() { ArangoTransactionHolder holder = (ArangoTransactionHolder) TransactionSynchronizationManager.getResource(this); try { return new ArangoTransactionObject(operations.db(), CollectionCallback.fromOperations(operations), getDefaultTimeout(), holder); diff --git a/src/main/java/com/arangodb/springframework/transaction/TransactionAttributeTemplate.java b/src/main/java/com/arangodb/springframework/transaction/TransactionAttributeTemplate.java index 4f75c851e..0f7afb3b6 100644 --- a/src/main/java/com/arangodb/springframework/transaction/TransactionAttributeTemplate.java +++ b/src/main/java/com/arangodb/springframework/transaction/TransactionAttributeTemplate.java @@ -54,8 +54,7 @@ public TransactionAttributeTemplate(PlatformTransactionManager transactionManage public TransactionAttributeTemplate(PlatformTransactionManager transactionManager, TransactionDefinition transactionDefinition) { super(transactionManager, transactionDefinition); - if (transactionDefinition instanceof TransactionAttribute) { - TransactionAttribute transactionAttribute = (TransactionAttribute) transactionDefinition; + if (transactionDefinition instanceof TransactionAttribute transactionAttribute) { setQualifier(transactionAttribute.getQualifier()); setLabels(transactionAttribute.getLabels()); setRollbackOn(transactionAttribute::rollbackOn);