Implement Neo4jTransactionManager.

This commit is contained in:
Michael Simons
2019-03-11 18:45:31 +01:00
parent bb48337187
commit 0197ef4cb6
19 changed files with 902 additions and 110 deletions

View File

@@ -1,2 +1,2 @@
lombok.nonNull.exceptionType = IllegalArgumentException
lombok.log.fieldName = LOG

13
pom.xml
View File

@@ -21,8 +21,8 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.data.build</groupId>
<artifactId>spring-data-parent</artifactId>
<groupId>org.springframework.data.build</groupId>
<version>2.2.0.BUILD-SNAPSHOT</version>
</parent>
@@ -97,6 +97,7 @@
<maven-surefire-plugin.version>3.0.0-M2</maven-surefire-plugin.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<mockito.version>2.23.4</mockito.version>
<neo4j-java-driver.version>1.7.2</neo4j-java-driver.version>
<neo4j.version>3.5.2</neo4j.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
@@ -151,6 +152,16 @@
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>${mockito.version}</version>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>${mockito.version}</version>
</dependency>
<dependency>
<groupId>org.neo4j</groupId>
<artifactId>neo4j</artifactId>

View File

@@ -21,8 +21,8 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-neo4j-rx-parent</artifactId>
<groupId>org.springframework.data</groupId>
<version>1.0.0.BUILD-SNAPSHOT</version>
</parent>
@@ -58,6 +58,12 @@
<artifactId>rxjava</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>javax.transaction</groupId>
<artifactId>jta</artifactId>
<version>1.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apiguardian</groupId>
<artifactId>apiguardian-api</artifactId>
@@ -72,6 +78,16 @@
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.neo4j.driver</groupId>
<artifactId>neo4j-java-driver</artifactId>
@@ -81,6 +97,10 @@
<artifactId>slf4j-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>

View File

@@ -18,42 +18,33 @@
*/
package org.springframework.data.neo4j.core;
import org.apiguardian.api.API;
import org.neo4j.driver.v1.Driver;
import org.neo4j.driver.v1.Session;
import org.neo4j.driver.v1.StatementResult;
import org.neo4j.driver.v1.Transaction;
import org.neo4j.driver.v1.StatementRunner;
import org.springframework.data.neo4j.core.transaction.DefaultNeo4jStatementRunnerSupplier;
import org.springframework.data.neo4j.core.transaction.StatementRunnerSupplier;
/**
* Default implementation of {@link Neo4jOperations}. Uses the Neo4j Java driver to connect to and interact with the
* database.
*
* @author Gerrit Meier
* @author Michael J. Simons
*/
public class Neo4jTemplate implements Neo4jOperations {
private final Driver driver;
private StatementRunnerSupplier<StatementRunner> statementRunnerSupplier;
@API(status = API.Status.STABLE, since = "1.0")
public Neo4jTemplate(Driver driver) {
this.driver = driver;
this.statementRunnerSupplier = new DefaultNeo4jStatementRunnerSupplier(driver);
}
@Override
public Object executeQuery(String query) {
Session session = driver.session();
Transaction transaction = session.beginTransaction();
try {
StatementResult result = transaction.run(query);
transaction.success();
return result.list();
} catch (Exception e) {
transaction.failure();
} finally {
transaction.close();
session.close();
}
return null;
// TODO Let's see whether we can stick with the statementrunner or if we need to differentiate between reactive runner and default. Current 2.0 react version has two complete separate interfaces
StatementRunner statementRunner = statementRunnerSupplier.get();
StatementResult result = statementRunner.run(query);
return result.list();
}
}

View File

@@ -1,36 +0,0 @@
/*
* Copyright (c) 2019 "Neo4j,"
* Neo4j Sweden AB [http://neo4j.com]
*
* This file is part of Neo4j.
*
* 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.
*/
package org.springframework.data.neo4j.core.session;
import org.neo4j.driver.v1.Driver;
import org.neo4j.driver.v1.Session;
/**
* Default implementation of a {@link Neo4jSessionFactory}.
*
* @author Michael J. Simons
*/
class DefaultNeo4jSessionFactory extends Neo4jSessionFactorySupport<Driver> {
@Override
public Session getSession() {
throw new UnsupportedOperationException("Not there yet.");
}
}

View File

