Create NodeManagerFactory, NodeManager and related infrastructure.

A NodeManager will be used to create, read, update, delete and query managed objects.
It will use a short lived persistence context.

This adds the basic skeletons for doing this but especially the infrastructure to allow the following usage:
* User configures a driver bean
* User provides a NodeManagerFactory using that driver and a given set of classes to be managed
* The factory gets augmented to handle transactions and facilitate Spring Datas class scanning mechanism

The Spring Boot starter will automatically provide a configured NodeManager instance.
This commit is contained in:
Michael Simons
2019-04-01 17:03:27 +02:00
parent c62cd13a19
commit f0b3172c85
32 changed files with 846 additions and 171 deletions

21
etc/adr/adr-003.adoc Normal file
View File

@@ -0,0 +1,21 @@
== ADR 3: Public classes that are part of internal API only must be final
=== Status
accepted, open
=== Context
Due to the fact that we are not yet on the module path, we need to have some classes public defined that are not meant
to be part of the public API.
=== Decision
Those classes should be marked `@API(status = API.Status.INTERNAL, since = "1.0")` as well as made final to at least
prevent people from inheriting from them.
We need to add architecture rules in jQAssistant.
=== Consequences
Potentially problems with some Spring proxies.
Need to be solved on a case by case incident.

View File

@@ -0,0 +1,60 @@
/*
* 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;
import org.apiguardian.api.API;
import org.neo4j.driver.v1.StatementRunner;
import org.neo4j.driver.v1.Transaction;
import org.springframework.data.neo4j.core.context.DefaultPersistenceContext;
import org.springframework.data.neo4j.core.context.PersistenceContext;
import org.springframework.data.neo4j.core.schema.Schema;
import org.springframework.lang.Nullable;
/**
* @author Michael J. Simons
*/
@API(status = API.Status.INTERNAL, since = "1.0")
class DefaultNodeManager implements NodeManager {
private final PersistenceContext persistenceContext = new DefaultPersistenceContext();
private final Schema schema;
private final Neo4jTemplate neo4jTemplate;
private final Transaction transaction;
DefaultNodeManager(Schema schema, StatementRunner statementRunner) {
this.schema = schema;
this.neo4jTemplate = new Neo4jTemplate(() -> statementRunner);
this.transaction = statementRunner instanceof Transaction ? (Transaction) statementRunner : null;
}
@Override
@Nullable
public Transaction getTransaction() {
return transaction;
}
@Override
public Object executeQuery(String query) {
return neo4jTemplate.executeQuery(query);
}
}

View File

