Skip to content

Commit

Permalink
Provide a way to configure the database being used.
Browse files Browse the repository at this point in the history
This fixes #124 by providing the a Neo4jDatabaseNameProvider.

The Neo4jDatabaseNameProvider allows for configuring the database used by all transactions managers and all templates. The configured database will be passed on to the Neo4j templates, both reactive and imperative. The Neo4j clients are still be able to use all databases, they don't know a fixed database name provider.

The database name provider must of course be provided to the transaction managers. Both reactive and imperative transaction managers keep on synchronising on the driver, but check the target database and will prevent switching the database in-between an application level transaction.

For those scenarios where a client wants to use a different database than the one configured with the default transaction manager, it must run in an implicit (aka auto commit transaction) or run a second transaction manager, configured to use the correct database.

In case interactions spawn multiple databases, several transaction managers must be applied and the propagation behaviour needs to be `REQUIRES_NEW`, meaning an ongoing transaction will be suspended when the database is switched and resumed when the inner transaction finishes.

The commit also contains the requried properties for the Spring Boot autoconfiguration.
  • Loading branch information
michael-simons committed Jan 28, 2020
1 parent f093001 commit 505d965
Show file tree
Hide file tree
Showing 27 changed files with 572 additions and 134 deletions.
1 change: 1 addition & 0 deletions pom.xml
Expand Up @@ -77,6 +77,7 @@
<module>examples/reactive-web</module>
<module>examples/imperative-web</module>
<module>examples/mapping</module>
<module>examples/multi-database</module>
</modules>

<properties>
Expand Down
Expand Up @@ -24,6 +24,7 @@
import org.neo4j.springframework.data.core.Neo4jTemplate;
import org.neo4j.springframework.data.core.mapping.Neo4jMappingContext;
import org.neo4j.springframework.data.core.transaction.Neo4jTransactionManager;
import org.neo4j.springframework.data.core.Neo4jDatabaseNameProvider;
import org.neo4j.springframework.data.repository.config.Neo4jRepositoryConfigurationExtension;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
Expand Down Expand Up @@ -60,19 +61,23 @@ public Neo4jClient neo4jClient(Driver driver) {
}