@@ -1,30 +0,0 @@
/*
* Copyright (c) 2019 "Neo4j,"
* Neo4j Sweden AB [http://neo4j.com]
*
* This file is part of Neo4j.
*
* 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.
*/
package org.springframework.data.neo4j.core.session;
import org.neo4j.driver.v1.Driver;
/**
* Support infrastructure for dealing with the Neo4j Driver.
*
* @param <D> Type of the driver.
* @author Michael J. Simons
*/
abstract class Neo4jSessionFactorySupport<D extends Driver> implements Neo4jSessionFactory {
}

View File

@@ -0,0 +1,64 @@
/*
* Copyright (c) 2019 "Neo4j,"
* Neo4j Sweden AB [http://neo4j.com]
*
* This file is part of Neo4j.
*
* 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.
*/
package org.springframework.data.neo4j.core.transaction;
import lombok.RequiredArgsConstructor;
import java.util.Collections;
import org.neo4j.driver.v1.AccessMode;
import org.neo4j.driver.v1.Driver;
import org.neo4j.driver.v1.StatementRunner;
import org.springframework.transaction.support.TransactionSynchronizationManager;
/**
* @author Michael J. Simons
*/
@RequiredArgsConstructor
public class DefaultNeo4jStatementRunnerSupplier implements StatementRunnerSupplier<StatementRunner> {
private final Driver driver;
@Override
public StatementRunner get() {
if (!TransactionSynchronizationManager.isSynchronizationActive()) {
return driver.session();
}
// Try existing transaction
Neo4jResourceHolder resourceHolder = (Neo4jResourceHolder) TransactionSynchronizationManager
.getResource(driver);
if (resourceHolder != null) {
return resourceHolder.getTransaction();
}
// Manually create a new synchronization
resourceHolder = new Neo4jResourceHolder(driver.session(AccessMode.WRITE, Collections.emptyList()));
TransactionSynchronizationManager.registerSynchronization(
new Neo4jSessionSynchronization(resourceHolder, driver));
resourceHolder.setSynchronizedWithTransaction(true);
TransactionSynchronizationManager.bindResource(driver, resourceHolder);
return resourceHolder.getTransaction();
}
}

View File

@@ -0,0 +1,102 @@
/*
* Copyright (c) 2019 "Neo4j,"
* Neo4j Sweden AB [http://neo4j.com]
*
* This file is part of Neo4j.
*
* 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.
*/
package org.springframework.data.neo4j.core.transaction;
import org.neo4j.driver.v1.Session;
import org.neo4j.driver.v1.Transaction;
import org.neo4j.driver.v1.TransactionConfig;
import org.springframework.transaction.support.ResourceHolderSupport;
import org.springframework.util.Assert;
/**
* Neo4j specific {@link ResourceHolderSupport resource holder}, wrapping a {@link org.neo4j.driver.v1.Transaction}.
* {@link Neo4jTransactionManager} binds instances of this class to the thread.
* <p>
* <strong>Note:</strong> Intended for internal usage only.
*
* @author Michael J. Simons
*/
public class Neo4jResourceHolder extends ResourceHolderSupport {
private final Session session;
private final Transaction transaction;
public Neo4jResourceHolder(Session session) {
this(session, TransactionConfig.empty());
}
public Neo4jResourceHolder(Session session, TransactionConfig transactionConfig) {
this.session = session;
this.transaction = this.session.beginTransaction(transactionConfig);
}
Transaction getTransaction() {
return transaction;
}
public void commit() {
Assert.state(hasActiveTransaction(), "Transaction must be open, but has already been closed.");
Assert.state(!isRollbackOnly(), "Resource msut not be marked as rollback only.");
transaction.success();
transaction.close();
}
public void rollback() {
Assert.state(hasActiveTransaction(), "Transaction must be open, but has already been closed.");
transaction.failure();
transaction.close();
}
public void close() {
Assert.state(hasActiveSession(), "Session must be open, but has already been closed.");
if (hasActiveTransaction()) {
transaction.close();
}
session.close();
}
@Override
public void setRollbackOnly() {
super.setRollbackOnly();
}
@Override
public void resetRollbackOnly() {
throw new UnsupportedOperationException();
}
public boolean hasActiveSession() {
return session.isOpen();
}
public boolean hasActiveTransaction() {
return transaction.isOpen();
}
}

View File