@@ -18,11 +18,12 @@
*/
package org.springframework.data.neo4j.core;
import java.util.function.Supplier;
import org.neo4j.driver.v1.Driver;
import org.neo4j.driver.v1.StatementResult;
import org.neo4j.driver.v1.StatementRunner;
import org.springframework.data.neo4j.core.transaction.DefaultNeo4jStatementRunnerSupplier;
import org.springframework.data.neo4j.core.transaction.StatementRunnerSupplier;
import org.springframework.data.neo4j.core.transaction.Neo4jTransactionUtils;
/**
* Default implementation of {@link Neo4jOperations}. Uses the Neo4j Java driver to connect to and interact with the
@@ -33,10 +34,14 @@ import org.springframework.data.neo4j.core.transaction.StatementRunnerSupplier;
*/
public class Neo4jTemplate implements Neo4jOperations {
private StatementRunnerSupplier<StatementRunner> statementRunnerSupplier;
private Supplier<StatementRunner> statementRunnerSupplier;
public Neo4jTemplate(Driver driver) {
this.statementRunnerSupplier = new DefaultNeo4jStatementRunnerSupplier(driver);
this(() -> Neo4jTransactionUtils.retrieveTransactionalStatementRunner(driver));
}
Neo4jTemplate(Supplier<StatementRunner> statementRunnerSupplier) {
this.statementRunnerSupplier = statementRunnerSupplier;
}
@Override

View File

@@ -0,0 +1,46 @@
/*
* 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;
import org.apiguardian.api.API;
import org.neo4j.driver.v1.Transaction;
import org.springframework.lang.Nullable;
/**
* Entry point for creating queries that return managed Nodes. The node manager is not supposed to be kept around
* for longer than necessary. Try to keep your transactions short to avoid memory pressure due to keeping track of
* managed nodes.
*
* @author Michael J. Simons
*/
@API(status = API.Status.STABLE, since = "1.0")
public interface NodeManager {
/**
* Clears all managed entities and flushes any open state to the underlying storage.
*/
default void flush() {
}
@Nullable
Transaction getTransaction();
@API(status = API.Status.EXPERIMENTAL)
Object executeQuery(String query);
}

View File

@@ -0,0 +1,145 @@
/*
* 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;
import lombok.extern.slf4j.Slf4j;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import org.apiguardian.api.API;
import org.neo4j.driver.v1.AccessMode;
import org.neo4j.driver.v1.Driver;
import org.neo4j.driver.v1.StatementRunner;
import org.springframework.data.neo4j.core.schema.Scanner;
import org.springframework.data.neo4j.core.schema.Schema;
import org.springframework.lang.Nullable;
/**
* Creates ready to use instances of {@link NodeManager}.
*
* @author Michael J. Simons
*/
@API(status = API.Status.STABLE, since = "1.0")
@Slf4j
public final class NodeManagerFactory {
/**
* Driver that is used to create new sessions, either by directly invoking it or through Springs transactional utils.
*/
private final Driver driver;
/**
* The initial set of classes that will be scanned in {@link #initialize()} to build the schema for node managers
* belonging to this factory.
*/
private final Set<Class<?>> initialPersistentClasses;
/** The scanner used to scan the initial set of persistent classes for creating a schema. */
private Scanner scanner = new NoopScanner();
@Nullable
private Schema schema;
private Function<Driver, StatementRunner> statementRunnerProvider = sourceDriver -> sourceDriver
.session(AccessMode.WRITE, Collections.emptyList()).beginTransaction();
/**
* Creates a new instance of a factory producing {@link NodeManager node managers}. When used in a transactional setup,
* i.e. with the {@link org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager}, make sure to use
* the same {@link Driver driver instance} for both the node and the transaction manager.
* <p>
* Spring Boots autoconfiguration for SDN RX will make sure that the same driver is used for both concerns.
*
* @param driver The driver used to obtain statement runners from when creating instances of node managers.
* @param initialPersistentClasses The set of classes that should be initially scanned
*/
public NodeManagerFactory(Driver driver, Class<?>... initialPersistentClasses) {
this.driver = driver;
this.initialPersistentClasses = new HashSet<>();
Arrays.stream(initialPersistentClasses).forEach(this.initialPersistentClasses::add);
}
/**
* Creates a new node manager. The returned manager is supposed to have a short lifetime. When used in a Spring setup,
* this method should not called directly by client code. Instead the client code should use an injected instance of
* {@link NodeManager}, which will participate in Springs application based transaction or in JTA transactions.
*
* @return A new node manager
*/
public NodeManager createNodeManager() {
Objects.requireNonNull(schema, "A schema is required. Did you call #initialize() before using this factory?");
return new DefaultNodeManager(schema, statementRunnerProvider.apply(driver));
}
/**
* Configures a provider for extracting sessions/transactions from a Neo4j driver. This method is not to be called
* from application code and only used by internal API.
*
* @param statementRunnerProvider A required provider of statement runners
*/
@API(status = API.Status.INTERNAL, since = "1.0")
public void setStatementRunnerProvider(Function<Driver, StatementRunner> statementRunnerProvider) {
Objects.requireNonNull(statementRunnerProvider, "A node manager factory requires a provider of statement runners.");
this.statementRunnerProvider = statementRunnerProvider;
}
/**
* Configures the scanner used to build a schema for domain objects. This method is not to be called from application
* code and only used by internal API.
*
* @param scanner
*/
@API(status = API.Status.INTERNAL, since = "1.0")
public void setScanner(Scanner scanner) {
Objects.requireNonNull(scanner, "A node manager factory requires a scanner.");
this.scanner = scanner;
}
/**
* This initializes this factory and is usually called by Springs infrastructure and only useful as standalone call
* when the node manager factory is used without Spring.
*/
public void initialize() {
log.info("Initializing schema with {} persistent classes", this.initialPersistentClasses.size());
this.schema = scanner.scan(Collections.unmodifiableSet(this.initialPersistentClasses));
}
/**
* A noop implementation of a Schema scanner, only provided to create instances of a {@link NodeManagerFactory} that
* are in a valid state without booting up a whole context.
*/
private static class NoopScanner implements Scanner {
@Override
public Schema scan(Collection<Class<?>> persistentClasses) {
return new Schema();
}
}
}

View File

@@ -21,5 +21,5 @@ package org.springframework.data.neo4j.core.context;
/**
* @author Michael J. Simons
*/
class PersistenceContextImpl implements PersistenceContext {
public class DefaultPersistenceContext implements PersistenceContext {
}

View File

@@ -25,12 +25,12 @@ import org.springframework.data.util.TypeInformation;
/**
* @author Michael J. Simons
*/
class Neo4jPersistentEntityImpl<T> extends BasicPersistentEntity<T, Neo4jPersistentProperty>
class DefaultNeo4jPersistentEntity<T> extends BasicPersistentEntity<T, Neo4jPersistentProperty>
implements Neo4jPersistentEntity<T> {
private final String primaryLabel;
Neo4jPersistentEntityImpl(TypeInformation<T> information) {
DefaultNeo4jPersistentEntity(TypeInformation<T> information) {
super(information);
Node nodeAnnotation = this.findAnnotation(Node.class);

View File

@@ -27,7 +27,7 @@ import org.springframework.data.mapping.model.SimpleTypeHolder;
/**
* @author Michael J. Simons
*/
class Neo4jPersistentPropertyImpl extends AnnotationBasedPersistentProperty<Neo4jPersistentProperty>
class DefaultNeo4jPersistentProperty extends AnnotationBasedPersistentProperty<Neo4jPersistentProperty>
implements Neo4jPersistentProperty {
/**
@@ -37,7 +37,7 @@ class Neo4jPersistentPropertyImpl extends AnnotationBasedPersistentProperty<Neo4
* @param owner must not be {@literal null}.
* @param simpleTypeHolder
*/
Neo4jPersistentPropertyImpl(Property property,
DefaultNeo4jPersistentProperty(Property property,
PersistentEntity<?, Neo4jPersistentProperty> owner,
SimpleTypeHolder simpleTypeHolder) {

View File

@@ -19,6 +19,7 @@
package org.springframework.data.neo4j.core.mapping;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
@@ -50,12 +51,14 @@ public class MappingContextBasedScannerImpl implements Scanner {
}
@Override
public Schema scan() {
public Schema scan(Collection<Class<?>> persistentClasses) {
final Schema schema = new Schema();
neo4jMappingContext.getPersistentEntities().forEach(m ->
schema.registerNodeDescription(describeAsNode(m))
);
persistentClasses.forEach(clazz -> {
Neo4jPersistentEntity<?> m = neo4jMappingContext.getPersistentEntity(clazz);
schema.registerNodeDescription(describeAsNode(m));
});
return schema;
}

View File

@@ -37,7 +37,7 @@ public class Neo4jMappingContext extends AbstractMappingContext<Neo4jPersistentE
@Override
protected <T> Neo4jPersistentEntity<?> createPersistentEntity(TypeInformation<T> typeInformation) {
return new Neo4jPersistentEntityImpl<>(typeInformation);
return new DefaultNeo4jPersistentEntity<>(typeInformation);
}
/*
@@ -48,6 +48,6 @@ public class Neo4jMappingContext extends AbstractMappingContext<Neo4jPersistentE
protected Neo4jPersistentProperty createPersistentProperty(Property property,
Neo4jPersistentEntity<?> neo4jPersistentProperties, SimpleTypeHolder simpleTypeHolder) {
return new Neo4jPersistentPropertyImpl(property, neo4jPersistentProperties, simpleTypeHolder);
return new DefaultNeo4jPersistentProperty(property, neo4jPersistentProperties, simpleTypeHolder);
}
}

View File

@@ -18,6 +18,8 @@
*/
package org.springframework.data.neo4j.core.schema;
import java.util.Collection;
import org.apiguardian.api.API;
/**
@@ -32,7 +34,8 @@ public interface Scanner {
/**
* Scans the relevant classes and creates a schema.
*
* @param persistentClasses The classes to scan
* @return The new schema.
*/
Schema scan();
Schema scan(Collection<Class<?>> persistentClasses);
}

View File

@@ -1,64 +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.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

@@ -32,17 +32,17 @@ import org.springframework.util.Assert;
*
* @author Michael J. Simons
*/
public class Neo4jResourceHolder extends ResourceHolderSupport {
public class Neo4jConnectionHolder extends ResourceHolderSupport {
private final Session session;
private final Transaction transaction;
Neo4jResourceHolder(Session session) {
Neo4jConnectionHolder(Session session) {
this(session, TransactionConfig.empty());
}
Neo4jResourceHolder(Session session, TransactionConfig transactionConfig) {
Neo4jConnectionHolder(Session session, TransactionConfig transactionConfig) {
this.session = session;
this.transaction = this.session.beginTransaction(transactionConfig);

View File

@@ -24,17 +24,17 @@ 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.
* participating in a non-native Neo4j transaction, such as a Jta transaction.
*/
class Neo4jSessionSynchronization
extends ResourceHolderSynchronization<Neo4jResourceHolder, Object> {
extends ResourceHolderSynchronization<Neo4jConnectionHolder, Object> {
private final Neo4jResourceHolder localResourceHolder;
private final Neo4jConnectionHolder localConnectionHolder;
Neo4jSessionSynchronization(Neo4jResourceHolder resourceHolder, Driver driver) {
Neo4jSessionSynchronization(Neo4jConnectionHolder connectionHolder, Driver driver) {
super(resourceHolder, driver);
this.localResourceHolder = resourceHolder;
super(connectionHolder, driver);
this.localConnectionHolder = connectionHolder;
}
/*
@@ -51,7 +51,7 @@ class Neo4jSessionSynchronization
* @see org.springframework.transaction.support.ResourceHolderSynchronization#processResourceAfterCommit(java.lang.Object)
*/
@Override
protected void processResourceAfterCommit(Neo4jResourceHolder resourceHolder) {
protected void processResourceAfterCommit(Neo4jConnectionHolder resourceHolder) {
super.processResourceAfterCommit(resourceHolder);
@@ -67,8 +67,8 @@ class Neo4jSessionSynchronization
@Override
public void afterCompletion(int status) {
if (status == TransactionSynchronization.STATUS_ROLLED_BACK && localResourceHolder.hasActiveTransaction()) {
localResourceHolder.rollback();
if (status == TransactionSynchronization.STATUS_ROLLED_BACK && localConnectionHolder.hasActiveTransaction()) {
localConnectionHolder.rollback();
}
super.afterCompletion(status);
@@ -79,7 +79,7 @@ class Neo4jSessionSynchronization
* @see org.springframework.transaction.support.ResourceHolderSynchronization#releaseResource(java.lang.Object, java.lang.Object)
*/
@Override
protected void releaseResource(Neo4jResourceHolder resourceHolder, Object resourceKey) {
protected void releaseResource(Neo4jConnectionHolder resourceHolder, Object resourceKey) {
if (resourceHolder.hasActiveSession()) {
resourceHolder.close();

View File

@@ -18,15 +18,23 @@
*/
package org.springframework.data.neo4j.core.transaction;
import lombok.extern.slf4j.Slf4j;
import java.time.Duration;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
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.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.data.neo4j.core.NodeManagerFactory;
import org.springframework.lang.Nullable;
import org.springframework.transaction.IllegalTransactionStateException;
import org.springframework.transaction.InvalidIsolationLevelException;
@@ -41,23 +49,44 @@ import org.springframework.transaction.support.TransactionSynchronizationUtils;
import org.springframework.util.Assert;
/**
* Dedicated {@link org.springframework.transaction.PlatformTransactionManager} for native Neo4j transactions.
* Dedicated {@link org.springframework.transaction.PlatformTransactionManager} for native Neo4j transactions. This
* transaction manager will synchronize a pair of a native Neo4j session/transaction and one instance of a
* {@link org.springframework.data.neo4j.core.NodeManager} with the transaction.
*
* @author Michael J. Simons
*/
public class Neo4jTransactionManager extends AbstractPlatformTransactionManager {
@Slf4j
public class Neo4jTransactionManager extends AbstractPlatformTransactionManager implements BeanFactoryAware {
private final Driver driver;
@Nullable
private NodeManagerFactory nodeManagerFactory;
@API(status = API.Status.STABLE, since = "1.0")
public Neo4jTransactionManager(Driver driver) {
this.driver = driver;
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
try {
this.setNodeManagerFactory(beanFactory.getBean(NodeManagerFactory.class));
} catch (NoSuchBeanDefinitionException ex) {
log.warn("Found no instance of {}", NodeManagerFactory.class);
}
}
public void setNodeManagerFactory(@Nullable NodeManagerFactory nodeManagerFactory) {
// TODO Check if the NodeManager uses the same datasource aka driver as we do
this.nodeManagerFactory = nodeManagerFactory;
}
@Override
protected Object doGetTransaction() throws TransactionException {
Neo4jResourceHolder resourceHolder = (Neo4jResourceHolder) TransactionSynchronizationManager
Neo4jConnectionHolder resourceHolder = (Neo4jConnectionHolder) TransactionSynchronizationManager
.getResource(driver);
return new Neo4jTransactionObject(resourceHolder);
}
@@ -83,13 +112,21 @@ public class Neo4jTransactionManager extends AbstractPlatformTransactionManager
try {
Session session = this.driver.session(accessMode, bookmarks);
Neo4jResourceHolder resourceHolder = new Neo4jResourceHolder(session, transactionConfig);
transactionObject.setResourceHolder(resourceHolder);
Neo4jConnectionHolder connectionHolder = new Neo4jConnectionHolder(session, transactionConfig);
connectionHolder.setSynchronizedWithTransaction(true);
transactionObject.setResourceHolder(connectionHolder);
TransactionSynchronizationManager.bindResource(this.driver, connectionHolder);
resourceHolder.setSynchronizedWithTransaction(true);
TransactionSynchronizationManager.bindResource(driver, resourceHolder);
if (this.nodeManagerFactory != null) {
NodeManagerHolder nodeManagerHolder = new NodeManagerHolder(this.nodeManagerFactory.createNodeManager());
nodeManagerHolder.setSynchronizedWithTransaction(true);
transactionObject.setNodeManagerHolder(nodeManagerHolder);
TransactionSynchronizationManager.bindResource(this.nodeManagerFactory, nodeManagerHolder);
}
} catch (Exception ex) {
ex.printStackTrace();
throw new TransactionSystemException(String.format("Could not open a new Neo4j session: %s", ex.getMessage()));
}
}
@@ -134,8 +171,14 @@ public class Neo4jTransactionManager extends AbstractPlatformTransactionManager
Neo4jTransactionObject transactionObject = extractNeo4jTransaction(transaction);
transactionObject.getRequiredResourceHolder().close();
transactionObject.getNodeManagerHolder()
.map(NodeManagerHolder::getNodeManager)
.ifPresent(nodeManager -> nodeManager.flush());
TransactionSynchronizationManager.unbindResource(driver);
if (this.nodeManagerFactory != null) {
TransactionSynchronizationManager.unbindResource(this.nodeManagerFactory);
}
}
private static TransactionConfig createTransactionConfigFrom(TransactionDefinition definition) {
@@ -171,16 +214,21 @@ public class Neo4jTransactionManager extends AbstractPlatformTransactionManager
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";
private static final String RESOURCE_HOLDER_NOT_PRESENT_MESSAGE = "Neo4jConnectionHolder 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;
@Nullable
private Neo4jConnectionHolder resourceHolder;
Neo4jTransactionObject(@Nullable Neo4jResourceHolder resourceHolder) {
@Nullable
private NodeManagerHolder nodeManagerHolder;
Neo4jTransactionObject(@Nullable Neo4jConnectionHolder resourceHolder) {
this.resourceHolder = resourceHolder;
}
@@ -190,23 +238,31 @@ public class Neo4jTransactionManager extends AbstractPlatformTransactionManager
*
* @param resourceHolder A newly created resource holder with a fresh drivers session,
*/
void setResourceHolder(@Nullable Neo4jResourceHolder resourceHolder) {
void setResourceHolder(@Nullable Neo4jConnectionHolder resourceHolder) {
this.resourceHolder = resourceHolder;
}
void setNodeManagerHolder(@Nullable NodeManagerHolder nodeManagerHolder) {
this.nodeManagerHolder = nodeManagerHolder;
}
/**
* @return {@literal true} if a {@link Neo4jResourceHolder} is set.
* @return {@literal true} if a {@link Neo4jConnectionHolder} is set.
*/
boolean hasResourceHolder() {
return resourceHolder != null;
}
Neo4jResourceHolder getRequiredResourceHolder() {
Neo4jConnectionHolder getRequiredResourceHolder() {
Assert.state(hasResourceHolder(), RESOURCE_HOLDER_NOT_PRESENT_MESSAGE);
return resourceHolder;
}
Optional<NodeManagerHolder> getNodeManagerHolder() {
return Optional.ofNullable(nodeManagerHolder);
}
void setRollbackOnly() {
getRequiredResourceHolder().setRollbackOnly();

View File

@@ -0,0 +1,91 @@
/*
* 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.util.Collections;
import org.neo4j.driver.v1.AccessMode;
import org.neo4j.driver.v1.Driver;
import org.neo4j.driver.v1.StatementRunner;
import org.springframework.data.neo4j.core.NodeManager;
import org.springframework.data.neo4j.core.NodeManagerFactory;
import org.springframework.transaction.support.TransactionSynchronizationManager;
/**
* Internal use only.
*/
public final class Neo4jTransactionUtils {
public static StatementRunner retrieveTransactionalStatementRunner(Driver driver) {
if (!TransactionSynchronizationManager.isSynchronizationActive()) {
return driver.session();
}
// Try existing transaction
Neo4jConnectionHolder connectionHolder = (Neo4jConnectionHolder) TransactionSynchronizationManager
.getResource(driver);
if (connectionHolder != null) {
return connectionHolder.getTransaction();
}
// Manually create a new synchronization
connectionHolder = new Neo4jConnectionHolder(driver.session(AccessMode.WRITE, Collections.emptyList()));
connectionHolder.setSynchronizedWithTransaction(true);
TransactionSynchronizationManager.registerSynchronization(
new Neo4jSessionSynchronization(connectionHolder, driver));
TransactionSynchronizationManager.bindResource(driver, connectionHolder);
return connectionHolder.getTransaction();
}
public static NodeManager retrieveTransactionalNodeManager(NodeManagerFactory nodeManagerFactory) {
if (!TransactionSynchronizationManager.isSynchronizationActive()) {
return nodeManagerFactory.createNodeManager();
}
// Try existing transaction
NodeManagerHolder nodeManagerHolder = (NodeManagerHolder) TransactionSynchronizationManager
.getResource(nodeManagerFactory);
if (nodeManagerHolder != null) {
return nodeManagerHolder.getNodeManager();
}
// Manually create a new synchronization
nodeManagerHolder = new NodeManagerHolder(nodeManagerFactory.createNodeManager());
nodeManagerHolder.setSynchronizedWithTransaction(true);
TransactionSynchronizationManager.registerSynchronization(
new NodeManagerSynchronization(nodeManagerHolder, nodeManagerFactory));
TransactionSynchronizationManager.bindResource(nodeManagerFactory, nodeManagerHolder);
return nodeManagerHolder.getNodeManager();
}
private Neo4jTransactionUtils() {
}
}

View File

@@ -18,15 +18,30 @@
*/
package org.springframework.data.neo4j.core.transaction;
import java.util.function.Supplier;
import org.springframework.data.neo4j.core.NodeManager;
import org.springframework.lang.Nullable;
import org.springframework.transaction.support.ResourceHolderSupport;
import org.springframework.util.Assert;
/**
* Used for obtaining {@link org.neo4j.driver.v1.Session session bound (standard or reactive)} bound statement runners.
* Dedicated holder for storing a NodeManager inside a transaction.
* <p>
* <strong>Note:</strong> Intended for internal usage only.
*
* @author Michael J. Simons
*/
@FunctionalInterface
public interface StatementRunnerSupplier<T> extends Supplier<T> {
public class NodeManagerHolder extends ResourceHolderSupport {
@Nullable
private final NodeManager nodeManager;
public NodeManagerHolder(@Nullable NodeManager nodeManager) {
this.nodeManager = nodeManager;
}
public NodeManager getNodeManager() {
Assert.state(this.nodeManager != null, "No NodeManager available");
return this.nodeManager;
}
}

View File

@@ -0,0 +1,44 @@
/*
* 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.springframework.data.neo4j.core.NodeManagerFactory;
import org.springframework.transaction.support.ResourceHolderSynchronization;
/**
* TODO Update Licence header https
*
* @author Michael J. Simons
*/
public class NodeManagerSynchronization
extends ResourceHolderSynchronization<NodeManagerHolder, Object> {
private final NodeManagerHolder localNodeManagerHolder;
NodeManagerSynchronization(NodeManagerHolder nodeManagerHolder, NodeManagerFactory nodeManagerFactory) {
super(nodeManagerHolder, nodeManagerFactory);
this.localNodeManagerHolder = nodeManagerHolder;
}
@Override
protected void flushResource(NodeManagerHolder resourceHolder) {
resourceHolder.getNodeManager().flush();
}
}

View File

@@ -31,7 +31,7 @@ import org.springframework.beans.factory.FactoryBean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;
import org.springframework.core.annotation.AliasFor;
import org.springframework.data.neo4j.core.Neo4jTemplate;
import org.springframework.data.neo4j.core.NodeManager;
import org.springframework.data.neo4j.repository.support.Neo4jRepositoryFactoryBean;
import org.springframework.transaction.PlatformTransactionManager;
@@ -78,9 +78,9 @@ public @interface EnableNeo4jRepositories {
Class<?> repositoryFactoryBeanClass() default Neo4jRepositoryFactoryBean.class;
/**
* Configures the name of the {@link Neo4jTemplate} bean to be used with the repositories detected.
* Configures the name of the {@link NodeManager} bean to be used with the repositories detected.
*/
String neo4jTemplateRef() default DEFAULT_NEO4J_TEMPLATE_BEAN_NAME;
String nodeManagerFactoryRef() default DEFAULT_NODE_MANAGER_FACTORY_BEAN_NAME;
/**
* Configures the name of the {@link PlatformTransactionManager} bean definition to be used to create repositories

View File

@@ -24,9 +24,12 @@ import java.util.Collections;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
import org.springframework.data.neo4j.core.schema.Node;
import org.springframework.data.neo4j.repository.Neo4jRepository;
import org.springframework.data.neo4j.repository.support.Neo4jRepositoryFactoryBean;
import org.springframework.data.neo4j.repository.support.NodeManagerFactoryBean;
import org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport;
import org.springframework.data.repository.config.RepositoryConfigurationSource;
@@ -48,9 +51,14 @@ public class Neo4jRepositoryConfigurationExtension extends RepositoryConfigurati
*/
static final String GENERATE_BEAN_NAME = "(generated)";
static final String DEFAULT_NEO4J_TEMPLATE_BEAN_NAME = "neo4jTemplate";
static final String DEFAULT_NODE_MANAGER_FACTORY_BEAN_NAME = "nodeManagerFactory";
static final String DEFAULT_TRANSACTION_MANAGER_BEAN_NAME = "transactionManager";
/**
* Holds the name of the shared NodeManagerBean created from the factory with the configured name.
*/
private String generatedNodeManagerBeanName;
/*
* (non-Javadoc)
* @see org.springframework.data.repository.config14.RepositoryConfigurationExtension#getRepositoryFactoryBeanClassName()
@@ -89,7 +97,57 @@ public class Neo4jRepositoryConfigurationExtension extends RepositoryConfigurati
builder.addPropertyValue("transactionManager",
source.getAttribute("transactionManagerRef").orElse(DEFAULT_TRANSACTION_MANAGER_BEAN_NAME));
builder.addPropertyReference("neo4jOperations",
source.getAttribute("neo4jTemplateRef").orElse(DEFAULT_NEO4J_TEMPLATE_BEAN_NAME));
builder.addPropertyReference("nodeManager", this.generatedNodeManagerBeanName);
}
@Override
public void registerBeansForRoot(BeanDefinitionRegistry registry,
RepositoryConfigurationSource config) {
// Mapping context
AbstractBeanDefinition neo4jMappingContextBeanDefinition = BeanDefinitionBuilder
.rootBeanDefinition(Neo4jMappingContext.class)
.getBeanDefinition();
String nameOfMappingContextBean = registerWithSourceAndGeneratedBeanName(
neo4jMappingContextBeanDefinition, registry, config);
// Augmented node manager factory (creating injectable, shared instances of NodeManager)
String nameOfNodeManagerFactory = config.getAttribute("nodeManagerFactoryRef")
.orElse(DEFAULT_NODE_MANAGER_FACTORY_BEAN_NAME);
AbstractBeanDefinition sharedSessionCreatorBeanDefinition = BeanDefinitionBuilder
.rootBeanDefinition(NodeManagerFactoryBean.class)
.addConstructorArgReference(nameOfNodeManagerFactory)
.addConstructorArgReference(nameOfMappingContextBean)
.getBeanDefinition();
this.generatedNodeManagerBeanName = registerWithSourceAndGeneratedBeanName(
sharedSessionCreatorBeanDefinition, registry, config);
}
/**
* Uses a generated bean name if {@code configuredBeanName} is equal to {@link #GENERATE_BEAN_NAME}, otherwise uses
* the configured bean name to register the new bean. Does not check if there's already a bean under the configured
* name but throws a {@link org.springframework.beans.factory.BeanDefinitionStoreException}. Checks whether a bean is
* already registered under {@code configuredBeanName} in the given {@link BeanDefinitionRegistry} and uses a
* generated name for registering the bean instead. If not, the suggested bean name is used.
*
* @param bean must not be {@literal null}.
* @param registry must not be {@literal null}.
* @param configuredBeanName must not be {@literal null} or empty.
* @param source must not be {@literal null}.
* @return the bean name used for registering the given {@link AbstractBeanDefinition}
* @throws org.springframework.beans.factory.BeanDefinitionStoreException if the BeanDefinition is invalid or if there
* is already a BeanDefinition for the specified bean name * (and we are not allowed to override it)
*/
private static String registerWithGeneratedNameOrUseConfigured(AbstractBeanDefinition bean,
BeanDefinitionRegistry registry, String configuredBeanName, Object source) {
String registeredBeanName = configuredBeanName;
if (GENERATE_BEAN_NAME.equals(configuredBeanName)) {
registeredBeanName = registerWithSourceAndGeneratedBeanName(bean, registry, source);
} else {
bean.setSource(source);
registry.registerBeanDefinition(configuredBeanName, bean);
}
return registeredBeanName;
}
}

View File

@@ -18,7 +18,7 @@
*/
package org.springframework.data.neo4j.repository.query;
import org.springframework.data.neo4j.core.Neo4jOperations;
import org.springframework.data.neo4j.core.NodeManager;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.repository.query.RepositoryQuery;
@@ -26,10 +26,11 @@ import org.springframework.data.repository.query.RepositoryQuery;
* Implementation of {@link RepositoryQuery} for derived finder methods.
*
* @author Gerrit Meier
**/
* @author Michael J. Simons
*/
public class PartTreeNeo4jQuery extends AbstractNeo4jQuery {
public PartTreeNeo4jQuery(Neo4jQueryMethod queryMethod, Neo4jOperations neo4jOperations) {
public PartTreeNeo4jQuery(Neo4jQueryMethod queryMethod, NodeManager nodeManager) {
}
@Override

View File

@@ -18,7 +18,7 @@
*/
package org.springframework.data.neo4j.repository.query;
import org.springframework.data.neo4j.core.Neo4jOperations;
import org.springframework.data.neo4j.core.NodeManager;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.repository.query.RepositoryQuery;
@@ -26,21 +26,22 @@ import org.springframework.data.repository.query.RepositoryQuery;
* Implementation of {@link RepositoryQuery} for String based custom Cypher query.
*
* @author Gerrit Meier
* @author Michael J. Simons
*/
public class StringBasedNeo4jQuery extends AbstractNeo4jQuery {
private final Neo4jQueryMethod queryMethod;
private final Neo4jOperations neo4jOperations;
private final NodeManager nodeManager;
public StringBasedNeo4jQuery(Neo4jQueryMethod queryMethod, Neo4jOperations neo4jOperations) {
public StringBasedNeo4jQuery(Neo4jQueryMethod queryMethod, NodeManager nodeManager) {
this.queryMethod = queryMethod;
this.neo4jOperations = neo4jOperations;
this.nodeManager = nodeManager;
}
@Override
public Object execute(Object[] parameters) {
return neo4jOperations.executeQuery(queryMethod.getAnnotatedQuery());
return nodeManager.executeQuery(queryMethod.getAnnotatedQuery());
}
@Override

View File

@@ -21,7 +21,7 @@ package org.springframework.data.neo4j.repository.support;
import java.lang.reflect.Method;
import java.util.Optional;
import org.springframework.data.neo4j.core.Neo4jOperations;
import org.springframework.data.neo4j.core.NodeManager;
import org.springframework.data.neo4j.repository.Neo4jRepository;
import org.springframework.data.neo4j.repository.query.Neo4jQueryMethod;
import org.springframework.data.neo4j.repository.query.PartTreeNeo4jQuery;
@@ -41,13 +41,14 @@ import org.springframework.data.repository.query.RepositoryQuery;
* Factory to create {@link Neo4jRepository} instances.
*
* @author Gerrit Meier
* @author Michael J. Simons
*/
class Neo4jRepositoryFactory extends RepositoryFactorySupport {
final class Neo4jRepositoryFactory extends RepositoryFactorySupport {
private final Neo4jOperations neo4jOperations;
private final NodeManager nodeManager;
Neo4jRepositoryFactory(Neo4jOperations neo4jOperations) {
this.neo4jOperations = neo4jOperations;
Neo4jRepositoryFactory(NodeManager nodeManager) {
this.nodeManager = nodeManager;
}
@Override
@@ -57,7 +58,7 @@ class Neo4jRepositoryFactory extends RepositoryFactorySupport {
@Override
protected Object getTargetRepository(RepositoryInformation metadata) {
return getTargetRepositoryViaReflection(metadata, neo4jOperations);
return getTargetRepositoryViaReflection(metadata, nodeManager);
}
@Override
@@ -73,18 +74,18 @@ class Neo4jRepositoryFactory extends RepositoryFactorySupport {
protected Optional<QueryLookupStrategy> getQueryLookupStrategy(Key key,
QueryMethodEvaluationContextProvider evaluationContextProvider) {
return Optional.of(new Neo4jQueryLookupStrategy(neo4jOperations, evaluationContextProvider));
return Optional.of(new Neo4jQueryLookupStrategy(nodeManager, evaluationContextProvider));
}
private class Neo4jQueryLookupStrategy implements QueryLookupStrategy {
private final Neo4jOperations neo4jOperations;
private final NodeManager nodeManager;
private final QueryMethodEvaluationContextProvider evaluationContextProvider;
private Neo4jQueryLookupStrategy(Neo4jOperations neo4jOperations,
private Neo4jQueryLookupStrategy(NodeManager nodeManager,
QueryMethodEvaluationContextProvider evaluationContextProvider) {
this.neo4jOperations = neo4jOperations;
this.nodeManager = nodeManager;
this.evaluationContextProvider = evaluationContextProvider;
}
@@ -97,10 +98,10 @@ class Neo4jRepositoryFactory extends RepositoryFactorySupport {
Neo4jQueryMethod queryMethod = new Neo4jQueryMethod(method, metadata, factory);
if (queryMethod.hasAnnotatedQuery()) {
return new StringBasedNeo4jQuery(queryMethod, neo4jOperations);
return new StringBasedNeo4jQuery(queryMethod, nodeManager);
}
return new PartTreeNeo4jQuery(queryMethod, neo4jOperations);
return new PartTreeNeo4jQuery(queryMethod, nodeManager);
}
}
}

View File

@@ -20,7 +20,7 @@ package org.springframework.data.neo4j.repository.support;
import java.io.Serializable;
import org.springframework.data.neo4j.core.Neo4jOperations;
import org.springframework.data.neo4j.core.NodeManager;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.core.support.RepositoryFactorySupport;
import org.springframework.data.repository.core.support.TransactionalRepositoryFactoryBeanSupport;
@@ -35,7 +35,7 @@ import org.springframework.data.repository.core.support.TransactionalRepositoryF
public class Neo4jRepositoryFactoryBean<T extends Repository<S, ID>, S, ID extends Serializable>
extends TransactionalRepositoryFactoryBeanSupport<T, S, ID> {
private Neo4jOperations neo4jOperations;
private NodeManager nodeManager;
/**
* Creates a new {@link TransactionalRepositoryFactoryBeanSupport} for the given repository interface.
@@ -46,13 +46,13 @@ public class Neo4jRepositoryFactoryBean<T extends Repository<S, ID>, S, ID exten
super(repositoryInterface);
}
public void setNeo4jOperations(Neo4jOperations neo4jOperations) {
this.neo4jOperations = neo4jOperations;
public void setNodeManager(NodeManager nodeManager) {
this.nodeManager = nodeManager;
}
@Override
protected RepositoryFactorySupport doCreateRepositoryFactory() {
return new Neo4jRepositoryFactory(neo4jOperations);
return new Neo4jRepositoryFactory(nodeManager);
}
}

View File

@@ -0,0 +1,67 @@
/*
* 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.repository.support;
import org.apiguardian.api.API;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.data.neo4j.core.NodeManager;
import org.springframework.data.neo4j.core.NodeManagerFactory;
import org.springframework.data.neo4j.core.mapping.MappingContextBasedScannerImpl;
import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
import org.springframework.data.neo4j.core.transaction.Neo4jTransactionUtils;
/**
* This is a shim that integrates a {@link NodeManagerFactory} with Spring Datas infrastructure. It takes in the factory
* that needs to be provided by the user and the mapping context provided by our infrastructure. The node manager factory
* is than augment by our {@link Neo4jTransactionUtils} so that it doesn't use unmanaged native transaction, but managed
* native transaction. Furthermore, the default noop scanner is replaced by a scanner based on our Neo4j mapping context.
*
* @author Gerrit Meier
* @author Michael J. Simons
*/
@API(status = API.Status.INTERNAL, since = "1.0")
public final class NodeManagerFactoryBean implements InitializingBean, FactoryBean<NodeManager> {
private final NodeManagerFactory target;
private final Neo4jMappingContext neo4jMappingContext;
public NodeManagerFactoryBean(NodeManagerFactory target, Neo4jMappingContext neo4jMappingContext) {
this.target = target;
this.neo4jMappingContext = neo4jMappingContext;
}
@Override
public NodeManager getObject() {
return SharedNodeManagerCreator.createSharedNodeManager(this.target);
}
@Override
public Class<?> getObjectType() {
return NodeManager.class;
}
@Override
public void afterPropertiesSet() {
target.setStatementRunnerProvider(Neo4jTransactionUtils::retrieveTransactionalStatementRunner);
target.setScanner(new MappingContextBasedScannerImpl(this.neo4jMappingContext));
target.initialize();
}
}

View File

@@ -0,0 +1,127 @@
/*
* 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.repository.support;
import lombok.extern.slf4j.Slf4j;
import java.io.Serializable;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Function;
import org.springframework.data.neo4j.core.NodeManager;
import org.springframework.data.neo4j.core.NodeManagerFactory;
import org.springframework.data.neo4j.core.transaction.Neo4jTransactionUtils;
import org.springframework.lang.Nullable;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.util.ReflectionUtils;
/**
* @author Michael J. Simons
*/
@Slf4j
final class SharedNodeManagerCreator {
private static final Set<String> TRANSACTION_REQUIRING_METHODS;
static {
Set<String> tmp = new HashSet<>();
tmp.add("save");
tmp.add("delete");
TRANSACTION_REQUIRING_METHODS = Collections.unmodifiableSet(tmp);
}
public static NodeManager createSharedNodeManager(NodeManagerFactory nodeManagerFactory) {
return (NodeManager) Proxy.newProxyInstance(SharedNodeManagerCreator.class.getClassLoader(),
new Class<?>[] { NodeManager.class }, new SharedNodeManagerInvocationHandler(nodeManagerFactory));
}
private static class SharedNodeManagerInvocationHandler implements InvocationHandler, Serializable {
private final NodeManagerFactory targetFactory;
SharedNodeManagerInvocationHandler(NodeManagerFactory targetFactory) {
this.targetFactory = targetFactory;
}
@Override
@Nullable
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
switch (methodName) {
case "equals":
// Only consider equal when proxies are identical.
return (proxy == args[0]);
case "hashCode":
// Use hashCode of proxy.
return hashCode();
case "toString":
// Deliver toString without touching a target Session.
return "Shared Session proxy for target factory [" + this.targetFactory + "]";
default:
Function<NodeManager, Object> methodCall =
targetSession -> ReflectionUtils.invokeMethod(method, targetSession, args);
return invokeInTransaction(methodName, methodCall);
}
}
private Object invokeInTransaction(String methodName, Function<NodeManager, Object> methodCall) {
// Determine current Session: either the transactional one
// managed by the factory or a temporary one for the given invocation.
NodeManager target = Neo4jTransactionUtils.retrieveTransactionalNodeManager(this.targetFactory);
if (TRANSACTION_REQUIRING_METHODS.contains(methodName)) {
if (target == null || (!TransactionSynchronizationManager.isActualTransactionActive()
&& target.getTransaction() != null)) {
throw new IllegalStateException("No NodeManager with actual transaction available "
+ "for current thread - cannot reliably process '" + methodName + "' call");
}
}
// Regular Session operations.
boolean isNewSession = false;
if (target == null) {
log.debug("Creating new Session for shared Session invocation");
target = this.targetFactory.createNodeManager();
isNewSession = true;
}
// Invoke method on current Session.
try {
return methodCall.apply(target);
} finally {
if (isNewSession) {
target.flush();
}
}
}
}
private SharedNodeManagerCreator() {
}
}

View File

@@ -24,7 +24,7 @@ import org.springframework.data.domain.Example;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.neo4j.core.Neo4jOperations;
import org.springframework.data.neo4j.core.NodeManager;
import org.springframework.data.neo4j.repository.Neo4jRepository;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
@@ -39,10 +39,10 @@ import org.springframework.transaction.annotation.Transactional;
@Transactional(readOnly = true)
class SimpleNeo4jRepository<T, ID> implements Neo4jRepository<T, ID> {
private final Neo4jOperations neo4jOperations;
private final NodeManager nodeManager;
SimpleNeo4jRepository(Neo4jOperations neo4jOperations) {
this.neo4jOperations = neo4jOperations;
SimpleNeo4jRepository(NodeManager nodeManager) {
this.nodeManager = nodeManager;
}
@Override

View File

@@ -26,7 +26,7 @@ import org.springframework.data.neo4j.core.schema.Node;
/**
* @author Michael J. Simons
*/
public class Neo4jPersistentEntityImplTest {
public class DefaultNeo4JPersistentEntityTest {
private Neo4jMappingContext mappingContext = new Neo4jMappingContext();

View File

@@ -49,7 +49,7 @@ class MappingContextBasedScannerImplTest {
neo4jMappingContext.initialize();
final Scanner scanner = new MappingContextBasedScannerImpl(neo4jMappingContext);
Schema schema = scanner.scan();
Schema schema = scanner.scan(new HashSet<>(Arrays.asList(BikeNode.class, UserNode.class)));
Optional<NodeDescription> optionalUserNodeDescription = schema.getNodeDescription("User");
assertThat(optionalUserNodeDescription)

View File

@@ -54,7 +54,7 @@ import org.springframework.transaction.support.TransactionTemplate;
*/
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
class DefaultNeo4jStatementRunnerSupplierTest {
class Neo4jTransactionUtilsTest {
@Mock
private Driver driver;
@@ -90,11 +90,8 @@ class DefaultNeo4jStatementRunnerSupplierTest {
@Test
void shouldWorkWithoutSynchronizations() {
StatementRunnerSupplier<StatementRunner> statementRunnerSupplier = new DefaultNeo4jStatementRunnerSupplier(
driver);
@SuppressWarnings({ "unused" })
StatementRunner statementRunner = statementRunnerSupplier.get();
StatementRunner statementRunner = Neo4jTransactionUtils.retrieveTransactionalStatementRunner(driver);
verify(driver).session();
verifyNoMoreInteractions(driver, session, transaction);
@@ -107,8 +104,6 @@ class DefaultNeo4jStatementRunnerSupplierTest {
Neo4jTransactionManager txManager = new Neo4jTransactionManager(driver);
TransactionTemplate txTemplate = new TransactionTemplate(txManager);
StatementRunnerSupplier<StatementRunner> statementRunnerSupplier = new DefaultNeo4jStatementRunnerSupplier(
driver);
txTemplate.execute(new TransactionCallbackWithoutResult() {
@@ -120,7 +115,8 @@ class DefaultNeo4jStatementRunnerSupplierTest {
assertThat(TransactionSynchronizationManager.hasResource(driver)).isTrue();
@SuppressWarnings({ "unused" })
StatementRunner statementRunner = statementRunnerSupplier.get();
StatementRunner statementRunner = Neo4jTransactionUtils
.retrieveTransactionalStatementRunner(driver);
transactionStatus.setRollbackOnly();
}
@@ -142,8 +138,6 @@ class DefaultNeo4jStatementRunnerSupplierTest {
Neo4jTransactionManager txManager = new Neo4jTransactionManager(driver);
TransactionTemplate txTemplate = new TransactionTemplate(txManager);
StatementRunnerSupplier<StatementRunner> statementRunnerSupplier = new DefaultNeo4jStatementRunnerSupplier(
driver);
txTemplate.execute(new TransactionCallbackWithoutResult() {
@@ -151,7 +145,8 @@ class DefaultNeo4jStatementRunnerSupplierTest {
protected void doInTransactionWithoutResult(TransactionStatus outerStatus) {
@SuppressWarnings({ "unused" })
StatementRunner outerStatementRunner = statementRunnerSupplier.get();
StatementRunner outerStatementRunner = Neo4jTransactionUtils
.retrieveTransactionalStatementRunner(driver);
assertThat(outerStatus.isNewTransaction()).isTrue();
txTemplate.execute(new TransactionCallbackWithoutResult() {
@@ -162,7 +157,8 @@ class DefaultNeo4jStatementRunnerSupplierTest {
assertThat(innerStatus.isNewTransaction()).isFalse();
@SuppressWarnings({ "unused" })
StatementRunner innerStatementRunner = statementRunnerSupplier.get();
StatementRunner innerStatementRunner = Neo4jTransactionUtils
.retrieveTransactionalStatementRunner(driver);
}
});
@@ -193,8 +189,6 @@ class DefaultNeo4jStatementRunnerSupplierTest {
JtaTransactionManager txManager = new JtaTransactionManager(userTransaction);
TransactionTemplate txTemplate = new TransactionTemplate(txManager);
StatementRunnerSupplier<StatementRunner> statementRunnerSupplier = new DefaultNeo4jStatementRunnerSupplier(
driver);
txTemplate.execute(new TransactionCallbackWithoutResult() {
@@ -206,7 +200,8 @@ class DefaultNeo4jStatementRunnerSupplierTest {
assertThat(TransactionSynchronizationManager.hasResource(driver)).isFalse();
@SuppressWarnings({ "unused" })
StatementRunner statementRunner = statementRunnerSupplier.get();
StatementRunner statementRunner = Neo4jTransactionUtils
.retrieveTransactionalStatementRunner(driver);
assertThat(TransactionSynchronizationManager.hasResource(driver)).isTrue();
}
@@ -233,8 +228,6 @@ class DefaultNeo4jStatementRunnerSupplierTest {
JtaTransactionManager txManager = new JtaTransactionManager(userTransaction);
TransactionTemplate txTemplate = new TransactionTemplate(txManager);
StatementRunnerSupplier<StatementRunner> statementRunnerSupplier = new DefaultNeo4jStatementRunnerSupplier(
driver);
txTemplate.execute(new TransactionCallbackWithoutResult() {
@@ -246,7 +239,8 @@ class DefaultNeo4jStatementRunnerSupplierTest {
assertThat(TransactionSynchronizationManager.hasResource(driver)).isFalse();
@SuppressWarnings({ "unused" })
StatementRunner statementRunner = statementRunnerSupplier.get();
StatementRunner statementRunner = Neo4jTransactionUtils
.retrieveTransactionalStatementRunner(driver);
assertThat(TransactionSynchronizationManager.hasResource(driver)).isTrue();

View File

@@ -23,9 +23,11 @@ import java.util.List;
import org.neo4j.driver.v1.Record;
import org.springframework.data.neo4j.repository.Neo4jRepository;
import org.springframework.data.neo4j.repository.query.Query;
import org.springframework.transaction.annotation.Transactional;
public interface PersonRepository extends Neo4jRepository<Person, Long> {
@Transactional
@Query("RETURN 1")
List<Record> customQuery();

View File

@@ -31,8 +31,7 @@ import org.neo4j.driver.v1.Record;
import org.springframework.beans.factory.annotation.Autowired;
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.NodeManagerFactory;
import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager;
import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories;
import org.springframework.test.context.ContextConfiguration;
@@ -90,15 +89,15 @@ class RepositoryIT {
}
@Bean
public PlatformTransactionManager transactionManager() {
public NodeManagerFactory nodeManagerFactory(Driver driver) {
return new Neo4jTransactionManager(driver());
return new NodeManagerFactory(driver, Person.class);
}
@Bean
public Neo4jOperations neo4jTemplate() {
public PlatformTransactionManager transactionManager(Driver driver) {
return new Neo4jTemplate(driver());
return new Neo4jTransactionManager(driver);
}
}
}