@Bean(Neo4jRepositoryConfigurationExtension.DEFAULT_NEO4J_TEMPLATE_BEAN_NAME)
public Neo4jTemplate neo4jTemplate(final Neo4jClient neo4jClient, final Neo4jMappingContext mappingContext) {
return new Neo4jTemplate(neo4jClient, mappingContext);
public Neo4jTemplate neo4jTemplate(final Neo4jClient neo4jClient, final Neo4jMappingContext mappingContext,
Neo4jDatabaseNameProvider databaseNameProvider) {

return new Neo4jTemplate(neo4jClient, mappingContext, databaseNameProvider);
}

/**
* Provides a {@link PlatformTransactionManager} for Neo4j based on the driver resulting from {@link #driver()}.
*
* @param driver The driver to synchronize against
* @param driver The driver to synchronize against
* @param databaseNameProvider The configured database name provider
* @return A platform transaction manager
*/
@Bean(Neo4jRepositoryConfigurationExtension.DEFAULT_TRANSACTION_MANAGER_BEAN_NAME)
public PlatformTransactionManager transactionManager(Driver driver) {
public PlatformTransactionManager transactionManager(Driver driver,
Neo4jDatabaseNameProvider databaseNameProvider) {

return new Neo4jTransactionManager(driver);
return new Neo4jTransactionManager(driver, databaseNameProvider);
}
}
Expand Up @@ -24,6 +24,7 @@
import org.neo4j.springframework.data.core.ReactiveNeo4jTemplate;
import org.neo4j.springframework.data.core.mapping.Neo4jMappingContext;
import org.neo4j.springframework.data.core.transaction.ReactiveNeo4jTransactionManager;
import org.neo4j.springframework.data.core.Neo4jDatabaseNameProvider;
import org.neo4j.springframework.data.repository.config.ReactiveNeo4jRepositoryConfigurationExtension;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
Expand Down Expand Up @@ -62,8 +63,9 @@ public ReactiveNeo4jClient neo4jClient(Driver driver) {

@Bean(ReactiveNeo4jRepositoryConfigurationExtension.DEFAULT_NEO4J_TEMPLATE_BEAN_NAME)
public ReactiveNeo4jTemplate neo4jTemplate(final ReactiveNeo4jClient neo4jClient,
final Neo4jMappingContext mappingContext) {
return new ReactiveNeo4jTemplate(neo4jClient, mappingContext);
final Neo4jMappingContext mappingContext, final Neo4jDatabaseNameProvider databaseNameProvider) {

return new ReactiveNeo4jTemplate(neo4jClient, mappingContext, databaseNameProvider);
}

/**
Expand All @@ -73,8 +75,8 @@ public ReactiveNeo4jTemplate neo4jTemplate(final ReactiveNeo4jClient neo4jClient
* @return A platform transaction manager
*/
@Bean(ReactiveNeo4jRepositoryConfigurationExtension.DEFAULT_TRANSACTION_MANAGER_BEAN_NAME)
public ReactiveTransactionManager reactiveTransactionManager(Driver driver) {
public ReactiveTransactionManager reactiveTransactionManager(Driver driver, Neo4jDatabaseNameProvider databaseNameProvider) {

return new ReactiveNeo4jTransactionManager(driver);
return new ReactiveNeo4jTransactionManager(driver, databaseNameProvider);
}
}
Expand Up @@ -27,6 +27,7 @@
import org.neo4j.springframework.data.core.convert.Neo4jConversions;
import org.neo4j.springframework.data.core.mapping.Neo4jMappingContext;
import org.neo4j.springframework.data.core.schema.Node;
import org.neo4j.springframework.data.core.Neo4jDatabaseNameProvider;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
Expand Down Expand Up @@ -127,4 +128,15 @@ protected final Set<Class<?>> scanForEntities(String basePackage) throws ClassNo

return initialEntitySet;
}

/**
* Configures the database name provider.
*
* @return The default database name provider, defaulting to the default database on Neo4j 4.0 and on no default on Neo4j 3.5 and prior.
*/
@Bean
protected Neo4jDatabaseNameProvider neo4jDatabaseNameProvider() {

return Neo4jDatabaseNameProvider.getDefaultDatabaseNameProvider();
}
}
Expand Up @@ -64,7 +64,6 @@ class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient {

this.driver = driver;
this.typeSystem = driver.defaultTypeSystem();

this.conversionService = new DefaultConversionService();
new Neo4jConversions().registerConvertersIn((ConverterRegistry) conversionService);
}
Expand Down
@@ -0,0 +1,80 @@
/*
* Copyright (c) 2019 "Neo4j,"
* Neo4j Sweden AB [https://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
*
* https://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.neo4j.springframework.data.core;

import java.util.Optional;

import org.apiguardian.api.API;
import org.springframework.util.Assert;

/**
* A provider interface that knows in which database repositories or either the reactive or imperative template should work.
* <p>An instance of a database name provider is only relevant when SDN-RX is used with a Neo4j 4.0+ cluster or server.
* <p>To select the default database, return an empty optional. If you return a database name, it must not be empty.
* The empty optional indicates an unset database name on the client, so that the server can decide on the default to use.
* <p>The provider is asked before any interaction of a repository or template with the cluster or server. That means you can
* in theory return different database names for each interaction. Be aware that you might end up with no data on queries
* or data stored to wrong database if you don't pay meticulously attention to the database you interact with.
*
* @author Michael J. Simons
* @soundtrack N.W.A. - Straight Outta Compton
* @since 1.0
*/
@API(status = API.Status.STABLE, since = "1.0")
@FunctionalInterface
public interface Neo4jDatabaseNameProvider {

/**
* @return The optional name of the database name to interact with. Use the empty optional to indicate the default database.
*/
Optional<String> getCurrentDatabaseName();

/**
* Creates a statically configured database name provider always answering with the configured {@code databaseName}.
*
* @param databaseName The database name to use, must not be null nor empty.
* @return A statically configured database name provider.
*/
static Neo4jDatabaseNameProvider createStaticDatabaseNameProvider(String databaseName) {

Assert.notNull(databaseName, "The database name must not be null.");
Assert.hasText(databaseName, "The database name must not be empty.");

return () -> Optional.of(databaseName);
}

/**
* A database name provider always returning the empty optional.
*
* @return A provider for the default database name.
*/
static Neo4jDatabaseNameProvider getDefaultDatabaseNameProvider() {

return DefaultNeo4jDatabaseNameProvider.INSTANCE;
}
}