@@ -0,0 +1,88 @@
/*
* Copyright (c) 2019 "Neo4j,"
* Neo4j Sweden AB [http://neo4j.com]
*
* This file is part of Neo4j.
*
* 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.
*/
package org.springframework.data.neo4j.core.transaction;
import org.neo4j.driver.v1.Driver;
import org.springframework.transaction.support.ResourceHolderSynchronization;
import org.springframework.transaction.support.TransactionSynchronization;
/**
* Neo4j specific {@link ResourceHolderSynchronization} for resource cleanup at the end of a transaction when
* participating in a non-native Neoj4 transaction, such as a Jta transaction.
*/
class Neo4jSessionSynchronization
extends ResourceHolderSynchronization<Neo4jResourceHolder, Object> {
private final Neo4jResourceHolder localResourceHolder;
Neo4jSessionSynchronization(Neo4jResourceHolder resourceHolder, Driver driver) {
super(resourceHolder, driver);
this.localResourceHolder = resourceHolder;
}
/*
* (non-Javadoc)
* @see org.springframework.transaction.support.ResourceHolderSynchronization#shouldReleaseBeforeCompletion()
*/
@Override
protected boolean shouldReleaseBeforeCompletion() {
return false;
}
/*
* (non-Javadoc)
* @see org.springframework.transaction.support.ResourceHolderSynchronization#processResourceAfterCommit(java.lang.Object)
*/
@Override
protected void processResourceAfterCommit(Neo4jResourceHolder resourceHolder) {
super.processResourceAfterCommit(resourceHolder);
if (resourceHolder.hasActiveTransaction()) {
resourceHolder.commit();
}
}
/*
* (non-Javadoc)
* @see org.springframework.transaction.support.ResourceHolderSynchronization#afterCompletion(int)
*/
@Override
public void afterCompletion(int status) {
if (status == TransactionSynchronization.STATUS_ROLLED_BACK && localResourceHolder.hasActiveTransaction()) {
localResourceHolder.rollback();
}
super.afterCompletion(status);
}
/*
* (non-Javadoc)
* @see org.springframework.transaction.support.ResourceHolderSynchronization#releaseResource(java.lang.Object, java.lang.Object)
*/
@Override
protected void releaseResource(Neo4jResourceHolder resourceHolder, Object resourceKey) {
if (resourceHolder.hasActiveSession()) {
resourceHolder.close();
}
}
}

View File

