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/config/ArangoConfiguration.java b/src/main/java/com/arangodb/springframework/config/ArangoConfiguration.java index 71974a951..724d79d37 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,8 @@ 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()); - } - } catch (final Exception e) { - throw new ArangoDBException(e); - } - return Optional.ofNullable(resolver); - } - - @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()); - } else if (collectionType == Document.class) { - resolver = (RelationResolver) new DocumentFromResolver(arangoTemplate()); - } - } else if (annotation instanceof To) { - if (collectionType == Edge.class) { - resolver = (RelationResolver) new EdgeToResolver(arangoTemplate()); - } else if (collectionType == Document.class) { - resolver = (RelationResolver) new DocumentToResolver(arangoTemplate()); - } - } else if (annotation instanceof Relations) { - resolver = (RelationResolver) new RelationsResolver(arangoTemplate()); - } - } catch (final Exception e) { - throw new ArangoDBException(e); - } - return Optional.ofNullable(resolver); - } - }; + return new DefaultResolverFactory(); } - } \ No newline at end of file diff --git a/src/main/java/com/arangodb/springframework/core/ArangoOperations.java b/src/main/java/com/arangodb/springframework/core/ArangoOperations.java index 57d71cc28..dac51ba1c 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,6 +55,14 @@ public interface ArangoOperations { */ ArangoDBVersion getVersion() throws DataAccessException; + /** + * 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 * {@code ArangoCursor} instance for the result list. @@ -85,12 +94,14 @@ 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 - * 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 @@ -101,11 +112,14 @@ ArangoCursor query(String query, Map bindVars, Class e * @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 @@ -114,7 +128,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. @@ -143,7 +159,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. @@ -172,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. @@ -198,7 +219,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 @@ -238,7 +261,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 @@ -268,7 +294,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 @@ -303,8 +331,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 @@ -332,17 +362,16 @@ 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. * - * @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 */ @@ -358,29 +387,36 @@ 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. * - * @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) throws DataAccessException; + Iterable findAll(DocumentReadOptions options, Class entityClass) throws DataAccessException; + + default Iterable findAll(Class entityClass) throws DataAccessException { + 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) 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, new DocumentReadOptions(), entityClass); + } /** * Creates new documents from the given documents, unless there is already a document with the _key given. If no @@ -413,8 +449,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 @@ -436,8 +474,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. @@ -447,32 +486,40 @@ MultiDocumentEntity> insertAll(Iterable * @throws DataAccessException * @since ArangoDB 3.4 */ - T repsert(T value) throws DataAccessException; + T repsert(T value, AqlQueryOptions options) throws DataAccessException; + + default T repsert(T value) throws DataAccessException { + return repsert(value, new AqlQueryOptions()); + } /** * 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) throws DataAccessException; + Iterable repsertAll(Iterable values, AqlQueryOptions options, Class entityClass) throws DataAccessException; + + default Iterable repsertAll(Iterable values, Class entityClass) throws DataAccessException { + return repsertAll(values, new AqlQueryOptions(), entityClass); + } /** * 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) throws DataAccessException; + boolean exists(Object id, DocumentExistsOptions options, Class entityClass) throws DataAccessException; + + default boolean exists(Object id, Class entityClass) throws DataAccessException { + return exists(id, new DocumentExistsOptions(), entityClass); + } /** * Drop an existing database @@ -501,7 +548,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/convert/resolver/AbstractResolver.java b/src/main/java/com/arangodb/springframework/core/convert/resolver/AbstractResolver.java index a98372dcd..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); } @@ -93,6 +99,22 @@ private Class enhancedTypeFor(final Class type) { return enhancer.createClass(); } + 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; + } + 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/DefaultResolverFactory.java b/src/main/java/com/arangodb/springframework/core/convert/resolver/DefaultResolverFactory.java new file mode 100644 index 000000000..bb789de29 --- /dev/null +++ b/src/main/java/com/arangodb/springframework/core/convert/resolver/DefaultResolverFactory.java @@ -0,0 +1,89 @@ +/* + * 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; +import java.util.Optional; + +import com.arangodb.ArangoDBException; +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.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) { + try { + if (annotation instanceof Ref) { + return Optional.of((ReferenceResolver) new RefResolver(template.getObject(), transactionBridge.getIfUnique())); + } + } catch (final Exception e) { + throw ArangoDBException.of(e); + } + return Optional.empty(); + } + + @SuppressWarnings("unchecked") + @Override + public Optional> getRelationResolver(final A annotation, + final Class collectionType) { + try { + if (annotation instanceof From) { + if (collectionType == Edge.class) { + return Optional.of((RelationResolver) new EdgeFromResolver(template.getObject(), transactionBridge.getIfUnique())); + } + return Optional.of((RelationResolver) new DocumentFromResolver(template.getObject(), transactionBridge.getIfUnique())); + } + if (annotation instanceof To) { + if (collectionType == Edge.class) { + return Optional.of((RelationResolver) new EdgeToResolver(template.getObject(), transactionBridge.getIfUnique())); + } + 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 ArangoDBException.of(e); + } + return Optional.empty(); + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + template = applicationContext.getBeanProvider(ArangoOperations.class); + transactionBridge = applicationContext.getBeanProvider(QueryTransactionBridge.class); + } +} 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..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,10 +20,10 @@ package com.arangodb.springframework.core.convert.resolver; +import com.arangodb.springframework.repository.query.QueryTransactionBridge; 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; @@ -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; + public DocumentFromResolver(final ArangoOperations template, QueryTransactionBridge transactionBridge) { + super(template.getConverter().getConversionService(), transactionBridge); + this.template = template; } @Override @@ -73,7 +73,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 e8440a4e3..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,10 +20,10 @@ package com.arangodb.springframework.core.convert.resolver; +import com.arangodb.springframework.repository.query.QueryTransactionBridge; 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; @@ -38,10 +38,10 @@ */ public class DocumentToResolver extends AbstractResolver implements RelationResolver { - private final ArangoOperations template; + private final ArangoOperations template; - public DocumentToResolver(final ArangoOperations template) { - super(template.getConverter().getConversionService()); + public DocumentToResolver(final ArangoOperations template, QueryTransactionBridge transactionBridge) { + super(template.getConverter().getConversionService(), transactionBridge); this.template = template; } @@ -73,7 +73,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 a1dcc0837..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 @@ -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; @@ -35,8 +36,8 @@ public class EdgeFromResolver extends AbstractResolver implements RelationResolv private final ArangoOperations template; - public EdgeFromResolver(final ArangoOperations template) { - super(template.getConverter().getConversionService()); + public EdgeFromResolver(final ArangoOperations template, QueryTransactionBridge transactionBridge) { + super(template.getConverter().getConversionService(), transactionBridge); this.template = template; } @@ -48,7 +49,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 69e92369a..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 @@ -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; @@ -35,8 +36,8 @@ public class EdgeToResolver extends AbstractResolver implements RelationResolver private final ArangoOperations template; - public EdgeToResolver(final ArangoOperations template) { - super(template.getConverter().getConversionService()); + public EdgeToResolver(final ArangoOperations template, QueryTransactionBridge transactionBridge) { + super(template.getConverter().getConversionService(), transactionBridge); this.template = template; } @@ -48,7 +49,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 6dfc80586..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; @@ -37,10 +38,10 @@ */ public class RefResolver extends AbstractResolver implements ReferenceResolver { - private final ArangoOperations template; + private final ArangoOperations template; - public RefResolver(final ArangoOperations template) { - super(template.getConverter().getConversionService()); + public RefResolver(final ArangoOperations template, QueryTransactionBridge transactionBridge) { + super(template.getConverter().getConversionService(), transactionBridge); this.template = template; } @@ -58,7 +59,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 7a69c8929..756ecbbda 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 @@ -24,6 +24,7 @@ import java.util.function.Supplier; import java.util.stream.Collectors; +import com.arangodb.springframework.repository.query.QueryTransactionBridge; import org.springframework.data.util.TypeInformation; import com.arangodb.ArangoCursor; @@ -36,10 +37,10 @@ */ public class RelationsResolver extends AbstractResolver implements RelationResolver { - private final ArangoOperations template; + private final ArangoOperations template; - public RelationsResolver(final ArangoOperations template) { - super(template.getConverter().getConversionService()); + public RelationsResolver(final ArangoOperations template, QueryTransactionBridge transactionBridge) { + super(template.getConverter().getConversionService(), transactionBridge); this.template = template; } @@ -108,7 +109,7 @@ private ArangoCursor _resolve( edges, // limit ? "LIMIT 1" : ""); - return template.query(query, bindVars, type); + return template.query(query, bindVars, defaultQueryOptions(), type); } } 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..508d6c425 100644 --- a/src/main/java/com/arangodb/springframework/core/template/ArangoTemplate.java +++ b/src/main/java/com/arangodb/springframework/core/template/ArangoTemplate.java @@ -35,10 +35,11 @@ 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; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; @@ -62,6 +63,7 @@ * @author Mark Vollmary * @author Christian Lechner * @author Reşat SABIQ + * @author Arne Burmeister */ public class ArangoTemplate implements ArangoOperations, CollectionCallback, ApplicationContextAware { @@ -76,6 +78,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; @@ -113,35 +116,36 @@ 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; + @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); + } }); } - private ArangoCollection _collection(final String name) { - return _collection(name, null, null); + private ArangoCollection _collection(final Class entityClass, boolean transactional) { + return _collection(entityClass, null, transactional); } - private ArangoCollection _collection(final Class entityClass) { - return _collection(entityClass, null); - } - - 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; @@ -149,86 +153,123 @@ private ArangoCollection _collection(final String name, final ArangoPersistentEn key -> { final ArangoCollection collection = db.collection(name); if (!collection.exists()) { - + if (transactional) { + LOGGER.debug("Creating collection {} during transaction", name); + } 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); - ensureCollectionIndexes(collection(collection), persistentEntity); - } + 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, value.getIndexes()); + } + } return collection; } @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() + 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)); + 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 (createNewIndex(IndexType.persistent, fields, existing)) { + existing.add(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 (createNewIndex(IndexType.persistent, fields, existing)) { + existing.add(collection.ensurePersistentIndex(fields, options)); + } + } - 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 (createNewIndex(IndexType.geo, fields, existing)) { + existing.add(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 (createNewIndex(IndexType.geo, fields, existing)) { + existing.add(collection.ensureGeoIndex(fields, options)); + } + } @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 (createNewIndex(IndexType.fulltext, fields, existing)) { + existing.add(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 (createNewIndex(IndexType.fulltext, fields, existing)) { + existing.add(collection.ensureFulltextIndex(fields, options)); + } + } - 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 (createNewIndex(IndexType.ttl, fields, existing)) { + existing.add(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 (createNewIndex(IndexType.ttl, fields, existing)) { + existing.add(collection.ensureTtlIndex(fields, options)); + } + } + + 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 Optional determineCollectionFromId(final Object id) { return id != null ? Optional.ofNullable(MetadataUtils.determineCollectionFromId(converter.convertId(id))) @@ -260,39 +301,23 @@ public ArangoDBVersion getVersion() throws DataAccessException { } } - @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 { - return query(query, null, options, entityClass); - } - @Override 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); + 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); } } - 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()); } @@ -311,7 +336,8 @@ public MultiDocumentEntity> deleteAll( MultiDocumentEntity> result; try { - result = _collection(entityClass).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); } @@ -320,15 +346,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) { @@ -344,12 +361,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 { @@ -358,7 +369,8 @@ public DocumentDeleteEntity delete(final Object id, final DocumentDeleteO DocumentDeleteEntity result; try { - result = _collection(entityClass, id).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); } @@ -367,11 +379,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, @@ -383,7 +390,8 @@ public MultiDocumentEntity> updateAll( MultiDocumentEntity> result; try { - result = _collection(entityClass).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); } @@ -393,15 +401,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 { @@ -410,7 +409,8 @@ public DocumentUpdateEntity update(final Object id, final T value, final DocumentUpdateEntity result; try { - result = _collection(value.getClass(), id).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); } @@ -420,11 +420,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, @@ -436,7 +431,8 @@ public MultiDocumentEntity> replaceAll( MultiDocumentEntity> result; try { - result = _collection(entityClass).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); } @@ -446,15 +442,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 { @@ -462,7 +449,8 @@ public DocumentUpdateEntity replace(final Object id, final T value, final DocumentUpdateEntity result; try { - result = _collection(value.getClass(), id).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); } @@ -472,16 +460,12 @@ 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 { try { - T res = _collection(entityClass, id).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)); } @@ -492,24 +476,20 @@ 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(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, 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, DocumentReadOptions options, final Class entityClass) throws DataAccessException { try { final Collection keys = new ArrayList<>(); ids.forEach(id -> keys.add(determineDocumentKeyFromId(id))); - Collection docs = _collection(entityClass).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)); @@ -529,7 +509,8 @@ public MultiDocumentEntity> insertAll( MultiDocumentEntity> result; try { - result = _collection(entityClass).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); } @@ -539,19 +520,14 @@ 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)); DocumentCreateEntity result; try { - result = _collection(value.getClass()).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); } @@ -562,14 +538,10 @@ 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(); + boolean transactional = options != null && options.getStreamTransactionId() != null; + final String collectionName = _collection(clazz, transactional).name(); potentiallyEmitEvent(new BeforeSaveEvent<>(value)); @@ -582,7 +554,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) { @@ -594,26 +566,28 @@ public T repsert(final T value) throws DataAccessException { return result; } - @SuppressWarnings({"rawtypes", "unchecked"}) + @SuppressWarnings("unchecked") @Override - public Iterable repsertAll(final Iterable values, final Class entityClass) throws DataAccessException { + public Iterable repsertAll(final Iterable values, AqlQueryOptions options, final Class entityClass) throws DataAccessException { if (!values.iterator().hasNext()) { return Collections.emptyList(); } - final String collectionName = _collection(entityClass).name(); + boolean transactional = options != null && options.getStreamTransactionId() != null; + final String collectionName = _collection(entityClass, transactional).name(); potentiallyEmitBeforeSaveEvent(values); Map bindVars = new HashMap<>(); bindVars.put("@col", collectionName); bindVars.put("docs", values); + @SuppressWarnings("rawtypes") List result; try { result = query( REPSERT_MANY_QUERY, bindVars, - entityClass + options, entityClass ).asListRemaining(); } catch (final ArangoDBException e) { throw translateException(e); @@ -700,9 +674,10 @@ private void updateDBFields(final Object value, final DocumentEntity documentEnt } @Override - public boolean exists(final Object id, final Class entityClass) throws DataAccessException { + public boolean exists(final Object id, DocumentExistsOptions options, final Class entityClass) throws DataAccessException { try { - return _collection(entityClass).documentExists(determineDocumentKeyFromId(id)); + boolean transactional = options != null && options.getStreamTransactionId() != null; + return _collection(entityClass, transactional).documentExists(determineDocumentKeyFromId(id), options); } catch (final ArangoDBException e) { throw translateException(e); } @@ -723,18 +698,18 @@ 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) throws DataAccessException { - return collection(_collection(name)); + public CollectionOperations collection(String name) throws DataAccessException { + return ArangoOperations.super.collection(name); } @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) { @@ -826,4 +801,8 @@ private List toList(Iterable it) { it.forEach(l::add); return l; } + + private static AqlQueryOptions asQueryOptions(DocumentReadOptions source) { + return new AqlQueryOptions().streamTransactionId(source.getStreamTransactionId()).allowDirtyRead(source.getAllowDirtyRead()); + } } 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..5a8e4ebb8 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,12 @@ 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 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; diff --git a/src/main/java/com/arangodb/springframework/repository/ArangoRepositoryFactory.java b/src/main/java/com/arangodb/springframework/repository/ArangoRepositoryFactory.java index dc54b6511..787249c0e 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(); } @@ -83,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; @@ -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..bb5ba8491 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(required = false) + 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/SimpleArangoRepository.java b/src/main/java/com/arangodb/springframework/repository/SimpleArangoRepository.java index 2ccc1c0a2..ae0902cec 100644 --- a/src/main/java/com/arangodb/springframework/repository/SimpleArangoRepository.java +++ b/src/main/java/com/arangodb/springframework/repository/SimpleArangoRepository.java @@ -27,14 +27,15 @@ 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; import com.arangodb.springframework.core.mapping.ArangoPersistentEntity; import com.arangodb.springframework.core.template.ArangoTemplate; import com.arangodb.springframework.core.util.AqlUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import com.arangodb.springframework.repository.query.QueryTransactionBridge; import org.springframework.dao.OptimisticLockingFailureException; import org.springframework.data.domain.*; import org.springframework.data.repository.query.FluentQuery; @@ -53,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; @@ -62,6 +61,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 +69,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()); @@ -89,7 +91,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; } @@ -102,7 +104,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, defaultQueryOptions(), domainClass); return returnOriginalEntities ? entities : saved; } @@ -114,7 +116,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()); } /** @@ -125,7 +127,7 @@ public Optional findById(final ID id) { */ @Override public boolean existsById(final ID id) { - return arangoTemplate.exists(id, domainClass); + return arangoTemplate.exists(id, defaultExistsOptions(), domainClass); } /** @@ -135,7 +137,7 @@ public boolean existsById(final ID id) { */ @Override public Iterable findAll() { - return arangoTemplate.findAll(domainClass); + return arangoTemplate.findAll(defaultReadOptions(), domainClass); } /** @@ -147,7 +149,7 @@ public Iterable findAll() { */ @Override public Iterable findAllById(final Iterable ids) { - return arangoTemplate.findAll(ids, domainClass); + return arangoTemplate.findAll(ids, defaultReadOptions(), domainClass); } /** @@ -169,7 +171,7 @@ public long count() { @Override public void deleteById(final ID id) { try { - arangoTemplate.delete(id, domainClass); + arangoTemplate.delete(id, defaultDeleteOptions(), domainClass); } catch (DocumentNotFoundException unknown) { // silently ignored } @@ -184,7 +186,7 @@ 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)) @@ -202,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); + 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) { @@ -255,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()); @@ -295,7 +293,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<>()); } /** @@ -340,10 +338,10 @@ 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, null, Long.class); + final ArangoCursor cursor = arangoTemplate.query(query, bindVars, defaultQueryOptions(), Long.class); return cursor.next(); } @@ -369,7 +367,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, @@ -379,7 +377,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) { @@ -407,4 +405,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/main/java/com/arangodb/springframework/repository/query/AbstractArangoQuery.java b/src/main/java/com/arangodb/springframework/repository/query/AbstractArangoQuery.java index 54c1f6af0..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; @@ -37,7 +36,7 @@ import com.arangodb.springframework.core.ArangoOperations; /** - * + * * @author Audrius Malele * @author Mark McCormick * @author Mark Vollmary @@ -51,14 +50,17 @@ 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!"); this.method = method; this.operations = operations; mappingContext = (ArangoMappingContext) operations.getConverter().getMappingContext(); this.domainClass = method.getEntityInformation().getJavaType(); + this.transactionBridge = transactionBridge; } @Override @@ -75,19 +77,23 @@ public Object execute(final Object[] parameters) { options.fullCount(true); } - final String query = createQuery(accessor, bindVars, options); + final QueryWithCollections query = createQuery(accessor, bindVars, options); + if (options.getStreamTransactionId() == null && transactionBridge != null) { + options.streamTransactionId(transactionBridge.getCurrentTransaction(query.getCollections())); + } + 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(query.getQuery(), bindVars, options, typeToRead); logWarningsIfNecessary(result); return processor.processResult(convertResult(result, accessor)); } 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()); }); } @@ -100,19 +106,19 @@ 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 * 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 QueryWithCollections createQuery( + ArangoParameterAccessor accessor, + Map bindVars, + AqlQueryOptions options); protected abstract boolean isCountQuery(); @@ -120,7 +126,7 @@ protected abstract String createQuery( /** * Merges AqlQueryOptions derived from @QueryOptions with dynamically passed AqlQueryOptions which takes priority - * + * * @param oldStatic * @param newDynamic * @return @@ -178,6 +184,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..d970c5d47 100644 --- a/src/main/java/com/arangodb/springframework/repository/query/DerivedArangoQuery.java +++ b/src/main/java/com/arangodb/springframework/repository/query/DerivedArangoQuery.java @@ -44,15 +44,16 @@ 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, + protected QueryWithCollections createQuery( + final ArangoParameterAccessor accessor, final Map bindVars, final AqlQueryOptions options) { 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..c22fca964 --- /dev/null +++ b/src/main/java/com/arangodb/springframework/repository/query/QueryTransactionBridge.java @@ -0,0 +1,74 @@ +/* + * 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; +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 { + + private static final ThreadLocal, String>> CURRENT_TRANSACTION = new NamedInheritableThreadLocal<>("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.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 + */ + @Nullable + public String getCurrentTransaction(Collection collections) { + return CURRENT_TRANSACTION.get().apply(collections); + } +} 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..19c249ece --- /dev/null +++ b/src/main/java/com/arangodb/springframework/repository/query/QueryWithCollections.java @@ -0,0 +1,42 @@ +/* + * 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; + +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 3cc068901..bacf9c113 100644 --- a/src/main/java/com/arangodb/springframework/repository/query/StringBasedArangoQuery.java +++ b/src/main/java/com/arangodb/springframework/repository/query/StringBasedArangoQuery.java @@ -33,9 +33,8 @@ 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.lang.reflect.Modifier; +import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -46,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(); @@ -69,13 +69,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 +91,35 @@ public StringBasedArangoQuery(final String query, final ArangoQueryMethod method } @Override - protected String createQuery( - final ArangoParameterAccessor accessor, + protected QueryWithCollections createQuery( + final ArangoParameterAccessor accessor, final Map bindVars, final AqlQueryOptions options) { extractBindVars(accessor, bindVars); - return prepareQuery(accessor); + return new QueryWithCollections(prepareQuery(accessor), allCollectionNames(bindVars)); + } + + private Collection allCollectionNames(Map bindVars) { + HashSet allCollections = new HashSet<>(); + if (!Modifier.isAbstract(domainClass.getModifiers())) { + allCollections.add(collectionName); + } + bindVars.entrySet().stream() + .filter(entry -> entry.getKey().startsWith("@")) + .map(Map.Entry::getValue) + .map(this::asCollectionName) + .map(AqlUtils::buildCollectionName) + .forEach(allCollections::add); + return allCollections; + } + + private String asCollectionName(Object value) { + if (value instanceof Class) { + return mappingContext.getRequiredPersistentEntity((Class) value).getCollection(); + } + return value.toString(); } @Override @@ -149,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 853c50324..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 @@ -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; @@ -45,12 +46,11 @@ import org.springframework.util.Assert; import java.util.*; -import java.util.stream.Collectors; /** * Creates a full AQL query from a PartTree and ArangoParameterAccessor */ -public class DerivedQueryCreator extends AbstractQueryCreator { +public class DerivedQueryCreator extends AbstractQueryCreator { private static final Logger LOGGER = LoggerFactory.getLogger(DerivedQueryCreator.class); private static final Set UNSUPPORTED_IGNORE_CASE = new HashSet<>(); @@ -88,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; @@ -120,13 +120,13 @@ protected Criteria or(final Criteria base, final Criteria criteria) { * @return */ @Override - protected String 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"); } 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(" "); } @@ -159,7 +159,7 @@ protected String 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); @@ -169,7 +169,7 @@ protected String 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()) { @@ -191,7 +191,7 @@ protected String complete(final Criteria criteria, final Sort sort) { } } } - return query.toString(); + return new QueryWithCollections(query.toString(), withCollections); } public double[] getUniquePoint() { @@ -260,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()) { @@ -273,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); @@ -285,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 @@ -311,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 { @@ -319,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())); } } } @@ -384,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"); @@ -397,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 @@ -409,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)); @@ -430,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: @@ -491,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) { @@ -587,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); } @@ -613,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); } @@ -631,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/ArangoTransactionHolder.java b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionHolder.java new file mode 100644 index 000000000..0d42db32c --- /dev/null +++ b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionHolder.java @@ -0,0 +1,76 @@ +/* + * 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.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; + +/** + * Synchronisation resource (has to be mutable). + * + * @see TransactionSynchronizationManager#bindResource(Object, Object) + * @see ArangoTransactionObject + * @author Arne Burmeister + */ +class ArangoTransactionHolder { + + private final Set collectionNames = new HashSet<>(); + private StreamTransactionEntity transaction = null; + private boolean rollbackOnly = false; + + @Nullable + String getStreamTransactionId() { + return transaction == null ? null : transaction.getId(); + } + + void setStreamTransaction(StreamTransactionEntity transaction) { + this.transaction = transaction; + } + + Set getCollectionNames() { + return 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 || isStatus(StreamTransactionStatus.aborted); + } + + void setRollbackOnly() { + rollbackOnly = true; + } + + public boolean isStatus(StreamTransactionStatus status) { + return transaction != null && transaction.getStatus() == status; + } +} 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..a9188a7e7 --- /dev/null +++ b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManagementConfigurer.java @@ -0,0 +1,55 @@ +/* + * 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; +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. + * + * @author Arne Burmeister + */ +public class ArangoTransactionManagementConfigurer implements TransactionManagementConfigurer { + + @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/main/java/com/arangodb/springframework/transaction/ArangoTransactionManager.java b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManager.java new file mode 100644 index 000000000..324be60d6 --- /dev/null +++ b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionManager.java @@ -0,0 +1,212 @@ +/* + * 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; +import com.arangodb.ArangoDatabase; +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.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#db() 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. + * + * @author Arne Burmeister + */ +public class ArangoTransactionManager extends AbstractPlatformTransactionManager implements InitializingBean { + + private final ArangoOperations operations; + private final QueryTransactionBridge bridge; + + public ArangoTransactionManager(ArangoOperations operations, QueryTransactionBridge bridge) { + this.operations = operations; + this.bridge = bridge; + setGlobalRollbackOnParticipationFailure(true); + 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 required"); + } + if (getTransactionSynchronization() == SYNCHRONIZATION_NEVER) { + throw new IllegalStateException("Transaction synchronization must not be disabled"); + } + } + + /** + * Creates a new transaction object. Any holder bound will be reused. + */ + @Override + protected Object doGetTransaction() { + ArangoTransactionHolder holder = (ArangoTransactionHolder) TransactionSynchronizationManager.getResource(this); + try { + return new ArangoTransactionObject(operations.db(), CollectionCallback.fromOperations(operations), getDefaultTimeout(), holder); + } catch (ArangoDBException error) { + throw new TransactionSystemException("Cannot create transaction object", error); + } + } + + /** + * Connect the new transaction object to the query bridge. + * + * @see QueryTransactionBridge#setCurrentTransaction(Function) + * @see #prepareSynchronization(DefaultTransactionStatus, TransactionDefinition) + * @throws InvalidIsolationLevelException for {@link TransactionDefinition#ISOLATION_SERIALIZABLE} + */ + @Override + protected void doBegin(Object transaction, TransactionDefinition definition) throws InvalidIsolationLevelException { + int isolationLevel = definition.getIsolationLevel(); + if (isolationLevel != TransactionDefinition.ISOLATION_DEFAULT && (isolationLevel & TransactionDefinition.ISOLATION_SERIALIZABLE) != 0) { + throw new InvalidIsolationLevelException("ArangoDB does not support isolation level serializable"); + } + ArangoTransactionObject tx = (ArangoTransactionObject) transaction; + bridge.setCurrentTransaction(collections -> { + try { + return tx.getOrBegin(collections).getStreamTransactionId(); + } catch (ArangoDBException error) { + throw new TransactionSystemException("Cannot begin transaction", error); + } + }); + } + + /** + * Commit the current stream transaction. The query bridge is cleared + * afterward. + * + * @see ArangoDatabase#commitStreamTransaction(String) + * @see QueryTransactionBridge#clearCurrentTransaction() + */ + @Override + protected void doCommit(DefaultTransactionStatus status) throws TransactionException { + ArangoTransactionObject tx = (ArangoTransactionObject) status.getTransaction(); + if (logger.isDebugEnabled()) { + logger.debug("Commit stream transaction " + tx); + } + 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); + } + } + + /** + * Roll back the current stream transaction. The query bridge is cleared + * afterward. + * + * @see ArangoDatabase#abortStreamTransaction(String) + * @see QueryTransactionBridge#clearCurrentTransaction() + */ + @Override + protected void doRollback(DefaultTransactionStatus status) throws TransactionException { + ArangoTransactionObject tx = (ArangoTransactionObject) status.getTransaction(); + if (logger.isDebugEnabled()) { + logger.debug("Rollback stream transaction " + tx); + } + try { + tx.rollback(); + } catch (ArangoDBException error) { + throw new TransactionSystemException("Cannot roll back transaction " + tx, error); + } finally { + afterCompletion(); + } + } + + /** + * Check if the transaction object has the bound holder. For new + * transactions the holder will be bound afterward. + */ + @Override + protected boolean isExistingTransaction(Object transaction) throws TransactionException { + ArangoTransactionHolder holder = ((ArangoTransactionObject) transaction).getHolder(); + return holder == TransactionSynchronizationManager.getResource(this); + } + + /** + * Mark the transaction as global rollback only. + * + * @see #isGlobalRollbackOnParticipationFailure() + */ + @Override + protected void doSetRollbackOnly(DefaultTransactionStatus status) throws TransactionException { + ArangoTransactionObject tx = (ArangoTransactionObject) status.getTransaction(); + tx.getHolder().setRollbackOnly(); + } + + /** + * 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 (transaction != null && status.isNewSynchronization()) { + ArangoTransactionHolder holder = transaction.getHolder(); + TransactionSynchronizationManager.bindResource(this, holder); + } + } + + private void afterCompletion() { + bridge.clearCurrentTransaction(); + TransactionSynchronizationManager.unbindResource(this); + } +} 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..d4f176cb6 --- /dev/null +++ b/src/main/java/com/arangodb/springframework/transaction/ArangoTransactionObject.java @@ -0,0 +1,132 @@ +/* + * 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; +import com.arangodb.ArangoDatabase; +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; +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; +import java.util.stream.Collectors; + +import static org.springframework.transaction.TransactionDefinition.TIMEOUT_DEFAULT; + +/** + * Transaction object used for {@link org.springframework.transaction.support.DefaultTransactionStatus#getTransaction()}. + * + * @see ArangoTransactionManager#doGetTransaction() + * @author Arne Burmeister + */ +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, CollectionCallback collectionCallback, int defaultTimeout, @Nullable ArangoTransactionHolder holder) { + this.database = database; + this.collectionCallback = collectionCallback; + this.holder = holder == null ? new ArangoTransactionHolder() : holder; + this.timeout = defaultTimeout; + } + + ArangoTransactionHolder getHolder() { + return holder; + } + + void configure(TransactionDefinition definition) { + if (definition.getTimeout() != TIMEOUT_DEFAULT) { + this.timeout = definition.getTimeout(); + } + if (definition instanceof TransactionAttribute) { + addCollections(((TransactionAttribute) definition).getLabels().stream() + .map(AqlUtils::buildCollectionName) + .collect(Collectors.toList())); + } + } + + 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])) + .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 getHolder(); + } + + void commit() throws ArangoDBException { + if (holder.isStatus(StreamTransactionStatus.running)) { + holder.setStreamTransaction(database.commitStreamTransaction(holder.getStreamTransactionId())); + } + } + + void rollback() throws ArangoDBException { + holder.setRollbackOnly(); + if (holder.isStatus(StreamTransactionStatus.running)) { + holder.setStreamTransaction(database.abortStreamTransaction(holder.getStreamTransactionId())); + } + } + + @Override + public boolean isRollbackOnly() { + return holder.isRollbackOnly(); + } + + @Override + public void flush() { + } + + @Override + public String toString() { + return holder.getStreamTransactionId() == null ? "(not begun)" : holder.getStreamTransactionId(); + } + + private void addCollections(Collection collections) { + 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); + } + } +} 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..0f7afb3b6 --- /dev/null +++ b/src/main/java/com/arangodb/springframework/transaction/TransactionAttributeTemplate.java @@ -0,0 +1,91 @@ +/* + * 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; +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 + * @author Arne Burmeister + */ +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) { + setQualifier(transactionAttribute.getQualifier()); + setLabels(transactionAttribute.getLabels()); + setRollbackOn(transactionAttribute::rollbackOn); + } + } + + @Override + @Nullable + public String getQualifier() { + return qualifier; + } + + public void setQualifier(@Nullable 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 rollbackOn.test(ex); + } + + public void setRollbackOn(Predicate rollbackOn) { + this.rollbackOn = rollbackOn; + } +} 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 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..29b0216bd --- /dev/null +++ b/src/test/java/com/arangodb/springframework/ArangoTransactionalTestConfiguration.java @@ -0,0 +1,17 @@ +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; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.transaction.TransactionalTestExecutionListener; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +@EnableTransactionManagement +@TestExecutionListeners(TransactionalTestExecutionListener.class) +@Import(ArangoTransactionManagementConfigurer.class) +public class ArangoTransactionalTestConfiguration { + +} 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"}) 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..a35ac4289 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.db(), is(notNullValue())); } @Test @@ -409,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)); 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/repository/query/QueryTransactionBridgeTest.java b/src/test/java/com/arangodb/springframework/repository/query/QueryTransactionBridgeTest.java new file mode 100644 index 000000000..48474f38a --- /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.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import java.util.Collections; + +import static org.hamcrest.MatcherAssert.assertThat; + +public class QueryTransactionBridgeTest { + + private QueryTransactionBridge underTest = new QueryTransactionBridge(); + + @Test + public void getCurrentTransactionInitiallyReturnsNull() { + assertThat(underTest.getCurrentTransaction(Collections.singleton("test")), Matchers.nullValue()); + } + + @Test + public void setCurrentTransactionIsAppliedOnGetCurrentTransaction() { + underTest.setCurrentTransaction(collections -> collections.iterator().next()); + assertThat(underTest.getCurrentTransaction(Collections.singleton("test")), Matchers.is("test")); + } + + @AfterEach + public void cleanup() { + underTest.clearCurrentTransaction(); + } +} 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 new file mode 100644 index 000000000..53af05b8b --- /dev/null +++ b/src/test/java/com/arangodb/springframework/transaction/ArangoTransactionManagerRepositoryTest.java @@ -0,0 +1,74 @@ +package com.arangodb.springframework.transaction; + +import com.arangodb.springframework.AbstractArangoTest; +import com.arangodb.springframework.ArangoTransactionalTestConfiguration; +import com.arangodb.springframework.repository.ActorRepository; +import com.arangodb.springframework.repository.MovieRepository; +import com.arangodb.springframework.testdata.Movie; +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; +import org.springframework.transaction.annotation.Transactional; + +import static org.assertj.core.api.Assertions.assertThat; + +@ContextConfiguration(classes = { ArangoTransactionalTestConfiguration.class }) +public class ArangoTransactionManagerRepositoryTest extends AbstractArangoTest { + + public ArangoTransactionManagerRepositoryTest() { + super(Movie.class); + } + + @Autowired + private MovieRepository movieRepository; + @Autowired + private ActorRepository actorRepository; + + Movie starWars = new Movie(); + + { + starWars.setName("Star Wars"); + } + + @Test + public void shouldWorkWithoutTransaction() { + movieRepository.save(starWars); + + assertThat(movieRepository.findById(starWars.getId())).isPresent(); + } + + @Test + @Transactional + public void shouldWorkWithinTransaction() { + movieRepository.save(starWars); + + assertThat(movieRepository.findById(starWars.getId())).isPresent(); + } + + @Test + @Transactional + public void shouldWorkAfterTransaction() { + TestTransaction.flagForCommit(); + + movieRepository.save(starWars); + TestTransaction.end(); + + assertThat(movieRepository.findById(starWars.getId())).isPresent(); + } + + @Test + @Transactional + public void shouldRollbackWithinTransaction() { + movieRepository.save(starWars); + TestTransaction.end(); + + 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 new file mode 100644 index 000000000..6c68c1eea --- /dev/null +++ b/src/test/java/com/arangodb/springframework/transaction/ArangoTransactionManagerTest.java @@ -0,0 +1,222 @@ +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 com.arangodb.model.TransactionCollectionOptions; +import com.arangodb.springframework.core.ArangoOperations; +import com.arangodb.springframework.repository.query.QueryTransactionBridge; +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.jupiter.MockitoExtension; +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; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +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.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.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; +import static org.springframework.beans.PropertyAccessorFactory.forDirectFieldAccess; + +@ExtendWith(MockitoExtension.class) +public class ArangoTransactionManagerTest { + + @Mock + private ArangoOperations operations; + @Mock + private QueryTransactionBridge bridge; + @InjectMocks + private ArangoTransactionManager underTest; + @Mock + private ArangoDatabase database; + private StreamTransactionEntity streamTransaction; + @Captor + private ArgumentCaptor, String>> beginPassed; + @Captor + private ArgumentCaptor optionsPassed; + + @BeforeEach + public void setupMocks() { + streamTransaction = new StreamTransactionEntity(); + when(operations.db()) + .thenReturn(database); + } + + @AfterEach + public void cleanupSync() { + TransactionSynchronizationManager.unbindResourceIfPossible(underTest); + TransactionSynchronizationManager.clear(); + } + + @Test + public void getTransactionReturnsNewTransactionWithoutStreamTransaction() { + TransactionStatus status = underTest.getTransaction(createTransactionAttribute("test")); + assertThat(status.isNewTransaction(), is(true)); + verify(bridge).setCurrentTransaction(any()); + ArangoTransactionHolder resource = (ArangoTransactionHolder) TransactionSynchronizationManager.getResource(underTest); + assertThat(resource.getStreamTransactionId(), nullValue()); + assertThat(resource.getCollectionNames(), empty()); + assertThat(resource.isRollbackOnly(), is(false)); + verifyNoInteractions(database); + } + + @Test + public void innerGetTransactionIsNotNewTransactionIncludingFormerCollections() { + 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")); + verifyNoInteractions(database); + } + + @Test + public void innerRollbackCausesUnexpectedRollbackOnOuterCommit() { + TransactionStatus outer = underTest.getTransaction(createTransactionAttribute("outer")); + TransactionStatus inner = underTest.getTransaction(createTransactionAttribute("inner")); + underTest.rollback(inner); + assertThrows(UnexpectedRollbackException.class, () -> underTest.commit(outer)); + assertThat(TransactionSynchronizationManager.getResource(underTest), nullValue()); + } + + @Test + public void getTransactionReturnsTransactionCreatesStreamTransactionWithAllCollectionsOnBridgeBeginCall() { + DefaultTransactionAttribute definition = createTransactionAttribute("timeout", "baz"); + definition.setTimeout(20); + TransactionStatus status = underTest.getTransaction(definition); + beginTransaction("123", "foo", "bar"); + assertThat(status.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 nestedGetTransactionReturnsExistingTransactionWithFormerCollections() { + TransactionStatus outer = underTest.getTransaction(createTransactionAttribute("outer", "foo")); + assertThat(outer.isNewTransaction(), is(true)); + + beginTransaction("123", "foo", "bar"); + + DefaultTransactionAttribute second = createTransactionAttribute("inner", "bar"); + TransactionStatus inner1 = underTest.getTransaction(second); + assertThat(inner1.isNewTransaction(), is(false)); + ArangoTransactionObject tx1 = getTransactionObject(inner1); + 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.getHolder().getStreamTransactionId(), is("123")); + underTest.commit(inner2); + underTest.commit(outer); + verify(database).commitStreamTransaction("123"); + assertThat(TransactionSynchronizationManager.getResource(underTest), nullValue()); + } + + @Test + public void getTransactionForPropagationSupportsWithoutExistingCreatesDummyTransaction() { + DefaultTransactionAttribute supports = createTransactionAttribute("test", "foo"); + supports.setPropagationBehavior(TransactionDefinition.PROPAGATION_SUPPORTS); + TransactionStatus empty = underTest.getTransaction(supports); + assertThat(empty.isNewTransaction(), is(false)); + underTest.commit(empty); + verifyNoInteractions(database); + assertThat(TransactionSynchronizationManager.getResource(underTest), nullValue()); + } + + @Test + public void getTransactionForPropagationSupportsWithExistingCreatesInner() { + TransactionStatus outer = underTest.getTransaction(createTransactionAttribute("outer", "foo")); + DefaultTransactionAttribute supports = createTransactionAttribute("supports", "bar"); + supports.setPropagationBehavior(TransactionDefinition.PROPAGATION_SUPPORTS); + TransactionStatus inner = underTest.getTransaction(supports); + assertThat(inner.isNewTransaction(), is(false)); + underTest.commit(inner); + ArangoTransactionObject transactionObject = getTransactionObject(inner); + assertThat(TransactionSynchronizationManager.getResource(underTest), is(transactionObject.getHolder())); + verifyNoInteractions(database); + } + + @Test + public void getTransactionWithMultipleBridgeCallsWorksForKnownCollections() { + underTest.getTransaction(createTransactionAttribute("test", "baz")); + beginTransaction("123", "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 + public void getTransactionWithMultipleBridgeCallsIgnoresAdditionalCollections() { + TransactionStatus state = underTest.getTransaction(createTransactionAttribute("test", "bar")); + beginTransaction("123", "foo"); + beginPassed.getValue().apply(Collections.singletonList("baz")); + assertThat(getTransactionObject(state).getHolder().getCollectionNames(), hasItems("foo", "bar")); + assertThat(getTransactionObject(state).getHolder().getCollectionNames(), not(hasItem("baz"))); + } + + @Test + public void getTransactionThrowsInvalidIsolationLevelExceptionForIsolationSerializable() { + DefaultTransactionAttribute definition = createTransactionAttribute("serializable"); + definition.setIsolationLevel(TransactionDefinition.ISOLATION_SERIALIZABLE); + assertThrows(InvalidIsolationLevelException.class, () -> underTest.getTransaction(definition)); + } + + private void beginTransaction(String id, String... collectionNames) { + ReflectionTestUtils.setField(streamTransaction, "id", id); + ReflectionTestUtils.setField(streamTransaction, "status", StreamTransactionStatus.running); + when(database.beginStreamTransaction(any())) + .thenReturn(streamTransaction); + lenient().when(database.getStreamTransaction(any())) + .thenReturn(streamTransaction); + verify(bridge).setCurrentTransaction(beginPassed.capture()); + beginPassed.getValue().apply(Arrays.asList(collectionNames)); + } + + @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"); + } + + 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 @@ - +