enum DefaultNeo4jDatabaseNameProvider implements Neo4jDatabaseNameProvider {
INSTANCE;

@Override
public Optional<String> getCurrentDatabaseName() {
return Optional.empty();
}
}
Expand Up @@ -83,19 +83,23 @@ public final class Neo4jTemplate implements Neo4jOperations, BeanFactoryAware {

private Neo4jEvents eventSupport;

private final Neo4jDatabaseNameProvider databaseNameProvider;

public Neo4jTemplate(Neo4jClient neo4jClient) {
this(neo4jClient, new Neo4jMappingContext());
this(neo4jClient, new Neo4jMappingContext(), Neo4jDatabaseNameProvider.getDefaultDatabaseNameProvider());
}

public Neo4jTemplate(Neo4jClient neo4jClient, Neo4jMappingContext neo4jMappingContext) {
public Neo4jTemplate(Neo4jClient neo4jClient, Neo4jMappingContext neo4jMappingContext, Neo4jDatabaseNameProvider databaseNameProvider) {

Assert.notNull(neo4jClient, "The Neo4jClient is required");
Assert.notNull(neo4jMappingContext, "The Neo4jMappingContext is required");
Assert.notNull(databaseNameProvider, "The database name provider is required");

this.neo4jClient = neo4jClient;
this.neo4jMappingContext = neo4jMappingContext;
this.cypherGenerator = CypherGenerator.INSTANCE;
this.eventSupport = new Neo4jEvents(null);
this.databaseNameProvider = databaseNameProvider;
}

@Override
Expand Down Expand Up @@ -172,22 +176,28 @@ public <T> List<T> findAllById(Iterable<?> ids, Class<T> domainType) {
@Override
public <T> T save(T instance) {

return saveImpl(instance, getDatabaseName());
}

private <T> T saveImpl(T instance, @Nullable String inDatabase) {

Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getPersistentEntity(instance.getClass());
T entityToBeSaved = eventSupport.maybeCallBeforeBind(instance);
Long internalId = neo4jClient
.query(() -> renderer.render(cypherGenerator.prepareSaveOf(entityMetaData)))
.in(inDatabase)
.bind((T) entityToBeSaved)
.with(neo4jMappingContext.getRequiredBinderFunctionFor((Class<T>) entityToBeSaved.getClass()))
.fetchAs(Long.class).one().get();

PersistentPropertyAccessor<T> propertyAccessor = entityMetaData.getPropertyAccessor(entityToBeSaved);

if (!entityMetaData.isUsingInternalIds()) {
processNestedAssociations(entityMetaData, entityToBeSaved);
processNestedAssociations(entityMetaData, entityToBeSaved, inDatabase);
return entityToBeSaved;
} else {
propertyAccessor.setProperty(entityMetaData.getRequiredIdProperty(), internalId);
processNestedAssociations(entityMetaData, entityToBeSaved);
processNestedAssociations(entityMetaData, entityToBeSaved, inDatabase);

return propertyAccessor.getBean();
}
Expand All @@ -196,6 +206,8 @@ public <T> T save(T instance) {
@Override
public <T> List<T> saveAll(Iterable<T> instances) {

String databaseName = getDatabaseName();

Collection<T> entities;
if (instances instanceof Collection) {
entities = (Collection<T>) instances;
Expand All @@ -214,7 +226,7 @@ public <T> List<T> saveAll(Iterable<T> instances) {
log.debug("Saving entities using single statements.");

return entities.stream()
.map(this::save)
.map(e -> saveImpl(e, databaseName))
.collect(toList());
}

Expand All @@ -228,12 +240,13 @@ public <T> List<T> saveAll(Iterable<T> instances) {
.map(binderFunction).collect(toList());
ResultSummary resultSummary = neo4jClient
.query(() -> renderer.render(cypherGenerator.prepareSaveOfMultipleInstancesOf(entityMetaData)))
.in(databaseName)
.bind(entityList).to(NAME_OF_ENTITY_LIST_PARAM)
.run();

// Save related
entitiesToBeSaved.forEach(entityToBeSaved -> {
processNestedAssociations(entityMetaData, entityToBeSaved);
processNestedAssociations(entityMetaData, entityToBeSaved, databaseName);
});

SummaryCounters counters = resultSummary.counters();
Expand All @@ -256,6 +269,7 @@ public <T> void deleteById(Object id, Class<T> domainType) {

Statement statement = cypherGenerator.prepareDeleteOf(entityMetaData, condition);
ResultSummary summary = this.neo4jClient.query(renderer.render(statement))
.in(getDatabaseName())
.bind(id).to(nameOfParameter)
.run();

Expand All @@ -274,6 +288,7 @@ public <T> void deleteAllById(Iterable<?> ids, Class<T> domainType) {

Statement statement = cypherGenerator.prepareDeleteOf(entityMetaData, condition);
ResultSummary summary = this.neo4jClient.query(renderer.render(statement))
.in(getDatabaseName())
.bind(ids).to(nameOfParameter)
.run();

Expand All @@ -288,7 +303,7 @@ public void deleteAll(Class<?> domainType) {
log.debug(() -> String.format("Deleting all nodes with primary label %s", entityMetaData.getPrimaryLabel()));

Statement statement = cypherGenerator.prepareDeleteOf(entityMetaData);
ResultSummary summary = this.neo4jClient.query(renderer.render(statement)).run();
ResultSummary summary = this.neo4jClient.query(renderer.render(statement)).in(getDatabaseName()).run();

log.debug(() -> String.format("Deleted %d nodes and %d relationships.", summary.counters().nodesDeleted(),
summary.counters().relationshipsDeleted()));
Expand All @@ -309,7 +324,7 @@ private <T> ExecutableQuery<T> createExecutableQuery(Class<T> domainType, Statem
return toExecutableQuery(preparedQuery);
}

private void processNestedAssociations(Neo4jPersistentEntity<?> neo4jPersistentEntity, Object parentObject) {
private void processNestedAssociations(Neo4jPersistentEntity<?> neo4jPersistentEntity, Object parentObject, @Nullable String inDatabase) {

PersistentPropertyAccessor<?> propertyAccessor = neo4jPersistentEntity.getPropertyAccessor(parentObject);

Expand All @@ -330,6 +345,7 @@ private void processNestedAssociations(Neo4jPersistentEntity<?> neo4jPersistentE
relationshipContext.getRelationship(), targetNodeDescription.getPrimaryLabel());

neo4jClient.query(renderer.render(relationshipRemoveQuery))
.in(inDatabase)
.bind(fromId).to(FROM_ID_PARAMETER_NAME).run();
}

Expand All @@ -347,7 +363,7 @@ private void processNestedAssociations(Neo4jPersistentEntity<?> neo4jPersistentE
valueToBeSaved = eventSupport.maybeCallBeforeBind(valueToBeSaved);

Long relatedInternalId = saveRelatedNode(valueToBeSaved, relationshipContext.getAssociationTargetType(),
targetNodeDescription);
targetNodeDescription, inDatabase);

// handle creation of relationship depending on properties on relationship or not
RelationshipStatementHolder statementHolder = relationshipContext.hasRelationshipWithProperties()
Expand All @@ -362,6 +378,7 @@ private void processNestedAssociations(Neo4jPersistentEntity<?> neo4jPersistentE
relatedValue);

neo4jClient.query(renderer.render(statementHolder.getRelationshipCreationQuery()))
.in(inDatabase)
.bind(fromId).to(FROM_ID_PARAMETER_NAME)
.bindAll(statementHolder.getProperties())
.run();
Expand All @@ -373,17 +390,23 @@ private void processNestedAssociations(Neo4jPersistentEntity<?> neo4jPersistentE
targetPropertyAccessor
.setProperty(targetNodeDescription.getRequiredIdProperty(), relatedInternalId);
}
processNestedAssociations(targetNodeDescription, valueToBeSaved);
processNestedAssociations(targetNodeDescription, valueToBeSaved, inDatabase);
}
});
}

private <Y> Long saveRelatedNode(Object entity, Class<Y> entityType, NodeDescription targetNodeDescription) {
private <Y> Long saveRelatedNode(Object entity, Class<Y> entityType, NodeDescription targetNodeDescription, @Nullable String inDatabase) {
return neo4jClient.query(() -> renderer.render(cypherGenerator.prepareSaveOf(targetNodeDescription)))
.in(inDatabase)
.bind((Y) entity).with(neo4jMappingContext.getRequiredBinderFunctionFor(entityType))
.fetchAs(Long.class).one().get();
}

private String getDatabaseName() {

return this.databaseNameProvider.getCurrentDatabaseName().orElse(null);
}

@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {

Expand All @@ -395,6 +418,7 @@ public <T> ExecutableQuery<T> toExecutableQuery(PreparedQuery<T> preparedQuery)

Neo4jClient.MappingSpec<T> mappingSpec = this
.neo4jClient.query(preparedQuery.getCypherQuery())
.in(getDatabaseName())
.bindAll(preparedQuery.getParameters())
.fetchAs(preparedQuery.getResultType());
Neo4jClient.RecordFetchSpec<T> fetchSpec = preparedQuery
Expand Down

0 comments on commit 505d965

Please sign in to comment.