@@ -0,0 +1,227 @@
/*
* Copyright (c) 2019 "Neo4j,"
* Neo4j Sweden AB [http://neo4j.com]
*
* This file is part of Neo4j.
*
* 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.
*/
package org.springframework.data.neo4j.core.transaction;
import java.time.Duration;
import java.util.Collections;
import java.util.List;
import org.apiguardian.api.API;
import org.neo4j.driver.v1.AccessMode;
import org.neo4j.driver.v1.Driver;
import org.neo4j.driver.v1.Session;
import org.neo4j.driver.v1.TransactionConfig;
import org.springframework.lang.Nullable;
import org.springframework.transaction.IllegalTransactionStateException;
import org.springframework.transaction.InvalidIsolationLevelException;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionException;
import org.springframework.transaction.TransactionSystemException;
import org.springframework.transaction.support.AbstractPlatformTransactionManager;
import org.springframework.transaction.support.DefaultTransactionStatus;
import org.springframework.transaction.support.SmartTransactionObject;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.transaction.support.TransactionSynchronizationUtils;
import org.springframework.util.Assert;
/**
* Dedicated {@link org.springframework.transaction.PlatformTransactionManager} for native Neo4j transactions.
*
* @author Michael J. Simons
*/
public class Neo4jTransactionManager extends AbstractPlatformTransactionManager {
private final Driver driver;
@API(status = API.Status.STABLE, since = "1.0")
public Neo4jTransactionManager(Driver driver) {
this.driver = driver;
}
@Override
protected Object doGetTransaction() throws TransactionException {
Neo4jResourceHolder resourceHolder = (Neo4jResourceHolder) TransactionSynchronizationManager
.getResource(driver);
return new Neo4jTransactionObject(resourceHolder);
}
@Override
protected boolean isExistingTransaction(Object transaction) throws TransactionException {
return extractNeo4jTransaction(transaction).hasResourceHolder();
}
@Override
protected void doBegin(Object transaction, TransactionDefinition definition) throws TransactionException {
Neo4jTransactionObject transactionObject = extractNeo4jTransaction(transaction);
TransactionConfig transactionConfig = createTransactionConfigFrom(definition);
boolean readOnly = definition.isReadOnly();
AccessMode accessMode = readOnly ? AccessMode.READ : AccessMode.WRITE;
List<String> bookmarks = Collections.emptyList(); // TODO Bookmarksupport
TransactionSynchronizationManager.setCurrentTransactionReadOnly(readOnly);
try {
Session session = this.driver.session(accessMode, bookmarks);
Neo4jResourceHolder resourceHolder = new Neo4jResourceHolder(session, transactionConfig);
transactionObject.setResourceHolder(resourceHolder);
resourceHolder.setSynchronizedWithTransaction(true);
TransactionSynchronizationManager.bindResource(driver, resourceHolder);
} catch (Exception ex) {
ex.printStackTrace();
throw new TransactionSystemException(String.format("Could not open a new Neo4j session:", ex));
}
}
@Override
protected Object doSuspend(Object transaction) throws TransactionException {
Neo4jTransactionObject transactionObject = extractNeo4jTransaction(transaction);
transactionObject.setResourceHolder(null);
return TransactionSynchronizationManager.unbindResource(driver);
}
@Override
protected void doResume(@Nullable Object transaction, Object suspendedResources) {
TransactionSynchronizationManager.bindResource(driver, suspendedResources);
}
@Override
protected void doCommit(DefaultTransactionStatus status) throws TransactionException {
Neo4jTransactionObject transactionObject = extractNeo4jTransaction(status);
transactionObject.getRequiredResourceHolder().commit();
}
@Override
protected void doRollback(DefaultTransactionStatus status) throws TransactionException {
Neo4jTransactionObject transactionObject = extractNeo4jTransaction(status);
transactionObject.getRequiredResourceHolder().rollback();
}
@Override
protected void doSetRollbackOnly(DefaultTransactionStatus status) throws TransactionException {
Neo4jTransactionObject transactionObject = extractNeo4jTransaction(status);
transactionObject.setRollbackOnly();
}
@Override
protected void doCleanupAfterCompletion(Object transaction) {
Neo4jTransactionObject transactionObject = extractNeo4jTransaction(transaction);
transactionObject.getRequiredResourceHolder().close();
TransactionSynchronizationManager.unbindResource(driver);
}
private static TransactionConfig createTransactionConfigFrom(TransactionDefinition definition) {
if (definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT) {
throw new InvalidIsolationLevelException(
"Neo4jTransactionManager is not allowed to support custom isolation levels.");
}
if (definition.getPropagationBehavior() != TransactionDefinition.PROPAGATION_REQUIRED) {
throw new IllegalTransactionStateException("Neo4jTransactionManager only supports 'required' propagation.");
}
TransactionConfig.Builder builder = TransactionConfig.builder();
if (definition.getTimeout() > 0) {
builder = builder.withTimeout(Duration.ofSeconds(definition.getTimeout()));
}
return builder.build();
}
private static Neo4jTransactionObject extractNeo4jTransaction(Object transaction) {
Assert.isInstanceOf(Neo4jTransactionObject.class, transaction,
() -> String.format("Expected to find a %s but it turned out to be %s.", Neo4jTransactionObject.class,
transaction.getClass()));
return (Neo4jTransactionObject) transaction;
}
private static Neo4jTransactionObject extractNeo4jTransaction(DefaultTransactionStatus status) {
return extractNeo4jTransaction(status.getTransaction());
}
static class Neo4jTransactionObject implements SmartTransactionObject {
private static final String RESOURCE_HOLDER_NOT_PRESENT_MESSAGE = "Neo4jResourceHolder is required but not present. o_O";
// The resource holder is null when the call to TransactionSynchronizationManager.getResource
// in Neo4jTransactionManager.doGetTransaction didn't return a corresponding resource holder.
// If it is null, there's no existing session / transaction.
private @Nullable Neo4jResourceHolder resourceHolder;
Neo4jTransactionObject(@Nullable Neo4jResourceHolder resourceHolder) {
this.resourceHolder = resourceHolder;
}
/**
* Usually called in {@link #doBegin(Object, TransactionDefinition)} which is called when there's
* no existing transaction.
*
* @param resourceHolder A newly created resource holder with a fresh drivers session,
*/
void setResourceHolder(@Nullable Neo4jResourceHolder resourceHolder) {
this.resourceHolder = resourceHolder;
}
/**
* @return {@literal true} if a {@link Neo4jResourceHolder} is set.
*/
boolean hasResourceHolder() {
return resourceHolder != null;
}
Neo4jResourceHolder getRequiredResourceHolder() {
Assert.state(hasResourceHolder(), RESOURCE_HOLDER_NOT_PRESENT_MESSAGE);
return resourceHolder;
}
void setRollbackOnly() {
getRequiredResourceHolder().setRollbackOnly();
}
@Override
public boolean isRollbackOnly() {
return this.hasResourceHolder() && this.resourceHolder.isRollbackOnly();
}
@Override
public void flush() {
TransactionSynchronizationUtils.triggerFlush();
}
}
}

