diff --git a/lombok.config b/lombok.config index e50c7ea43..b94b2470c 100644 --- a/lombok.config +++ b/lombok.config @@ -1,2 +1,2 @@ lombok.nonNull.exceptionType = IllegalArgumentException -lombok.log.fieldName = LOG + diff --git a/pom.xml b/pom.xml index bcba9e1f8..3dc0d26ec 100644 --- a/pom.xml +++ b/pom.xml @@ -21,8 +21,8 @@ 4.0.0 - org.springframework.data.build spring-data-parent + org.springframework.data.build 2.2.0.BUILD-SNAPSHOT @@ -97,6 +97,7 @@ 3.0.0-M2 ${java.version} ${java.version} + 2.23.4 1.7.2 3.5.2 UTF-8 @@ -151,6 +152,16 @@ pom import + + org.mockito + mockito-core + ${mockito.version} + + + org.mockito + mockito-junit-jupiter + ${mockito.version} + org.neo4j neo4j diff --git a/spring-data-neo4j/pom.xml b/spring-data-neo4j/pom.xml index 6464c1960..ecff24d53 100644 --- a/spring-data-neo4j/pom.xml +++ b/spring-data-neo4j/pom.xml @@ -21,8 +21,8 @@ 4.0.0 - org.springframework.data spring-data-neo4j-rx-parent + org.springframework.data 1.0.0.BUILD-SNAPSHOT @@ -58,6 +58,12 @@ rxjava true + + javax.transaction + jta + 1.1 + test + org.apiguardian apiguardian-api @@ -72,6 +78,16 @@ junit-jupiter test + + org.mockito + mockito-core + test + + + org.mockito + mockito-junit-jupiter + test + org.neo4j.driver neo4j-java-driver @@ -81,6 +97,10 @@ slf4j-api test + + org.slf4j + slf4j-api + org.springframework spring-beans diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java index 5ee20ab38..8a7ffbc0f 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java @@ -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 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(); } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/session/DefaultNeo4jSessionFactory.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/session/DefaultNeo4jSessionFactory.java deleted file mode 100644 index aacd35ef7..000000000 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/session/DefaultNeo4jSessionFactory.java +++ /dev/null @@ -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 { - - @Override - public Session getSession() { - - throw new UnsupportedOperationException("Not there yet."); - } -} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/session/Neo4jSessionFactorySupport.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/session/Neo4jSessionFactorySupport.java deleted file mode 100644 index 7c5f3d4eb..000000000 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/session/Neo4jSessionFactorySupport.java +++ /dev/null @@ -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 Type of the driver. - * @author Michael J. Simons - */ -abstract class Neo4jSessionFactorySupport implements Neo4jSessionFactory { -} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/DefaultNeo4jStatementRunnerSupplier.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/DefaultNeo4jStatementRunnerSupplier.java new file mode 100644 index 000000000..d18445f6a --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/DefaultNeo4jStatementRunnerSupplier.java @@ -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 { + + 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(); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jResourceHolder.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jResourceHolder.java new file mode 100644 index 000000000..73b99b3a1 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jResourceHolder.java @@ -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. + *

+ * Note: 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(); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jSessionSynchronization.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jSessionSynchronization.java new file mode 100644 index 000000000..6b6022756 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jSessionSynchronization.java @@ -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 { + + 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(); + } + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManager.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManager.java new file mode 100644 index 000000000..f956b7153 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManager.java @@ -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 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(); + } + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/session/Neo4jSessionFactory.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/StatementRunnerSupplier.java similarity index 65% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/session/Neo4jSessionFactory.java rename to spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/StatementRunnerSupplier.java index 0d79c36ea..4d2a2934c 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/session/Neo4jSessionFactory.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/StatementRunnerSupplier.java @@ -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. + *

+ * Note: Intended for internal usage only. * * @author Michael J. Simons */ -public interface Neo4jSessionFactory { - - Session getSession(); +@FunctionalInterface +public interface StatementRunnerSupplier extends Supplier { } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/session/package-info.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/package-info.java similarity index 70% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/session/package-info.java rename to spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/package-info.java index 7340af78b..f75ba3547 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/session/package-info.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/transaction/package-info.java @@ -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; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/EnableNeo4jRepositories.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/EnableNeo4jRepositories.java index 2a2bdef7d..90cdedc3b 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/EnableNeo4jRepositories.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/EnableNeo4jRepositories.java @@ -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 diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/Neo4jRepositoryConfigurationExtension.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/Neo4jRepositoryConfigurationExtension.java index a43f39e6f..e05e15a09 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/Neo4jRepositoryConfigurationExtension.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/Neo4jRepositoryConfigurationExtension.java @@ -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)); } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java index 5db19abb3..ceebc17b0 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepository.java @@ -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 implements Neo4jRepository { private final Neo4jOperations neo4jOperations; @@ -51,11 +56,13 @@ class SimpleNeo4jRepository implements Neo4jRepository { } @Override + @Transactional public S save(S entity) { throw new UnsupportedOperationException("Not there yet."); } @Override + @Transactional public Iterable saveAll(Iterable entities) { throw new UnsupportedOperationException("Not there yet."); } @@ -86,23 +93,31 @@ class SimpleNeo4jRepository implements Neo4jRepository { } @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 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 diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/DefaultNeo4jStatementRunnerSupplierTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/DefaultNeo4jStatementRunnerSupplierTest.java new file mode 100644 index 000000000..7efe3a9e9 --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/DefaultNeo4jStatementRunnerSupplierTest.java @@ -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 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 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 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()); + } +} diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManagerTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManagerTest.java new file mode 100644 index 000000000..7b4ac3a84 --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/transaction/Neo4jTransactionManagerTest.java @@ -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(); + } +} diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java index c51f7736c..dd5e2ca94 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/integration/RepositoryIT.java @@ -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()); + } + } } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepositoryTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepositoryTest.java index c8113e9a7..8267394bd 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepositoryTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/repository/support/SimpleNeo4jRepositoryTest.java @@ -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