View File

@@ -16,16 +16,17 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.neo4j.core.session;
package org.springframework.data.neo4j.core.transaction;
import org.neo4j.driver.v1.Session;
import java.util.function.Supplier;
/**
* Interface for factories creating {@link org.neo4j.driver.v1.Session Neo4j driver sessions}.
* Used for obtaining {@link org.neo4j.driver.v1.Session session bound (standard or reactive)} bound statement runners.
* <p>
* <strong>Note:</strong> Intended for internal usage only.
*
* @author Michael J. Simons
*/
public interface Neo4jSessionFactory {
Session getSession();
@FunctionalInterface
public interface StatementRunnerSupplier<T> extends Supplier<T> {
}

View File

@@ -2,6 +2,6 @@
* Core infrastructure for providing Neo4j sessions to Spring Data Neo4j.
*/
@NonNullApi
package org.springframework.data.neo4j.core.session;
package org.springframework.data.neo4j.core.transaction;
import org.springframework.lang.NonNullApi;

View File

@@ -18,6 +18,8 @@
*/
package org.springframework.data.neo4j.repository.config;
import static org.springframework.data.neo4j.repository.config.Neo4jRepositoryConfigurationExtension.*;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
@@ -31,6 +33,7 @@ import org.springframework.context.annotation.Import;
import org.springframework.core.annotation.AliasFor;
import org.springframework.data.neo4j.core.Neo4jTemplate;
import org.springframework.data.neo4j.repository.support.Neo4jRepositoryFactoryBean;
import org.springframework.transaction.PlatformTransactionManager;
/**
* Annotation to activate Neo4j repositories. If no base package is configured through either {@link #value()},
@@ -77,7 +80,13 @@ public @interface EnableNeo4jRepositories {
/**
* Configures the name of the {@link Neo4jTemplate} bean to be used with the repositories detected.
*/
String neo4jTemplateRef() default "neo4jTemplate";
String neo4jTemplateRef() default DEFAULT_NEO4J_TEMPLATE_BEAN_NAME;
/**
* Configures the name of the {@link PlatformTransactionManager} bean definition to be used to create repositories
* discovered through this annotation. Defaults to {@code transactionManager}.
*/
String transactionManagerRef() default DEFAULT_TRANSACTION_MANAGER_BEAN_NAME;
/**
* Specifies which types are eligible for component scanning. Further narrows the set of candidate components from

View File

@@ -18,11 +18,11 @@
*/
package org.springframework.data.neo4j.repository.config;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.data.neo4j.repository.support.Neo4jRepositoryFactoryBean;
import org.springframework.data.repository.config.AnnotationRepositoryConfigurationSource;
import org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport;
import org.springframework.data.repository.config.RepositoryConfigurationSource;
/**
* This dedicated Neo4j repository extension will be registered via {@link Neo4jRepositoriesRegistrar} and then provide
@@ -35,6 +35,14 @@ class Neo4jRepositoryConfigurationExtension extends RepositoryConfigurationExten
private static final String MODULE_PREFIX = "neo4j";
/**
* See {@link AbstractBeanDefinition#INFER_METHOD}.
*/
static final String GENERATE_BEAN_NAME = "(generated)";
static final String DEFAULT_NEO4J_TEMPLATE_BEAN_NAME = "neo4jTemplate";
static final String DEFAULT_TRANSACTION_MANAGER_BEAN_NAME = "transactionManager";
/*
* (non-Javadoc)
* @see org.springframework.data.repository.config14.RepositoryConfigurationExtension#getRepositoryFactoryBeanClassName()
@@ -55,13 +63,15 @@ class Neo4jRepositoryConfigurationExtension extends RepositoryConfigurationExten
/*
* (non-Javadoc)
* @see org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport#postProcess(org.springframework.beans.factory.support.BeanDefinitionBuilder, org.springframework.data.repository.config.AnnotationRepositoryConfigurationSource)
* @see org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport#postProcess(org.springframework.beans.factory.support.BeanDefinitionBuilder, org.springframework.data.repository.config.RepositoryConfigurationSource)
*/
@Override
public void postProcess(BeanDefinitionBuilder builder, AnnotationRepositoryConfigurationSource config) {
public void postProcess(BeanDefinitionBuilder builder, RepositoryConfigurationSource source) {
AnnotationAttributes attributes = config.getAttributes();
builder.addPropertyValue("transactionManager",
source.getAttribute("transactionManagerRef").orElse(DEFAULT_TRANSACTION_MANAGER_BEAN_NAME));
builder.addPropertyReference("neo4jOperations", attributes.getString("neo4jTemplateRef"));
builder.addPropertyReference("neo4jOperations",
source.getAttribute("neo4jTemplateRef").orElse(DEFAULT_NEO4J_TEMPLATE_BEAN_NAME));
}
}

View File

@@ -26,12 +26,17 @@ import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.neo4j.core.Neo4jOperations;
import org.springframework.data.neo4j.repository.Neo4jRepository;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
/**
* Repository base implementation for Neo4j.
*
* @author Gerrit Meier
* @author Michael J. Simons
*/
@Repository
@Transactional(readOnly = true)
class SimpleNeo4jRepository<T, ID> implements Neo4jRepository<T, ID> {
private final Neo4jOperations neo4jOperations;
@@ -51,11 +56,13 @@ class SimpleNeo4jRepository<T, ID> implements Neo4jRepository<T, ID> {
}
@Override
@Transactional
public <S extends T> S save(S entity) {
throw new UnsupportedOperationException("Not there yet.");
}
@Override
@Transactional
public <S extends T> Iterable<S> saveAll(Iterable<S> entities) {
throw new UnsupportedOperationException("Not there yet.");
}
@@ -86,23 +93,31 @@ class SimpleNeo4jRepository<T, ID> implements Neo4jRepository<T, ID> {
}
@Override
@Transactional
public void deleteById(ID id) {
throw new UnsupportedOperationException("Not there yet.");
}
@Override
@Transactional
public void delete(T entity) {
throw new UnsupportedOperationException("Not there yet.");
}
@Override
@Transactional
public void deleteAll(Iterable<? extends T> entities) {
throw new UnsupportedOperationException("Not there yet.");
}
@Override
@Transactional
public void deleteAll() {
throw new UnsupportedOperationException("Not there yet.");
for (T element : findAll()) {
delete(element);
}
}
@Override

View File

@@ -0,0 +1,221 @@
/*
* Copyright (c) 2019 "Neo4j,"
* Neo4j Sweden AB [http://neo4j.com]
*
* This file is part of Neo4j.
*
* 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.
*/
package org.springframework.data.neo4j.core.transaction;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import java.util.Collections;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.transaction.Status;
import javax.transaction.UserTransaction;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.neo4j.driver.v1.AccessMode;
import org.neo4j.driver.v1.Driver;
import org.neo4j.driver.v1.Session;
import org.neo4j.driver.v1.StatementRunner;
import org.neo4j.driver.v1.Transaction;
import org.neo4j.driver.v1.TransactionConfig;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.jta.JtaTransactionManager;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.transaction.support.TransactionTemplate;
/**
* @author Michael J. Simons
*/
@ExtendWith(MockitoExtension.class)
class DefaultNeo4jStatementRunnerSupplierTest {
@Mock
private Driver driver;
@Mock
private Session session;
@Mock
private Transaction transaction;
@Mock
UserTransaction userTransaction;
@BeforeEach
void setUp() {
AtomicBoolean sessionIsOpen = new AtomicBoolean(true);
AtomicBoolean transactionIsOpen = new AtomicBoolean(true);
when(driver.session(AccessMode.WRITE, Collections.emptyList())).thenReturn(session);
when(session.beginTransaction(any(TransactionConfig.class))).thenReturn(transaction);
doAnswer(invocation -> {
sessionIsOpen.set(false);
return null;
}).when(session).close();
when(session.isOpen()).thenAnswer(invocation -> sessionIsOpen.get());
doAnswer(invocation -> {
transactionIsOpen.set(false);
return null;
}).when(transaction).close();
when(transaction.isOpen()).thenAnswer(invocation -> transactionIsOpen.get());
}
@Nested
class BasedOnJtaTransactions {
@Test
void shouldParticipateInOngoingTransactionWithCommit() throws Exception {
when(userTransaction.getStatus()).thenReturn(Status.STATUS_NO_TRANSACTION, Status.STATUS_ACTIVE,
Status.STATUS_ACTIVE);
JtaTransactionManager txManager = new JtaTransactionManager(userTransaction);
TransactionTemplate txTemplate = new TransactionTemplate(txManager);
StatementRunnerSupplier<StatementRunner> statementRunnerSupplier = new DefaultNeo4jStatementRunnerSupplier(
driver);
txTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isTrue();
assertThat(transactionStatus.isNewTransaction()).isTrue();
assertThat(TransactionSynchronizationManager.hasResource(driver)).isFalse();
StatementRunner statementRunner = statementRunnerSupplier.get();
assertThat(TransactionSynchronizationManager.hasResource(driver)).isTrue();
}
});
verify(userTransaction).begin();
verify(driver).session(AccessMode.WRITE, Collections.emptyList());
verify(session, times(2)).isOpen();
verify(session).beginTransaction(any(TransactionConfig.class));
verify(session).close();
verify(transaction, times(3)).isOpen();
verify(transaction).success();
verify(transaction).close();
}
@Test
public void shouldParticipateInOngoingTransactionWithRollback() throws Exception {
when(userTransaction.getStatus()).thenReturn(Status.STATUS_NO_TRANSACTION, Status.STATUS_ACTIVE,
Status.STATUS_ACTIVE);
JtaTransactionManager txManager = new JtaTransactionManager(userTransaction);
TransactionTemplate txTemplate = new TransactionTemplate(txManager);
StatementRunnerSupplier<StatementRunner> statementRunnerSupplier = new DefaultNeo4jStatementRunnerSupplier(
driver);
txTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isTrue();
assertThat(transactionStatus.isNewTransaction()).isTrue();
assertThat(TransactionSynchronizationManager.hasResource(driver)).isFalse();
@SuppressWarnings({ "unused" })
StatementRunner statementRunner = statementRunnerSupplier.get();
assertThat(TransactionSynchronizationManager.hasResource(driver)).isTrue();
transactionStatus.setRollbackOnly();
}
});
verify(userTransaction).begin();
verify(userTransaction).rollback();
verify(driver).session(AccessMode.WRITE, Collections.emptyList());
verify(session, times(2)).isOpen();
verify(session).beginTransaction(any(TransactionConfig.class));
verify(session).close();
verify(transaction, times(3)).isOpen();
verify(transaction).failure();
verify(transaction).close();
}
}
@Nested
class BasedOnNeo4jTransactions {
@Test
public void shouldParticipateInOngoingTransaction() {
Neo4jTransactionManager txManager = new Neo4jTransactionManager(driver);
TransactionTemplate txTemplate = new TransactionTemplate(txManager);
StatementRunnerSupplier<StatementRunner> statementRunnerSupplier = new DefaultNeo4jStatementRunnerSupplier(
driver);
txTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isTrue();
assertThat(transactionStatus.isNewTransaction()).isTrue();
assertThat(TransactionSynchronizationManager.hasResource(driver)).isTrue();
@SuppressWarnings({ "unused" })
StatementRunner statementRunner = statementRunnerSupplier.get();
transactionStatus.setRollbackOnly();
}
});
verify(driver).session(AccessMode.WRITE, Collections.emptyList());
verify(session).isOpen();
verify(session).beginTransaction(any(TransactionConfig.class));
verify(session).close();
verify(transaction, times(2)).isOpen();
verify(transaction).failure();
verify(transaction).close();
}
}
@AfterEach
void verifyTransactionSynchronizationManagerState() {
assertTrue(TransactionSynchronizationManager.getResourceMap().isEmpty());
assertFalse(TransactionSynchronizationManager.isSynchronizationActive());
assertNull(TransactionSynchronizationManager.getCurrentTransactionName());
assertFalse(TransactionSynchronizationManager.isCurrentTransactionReadOnly());
assertNull(TransactionSynchronizationManager.getCurrentTransactionIsolationLevel());
assertFalse(TransactionSynchronizationManager.isActualTransactionActive());
}
}

View File

@@ -0,0 +1,82 @@
/*
* Copyright (c) 2019 "Neo4j,"
* Neo4j Sweden AB [http://neo4j.com]
*
* This file is part of Neo4j.
*
* 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.
*/
package org.springframework.data.neo4j.core.transaction;
import static org.mockito.Mockito.*;
import java.util.Collections;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.neo4j.driver.v1.AccessMode;
import org.neo4j.driver.v1.Driver;
import org.neo4j.driver.v1.Session;
import org.neo4j.driver.v1.StatementResult;
import org.neo4j.driver.v1.Transaction;
import org.neo4j.driver.v1.TransactionConfig;
import org.springframework.data.neo4j.core.Neo4jTemplate;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
/**
* @author Michael J. Simons
*/
@ExtendWith(MockitoExtension.class)
class Neo4jTransactionManagerTest {
@Mock
private Driver driver;
@Mock
private Session session;
@Mock
private Transaction transaction;
@Mock
private StatementResult statementResult;
@Test
public void triggerCommitCorrectly() {
when(driver.session(AccessMode.WRITE, Collections.emptyList())).thenReturn(session);
when(session.beginTransaction(any(TransactionConfig.class))).thenReturn(transaction);
when(transaction.run(anyString())).thenReturn(statementResult);
when(session.isOpen()).thenReturn(true);
when(transaction.isOpen()).thenReturn(true, false);
Neo4jTransactionManager txManager = new Neo4jTransactionManager(driver);
TransactionStatus txStatus = txManager.getTransaction(new DefaultTransactionDefinition());
Neo4jTemplate template = new Neo4jTemplate(driver);
template.executeQuery("RETURN 1");
txManager.commit(txStatus);
verify(driver).session(AccessMode.WRITE, Collections.emptyList());
verify(session).isOpen();
verify(session).beginTransaction(any(TransactionConfig.class));
verify(transaction, times(2)).isOpen();
verify(transaction).success();
verify(transaction).close();
verify(session).close();
}
}

View File

@@ -33,9 +33,12 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.neo4j.core.Neo4jOperations;
import org.springframework.data.neo4j.core.Neo4jTemplate;
import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager;
import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.testcontainers.containers.Neo4jContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
@@ -45,7 +48,8 @@ import org.testcontainers.junit.jupiter.Testcontainers;
@Testcontainers
class RepositoryIT {
@Container private static Neo4jContainer neo4jContainer = new Neo4jContainer().withAdminPassword(null);
@Container
private static Neo4jContainer neo4jContainer = new Neo4jContainer().withAdminPassword(null);
private final PersonRepository repository;
@@ -75,16 +79,26 @@ class RepositoryIT {
@Configuration
@EnableNeo4jRepositories
@EnableTransactionManagement
static class Config {
@Bean
public Neo4jOperations neo4jTemplate() {
String boltUrl = neo4jContainer.getBoltUrl();
Driver driver = GraphDatabase.driver(boltUrl, AuthTokens.none());
public Driver driver() {
return new Neo4jTemplate(driver);
String boltUrl = neo4jContainer.getBoltUrl();
return GraphDatabase.driver(boltUrl, AuthTokens.none());
}
}
@Bean
public PlatformTransactionManager transactionManager() {
return new Neo4jTransactionManager(driver());
}
@Bean
public Neo4jOperations neo4jTemplate() {
return new Neo4jTemplate(driver());
}
}
}

View File

@@ -20,6 +20,8 @@ package org.springframework.data.neo4j.repository.support;
import static org.assertj.core.api.Assertions.*;
import java.util.Collections;
import org.junit.jupiter.api.Test;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.Pageable;
@@ -74,7 +76,8 @@ class SimpleNeo4jRepositoryTest {
@Test
void deleteAll1NotImplemented() {
assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> repository.deleteAll(null));
assertThatExceptionOfType(UnsupportedOperationException.class)
.isThrownBy(() -> repository.deleteAll(Collections.emptyList()));
}
@Test