fix: Unify Bookmarkmanager creation and usage for good.

This commit is contained in:
Michael Simons
2023-10-04 14:42:53 +02:00
parent ff494f5b85
commit d33425b154
13 changed files with 500 additions and 36 deletions

View File

@@ -737,7 +737,7 @@ instance it will most likely not make any difference.
In a cluster this can be a sensible approach only and if only you can tolerate stale reads and are not in danger of
overwriting old data.
You need to provide the following configuration in your system and make sure that SDN uses the transaction manager:
The following configuration creates a "noop" variant of the bookmark manager that will be picked up from relevant classes.
[source,java,indent=0,tabsize=4]
.BookmarksDisabledConfig.java
@@ -745,25 +745,20 @@ You need to provide the following configuration in your system and make sure tha
import org.neo4j.driver.Driver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.neo4j.core.DatabaseSelectionProvider;
import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager;
import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
@Configuration
public class BookmarksDisabledConfig {
@Bean
public PlatformTransactionManager transactionManager(
Driver driver, DatabaseSelectionProvider databaseNameProvider) {
public Neo4jBookmarkManager neo4jBookmarkManager() {
Neo4jBookmarkManager bookmarkManager = Neo4jBookmarkManager.noop(); // <.>
return new Neo4jTransactionManager(
driver, databaseNameProvider, bookmarkManager);
return Neo4jBookmarkManager.noop();
}
}
----
<.> Get an instance of the Noop bookmark manager
You can configure the pairs of `Neo4jTransactionManager/Neo4jClient` and `ReactiveNeo4jTransactionManager/ReactiveNeo4jClient` individually, but we recommend in doing so only when you already configuring them for specific database selection needs.
[[faq.annotations.specific]]
== Do I need to use Neo4j specific annotations?

View File

@@ -35,12 +35,16 @@ import org.neo4j.driver.Session;
import org.neo4j.driver.Value;
import org.neo4j.driver.summary.ResultSummary;
import org.neo4j.driver.types.TypeSystem;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.converter.ConverterRegistry;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.support.PersistenceExceptionTranslator;
import org.springframework.data.neo4j.core.convert.Neo4jConversions;
import org.springframework.data.neo4j.core.support.BookmarkManagerReference;
import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager;
import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager;
import org.springframework.data.neo4j.core.transaction.Neo4jTransactionUtils;
@@ -56,7 +60,7 @@ import org.springframework.util.StringUtils;
* @author Michael J. Simons
* @since 6.0
*/
final class DefaultNeo4jClient implements Neo4jClient {
final class DefaultNeo4jClient implements Neo4jClient, ApplicationContextAware {
private final Driver driver;
private @Nullable final DatabaseSelectionProvider databaseSelectionProvider;
@@ -65,14 +69,14 @@ final class DefaultNeo4jClient implements Neo4jClient {
private final Neo4jPersistenceExceptionTranslator persistenceExceptionTranslator = new Neo4jPersistenceExceptionTranslator();
// Local bookmark manager when using outside managed transactions
private final Neo4jBookmarkManager bookmarkManager;
private final BookmarkManagerReference bookmarkManager;
DefaultNeo4jClient(Builder builder) {
this.driver = builder.driver;
this.databaseSelectionProvider = builder.databaseSelectionProvider;
this.userSelectionProvider = builder.userSelectionProvider;
this.bookmarkManager = builder.bookmarkManager != null ? builder.bookmarkManager : Neo4jBookmarkManager.create();
this.bookmarkManager = new BookmarkManagerReference(Neo4jBookmarkManager::create, builder.bookmarkManager);
this.conversionService = new DefaultConversionService();
Optional.ofNullable(builder.neo4jConversions).orElseGet(Neo4jConversions::new).registerConvertersIn((ConverterRegistry) conversionService);
@@ -82,13 +86,19 @@ final class DefaultNeo4jClient implements Neo4jClient {
public QueryRunner getQueryRunner(DatabaseSelection databaseSelection, UserSelection impersonatedUser) {
QueryRunner queryRunner = Neo4jTransactionManager.retrieveTransaction(driver, databaseSelection, impersonatedUser);
Collection<Bookmark> lastBookmarks = bookmarkManager.getBookmarks();
Collection<Bookmark> lastBookmarks = bookmarkManager.resolve().getBookmarks();
if (queryRunner == null) {
queryRunner = driver.session(Neo4jTransactionUtils.sessionConfig(false, lastBookmarks, databaseSelection, impersonatedUser));
}
return new DelegatingQueryRunner(queryRunner, lastBookmarks, bookmarkManager::updateBookmarks);
return new DelegatingQueryRunner(queryRunner, lastBookmarks, bookmarkManager.resolve()::updateBookmarks);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.bookmarkManager.setApplicationContext(applicationContext);
}
private static class DelegatingQueryRunner implements QueryRunner {

View File

@@ -26,11 +26,15 @@ import org.neo4j.driver.reactivestreams.ReactiveSession;
import org.neo4j.driver.summary.ResultSummary;
import org.neo4j.driver.types.TypeSystem;
import org.reactivestreams.Publisher;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.converter.ConverterRegistry;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.dao.DataAccessException;
import org.springframework.data.neo4j.core.convert.Neo4jConversions;
import org.springframework.data.neo4j.core.support.BookmarkManagerReference;
import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager;
import org.springframework.data.neo4j.core.transaction.Neo4jTransactionUtils;
import org.springframework.data.neo4j.core.transaction.ReactiveNeo4jTransactionManager;
@@ -59,7 +63,7 @@ import java.util.function.Supplier;
* @soundtrack Die Toten Hosen - Im Auftrag des Herrn
* @since 6.0
*/
final class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient {
final class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient, ApplicationContextAware {
private final Driver driver;
private @Nullable final ReactiveDatabaseSelectionProvider databaseSelectionProvider;
@@ -68,7 +72,7 @@ final class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient {
private final Neo4jPersistenceExceptionTranslator persistenceExceptionTranslator = new Neo4jPersistenceExceptionTranslator();
// Local bookmark manager when using outside managed transactions
private final Neo4jBookmarkManager bookmarkManager;
private final BookmarkManagerReference bookmarkManager;
DefaultReactiveNeo4jClient(Builder builder) {
@@ -78,7 +82,7 @@ final class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient {
this.conversionService = new DefaultConversionService();
Optional.ofNullable(builder.neo4jConversions).orElseGet(Neo4jConversions::new).registerConvertersIn((ConverterRegistry) conversionService);
this.bookmarkManager = builder.bookmarkManager != null ? builder.bookmarkManager : Neo4jBookmarkManager.createReactive();
this.bookmarkManager = new BookmarkManagerReference(Neo4jBookmarkManager::createReactive, builder.bookmarkManager);
}
@Override
@@ -88,12 +92,18 @@ final class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient {
.flatMap(targetDatabaseAndUser ->
ReactiveNeo4jTransactionManager.retrieveReactiveTransaction(driver, targetDatabaseAndUser.getT1(), targetDatabaseAndUser.getT2())
.map(ReactiveQueryRunner.class::cast)
.zipWith(Mono.just(bookmarkManager.getBookmarks()))
.zipWith(Mono.just(bookmarkManager.resolve().getBookmarks()))
.switchIfEmpty(Mono.fromSupplier(() -> {
Collection<Bookmark> lastBookmarks = bookmarkManager.getBookmarks();
Collection<Bookmark> lastBookmarks = bookmarkManager.resolve().getBookmarks();
return Tuples.of(driver.session(ReactiveSession.class, Neo4jTransactionUtils.sessionConfig(false, lastBookmarks, targetDatabaseAndUser.getT1(), targetDatabaseAndUser.getT2())), lastBookmarks);
})))
.map(t -> new DelegatingQueryRunner(t.getT1(), t.getT2(), bookmarkManager::updateBookmarks));
.map(t -> new DelegatingQueryRunner(t.getT1(), t.getT2(), bookmarkManager.resolve()::updateBookmarks));
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
bookmarkManager.setApplicationContext(applicationContext);
}
private static class DelegatingQueryRunner implements ReactiveQueryRunner {

View File

@@ -0,0 +1,94 @@
/*
* Copyright 2011-2023 the original author or authors.
*
* 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.springframework.data.neo4j.core.support;
import java.util.function.Supplier;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager;
import org.springframework.lang.Nullable;
/**
* Don't use outside SDN code. You have been warned.
*
* @author Michael J. Simons
*/
public final class BookmarkManagerReference implements ApplicationContextAware {
private final Supplier<Neo4jBookmarkManager> defaultBookmarkManagerSupplier;
private ObjectProvider<Neo4jBookmarkManager> neo4jBookmarkManagers = new ObjectProvider<Neo4jBookmarkManager>() {
@Override
public Neo4jBookmarkManager getObject(Object... args) throws BeansException {
throw new BeanCreationException("This provider can't create new beans");
}
@Override
public Neo4jBookmarkManager getIfAvailable() throws BeansException {
return null;
}
@Override
public Neo4jBookmarkManager getIfUnique() throws BeansException {
return null;
}
@Override
public Neo4jBookmarkManager getObject() throws BeansException {
throw new BeanCreationException("This provider can't create new beans");
}
};
@Nullable
private volatile Neo4jBookmarkManager bookmarkManager;
private ApplicationEventPublisher applicationEventPublisher;
public BookmarkManagerReference(Supplier<Neo4jBookmarkManager> defaultBookmarkManagerSupplier, @Nullable Neo4jBookmarkManager bookmarkManager) {
this.defaultBookmarkManagerSupplier = defaultBookmarkManagerSupplier;
this.bookmarkManager = bookmarkManager;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.neo4jBookmarkManagers = applicationContext.getBeanProvider(Neo4jBookmarkManager.class);
this.applicationEventPublisher = applicationContext;
if (this.bookmarkManager != null) {
this.bookmarkManager.setApplicationEventPublisher(this.applicationEventPublisher);
}
}
public Neo4jBookmarkManager resolve() {
Neo4jBookmarkManager result = this.bookmarkManager;
if (result == null) {
synchronized (this) {
result = this.bookmarkManager;
if (result == null) {
this.bookmarkManager = neo4jBookmarkManagers.getIfAvailable(this.defaultBookmarkManagerSupplier);
this.bookmarkManager.setApplicationEventPublisher(this.applicationEventPublisher);
result = this.bookmarkManager;
}
}
}
return result;
}
}

View File

@@ -30,6 +30,7 @@ import org.springframework.data.neo4j.core.DatabaseSelection;
import org.springframework.data.neo4j.core.DatabaseSelectionProvider;
import org.springframework.data.neo4j.core.UserSelection;
import org.springframework.data.neo4j.core.UserSelectionProvider;
import org.springframework.data.neo4j.core.support.BookmarkManagerReference;
import org.springframework.lang.Nullable;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionException;
@@ -136,7 +137,7 @@ public final class Neo4jTransactionManager extends AbstractPlatformTransactionMa
*/
private final UserSelectionProvider userSelectionProvider;
private final Neo4jBookmarkManager bookmarkManager;
private final BookmarkManagerReference bookmarkManager;
/**
* This will create a transaction manager for the default database.
@@ -181,14 +182,13 @@ public final class Neo4jTransactionManager extends AbstractPlatformTransactionMa
this.userSelectionProvider = builder.userSelectionProvider == null ?
UserSelectionProvider.getDefaultSelectionProvider() :
builder.userSelectionProvider;
this.bookmarkManager =
builder.bookmarkManager == null ? Neo4jBookmarkManager.create() : builder.bookmarkManager;
this.bookmarkManager = new BookmarkManagerReference(Neo4jBookmarkManager::create, builder.bookmarkManager);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.bookmarkManager.setApplicationEventPublisher(applicationContext);
this.bookmarkManager.setApplicationContext(applicationContext);
}
/**
@@ -298,7 +298,7 @@ public final class Neo4jTransactionManager extends AbstractPlatformTransactionMa
try {
// Prepare configuration data
Neo4jTransactionContext context = new Neo4jTransactionContext(
databaseSelectionProvider.getDatabaseSelection(), userSelectionProvider.getUserSelection(), bookmarkManager.getBookmarks());
databaseSelectionProvider.getDatabaseSelection(), userSelectionProvider.getUserSelection(), bookmarkManager.resolve().getBookmarks());
// Configure and open session together with a native transaction
Session session = this.driver.session(
@@ -341,7 +341,7 @@ public final class Neo4jTransactionManager extends AbstractPlatformTransactionMa
Neo4jTransactionObject transactionObject = extractNeo4jTransaction(status);
Neo4jTransactionHolder transactionHolder = transactionObject.getRequiredResourceHolder();
Collection<Bookmark> newBookmarks = transactionHolder.commit();
this.bookmarkManager.updateBookmarks(transactionHolder.getBookmarks(), newBookmarks);
this.bookmarkManager.resolve().updateBookmarks(transactionHolder.getBookmarks(), newBookmarks);
}
@Override

View File

@@ -30,6 +30,7 @@ import org.springframework.data.neo4j.core.DatabaseSelection;
import org.springframework.data.neo4j.core.ReactiveDatabaseSelectionProvider;
import org.springframework.data.neo4j.core.ReactiveUserSelectionProvider;
import org.springframework.data.neo4j.core.UserSelection;
import org.springframework.data.neo4j.core.support.BookmarkManagerReference;
import org.springframework.lang.Nullable;
import org.springframework.transaction.NoTransactionException;
import org.springframework.transaction.TransactionDefinition;
@@ -133,7 +134,7 @@ public final class ReactiveNeo4jTransactionManager extends AbstractReactiveTrans
*/
private final ReactiveUserSelectionProvider userSelectionProvider;
private final Neo4jBookmarkManager bookmarkManager;
private final BookmarkManagerReference bookmarkManager;
/**
* This will create a transaction manager for the default database.
@@ -178,14 +179,13 @@ public final class ReactiveNeo4jTransactionManager extends AbstractReactiveTrans
this.userSelectionProvider = builder.userSelectionProvider == null ?
ReactiveUserSelectionProvider.getDefaultSelectionProvider() :
builder.userSelectionProvider;
this.bookmarkManager =
builder.bookmarkManager == null ? Neo4jBookmarkManager.createReactive() : builder.bookmarkManager;
this.bookmarkManager = new BookmarkManagerReference(Neo4jBookmarkManager::createReactive, builder.bookmarkManager);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.bookmarkManager.setApplicationEventPublisher(applicationContext);
this.bookmarkManager.setApplicationContext(applicationContext);
}
/**
@@ -292,7 +292,7 @@ public final class ReactiveNeo4jTransactionManager extends AbstractReactiveTrans
userSelectionProvider
.getUserSelection()
.switchIfEmpty(Mono.just(UserSelection.connectedUser())),
(databaseSelection, userSelection) -> new Neo4jTransactionContext(databaseSelection, userSelection, bookmarkManager.getBookmarks()))
(databaseSelection, userSelection) -> new Neo4jTransactionContext(databaseSelection, userSelection, bookmarkManager.resolve().getBookmarks()))
.map(context -> Tuples.of(context, this.driver.session(ReactiveSession.class, Neo4jTransactionUtils.sessionConfig(readOnly, context.getBookmarks(), context.getDatabaseSelection(), context.getUserSelection()))))
.flatMap(contextAndSession -> Mono.fromDirect(contextAndSession.getT2().beginTransaction(transactionConfig)).single()
.map(nativeTransaction -> new ReactiveNeo4jTransactionHolder(contextAndSession.getT1(),
@@ -325,7 +325,7 @@ public final class ReactiveNeo4jTransactionManager extends AbstractReactiveTrans
ReactiveNeo4jTransactionHolder holder = extractNeo4jTransaction(genericReactiveTransaction)
.getRequiredResourceHolder();
return holder.commit()
.doOnNext(bookmark -> bookmarkManager.updateBookmarks(holder.getBookmarks(), bookmark))
.doOnNext(bookmark -> bookmarkManager.resolve().updateBookmarks(holder.getBookmarks(), bookmark))
.then();
}

View File

@@ -52,6 +52,7 @@ import org.neo4j.driver.types.TypeSystem;
import org.springframework.data.neo4j.core.DatabaseSelection;
import org.springframework.data.neo4j.core.UserSelection;
import org.springframework.data.neo4j.core.Neo4jClient;
import org.springframework.data.neo4j.core.support.BookmarkManagerReference;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.jta.JtaTransactionManager;
@@ -152,7 +153,7 @@ class Neo4jTransactionManagerTest {
throws NoSuchFieldException, IllegalAccessException {
Field bookmarkManager = Neo4jTransactionManager.class.getDeclaredField("bookmarkManager");
bookmarkManager.setAccessible(true);
bookmarkManager.set(txManager, value);
bookmarkManager.set(txManager, new BookmarkManagerReference(Neo4jBookmarkManager::create, value));
}
@Nested

View File

@@ -46,6 +46,7 @@ import org.neo4j.driver.reactivestreams.ReactiveSession;
import org.neo4j.driver.reactivestreams.ReactiveTransaction;
import org.springframework.data.neo4j.core.DatabaseSelection;
import org.springframework.data.neo4j.core.UserSelection;
import org.springframework.data.neo4j.core.support.BookmarkManagerReference;
import org.springframework.data.r2dbc.connectionfactory.R2dbcTransactionManager;
import org.springframework.transaction.reactive.TransactionSynchronizationManager;
import org.springframework.transaction.reactive.TransactionalOperator;
@@ -169,7 +170,7 @@ class ReactiveNeo4jTransactionManagerTest {
throws NoSuchFieldException, IllegalAccessException {
Field bookmarkManager = ReactiveNeo4jTransactionManager.class.getDeclaredField("bookmarkManager");
bookmarkManager.setAccessible(true);
bookmarkManager.set(txManager, value);
bookmarkManager.set(txManager, new BookmarkManagerReference(Neo4jBookmarkManager::createReactive, value));
}
}

View File

@@ -80,7 +80,6 @@ class CustomQueriesIT {
static void setupData(@Autowired Driver driver, @Autowired BookmarkCapture bookmarkCapture) throws IOException {
try (Session session = driver.session(bookmarkCapture.createSessionConfig())) {
session.run("MATCH (n) DETACH DELETE n").consume();
session.run("MATCH (n) DETACH DELETE n").consume();
CypherUtils.loadCypherFromResource("/data/movies.cypher", session);
bookmarkCapture.seedWith(session.lastBookmarks());

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2011-2023 the original author or authors.
*
* 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.springframework.data.neo4j.integration.bookmarks;
import java.io.IOException;
import java.io.UncheckedIOException;
import org.neo4j.driver.Driver;
import org.neo4j.driver.Session;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.data.neo4j.integration.movies.shared.CypherUtils;
/**
* @author Michael J. Simons
*/
public final class DatabaseInitializer implements InitializingBean {
private final Driver driver;
public DatabaseInitializer(Driver driver) {
this.driver = driver;
}
@Override
public void afterPropertiesSet() {
try (Session session = driver.session()) {
session.run("MATCH (n) DETACH DELETE n").consume();
CypherUtils.loadCypherFromResource("/data/movies.cypher", session);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
}

View File

@@ -0,0 +1,33 @@
/*
* Copyright 2011-2023 the original author or authors.
*
* 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.springframework.data.neo4j.integration.bookmarks;
import org.springframework.data.neo4j.core.schema.GeneratedValue;
import org.springframework.data.neo4j.core.schema.Id;
import org.springframework.data.neo4j.core.schema.Node;
/**
* @author Michael J. Simons
*/
@Node
public class Person {
@Id
@GeneratedValue
private String id;
private String name;
}

View File

@@ -0,0 +1,138 @@
/*
* Copyright 2011-2023 the original author or authors.
*
* 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.springframework.data.neo4j.integration.bookmarks.imperative;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
import org.neo4j.driver.Driver;
import org.neo4j.driver.SessionConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager;
import org.springframework.data.neo4j.integration.bookmarks.DatabaseInitializer;
import org.springframework.data.neo4j.integration.bookmarks.Person;
import org.springframework.data.neo4j.repository.Neo4jRepository;
import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories;
import org.springframework.data.neo4j.repository.query.Query;
import org.springframework.data.neo4j.test.Neo4jExtension;
import org.springframework.data.neo4j.test.Neo4jImperativeTestConfiguration;
import org.springframework.data.neo4j.test.Neo4jIntegrationTest;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.EnableTransactionManagement;
/**
* @author Michael J. Simons
*/
@Neo4jIntegrationTest
public class NoopBookmarkmanagerIT {
protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport;
@Configuration
@EnableNeo4jRepositories(considerNestedRepositories = true)
@EnableTransactionManagement
@ComponentScan
@EnableAsync
static class Config extends Neo4jImperativeTestConfiguration {
@Bean
DatabaseInitializer databaseInitializer(Driver driver) {
return new DatabaseInitializer(driver);
}
@Bean
public Driver driver() {
var driver = neo4jConnectionSupport.getDriver();
return Mockito.spy(driver);
}
@Override
public Neo4jBookmarkManager bookmarkManager() {
return Neo4jBookmarkManager.noop();
}
@Override
public boolean isCypher5Compatible() {
return neo4jConnectionSupport.isCypher5SyntaxCompatible();
}
}
@Test
void mustNotUseBookmarks(@Autowired PersonService personService, @Autowired Driver driver) throws ExecutionException, InterruptedException {
var movies = personService.getMoviesByActorNameLike("Bill");
assertThat(movies).hasSize(5);
var sessionConfigCaptor = ArgumentCaptor.forClass(SessionConfig.class);
verify(driver, times(5)).session(any(), sessionConfigCaptor.capture());
assertThat(sessionConfigCaptor.getAllValues())
.allMatch(cfg -> {
var bookmarks = new ArrayList<>();
if (cfg.bookmarks() != null) {
cfg.bookmarks().forEach(bookmarks::add);
}
return bookmarks.isEmpty();
});
}
interface PersonRepository extends Neo4jRepository<Person, String> {
@Async
@Query("MATCH (p:Person) WHERE p.name =~ (('.*' + $name) + '.*') RETURN p.name")
CompletableFuture<List<String>> findMatchingNames(String name);
@Query("MATCH (m:Movie)<-[:ACTED_IN]-(p:Person) WHERE p.name= $name return m.title")
CompletableFuture<List<String>> getPersonMovies(String name);
}
@Service
static class PersonService {
private final PersonRepository personRepository;
PersonService(PersonRepository personRepository) {
this.personRepository = personRepository;
}
public List<String> getMoviesByActorNameLike(String namePattern) throws ExecutionException, InterruptedException {
CompletableFuture<List<String>> completableFutureCompletableFuture = personRepository.findMatchingNames(namePattern)
.thenCompose(names -> {
List<String> result = Collections.synchronizedList(new ArrayList<String>());
var futures = names.stream().map(personRepository::getPersonMovies)
.map(cf -> cf.thenAccept(result::addAll))
.toArray(CompletableFuture[]::new);
return CompletableFuture.allOf(futures)
.thenApply(__ -> result);
});
return completableFutureCompletableFuture.get();
}
}
}

View File

@@ -0,0 +1,137 @@
/*
* Copyright 2011-2023 the original author or authors.
*
* 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.springframework.data.neo4j.integration.bookmarks.reactive;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
import org.neo4j.driver.Driver;
import org.neo4j.driver.SessionConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager;
import org.springframework.data.neo4j.integration.bookmarks.DatabaseInitializer;
import org.springframework.data.neo4j.integration.bookmarks.Person;
import org.springframework.data.neo4j.repository.ReactiveNeo4jRepository;
import org.springframework.data.neo4j.repository.config.EnableReactiveNeo4jRepositories;
import org.springframework.data.neo4j.repository.query.Query;
import org.springframework.data.neo4j.test.Neo4jExtension;
import org.springframework.data.neo4j.test.Neo4jIntegrationTest;
import org.springframework.data.neo4j.test.Neo4jReactiveTestConfiguration;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
/**
* @author Michael J. Simons
*/
@Neo4jIntegrationTest
@Tag(Neo4jExtension.NEEDS_REACTIVE_SUPPORT)
public class ReactiveNoopBookmarkmanagerIT {
protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport;
@Configuration
@EnableReactiveNeo4jRepositories(considerNestedRepositories = true)
@EnableTransactionManagement
@ComponentScan
static class Config extends Neo4jReactiveTestConfiguration {
@Bean
DatabaseInitializer databaseInitializer(Driver driver) {
return new DatabaseInitializer(driver);
}
@Bean
public Driver driver() {
var driver = neo4jConnectionSupport.getDriver();
return Mockito.spy(driver);
}
@Override
public Neo4jBookmarkManager bookmarkManager() {
return Neo4jBookmarkManager.noop();
}
@Override
public boolean isCypher5Compatible() {
return neo4jConnectionSupport.isCypher5SyntaxCompatible();
}
}
@Test
void mustNotUseBookmarks(@Autowired PersonService personService, @Autowired Driver driver) {
AtomicReference<List<String>> result = new AtomicReference<>();
personService.getMoviesByActorNameLike("Bill")
.as(StepVerifier::create)
.consumeNextWith(result::set)
.verifyComplete();
assertThat(result)
.hasValueSatisfying(movies -> assertThat(movies).hasSize(5));
var sessionConfigCaptor = ArgumentCaptor.forClass(SessionConfig.class);
verify(driver, times(5)).session(any(), sessionConfigCaptor.capture());
assertThat(sessionConfigCaptor.getAllValues())
.allMatch(cfg -> {
var bookmarks = new ArrayList<>();
if (cfg.bookmarks() != null) {
cfg.bookmarks().forEach(bookmarks::add);
}
return bookmarks.isEmpty();
});
}
interface PersonRepository extends ReactiveNeo4jRepository<Person, String> {
@Query("MATCH (p:Person) WHERE p.name =~ (('.*' + $name) + '.*') RETURN p.name")
Flux<String> findMatchingNames(String name);
@Query("MATCH (m:Movie)<-[:ACTED_IN]-(p:Person) WHERE p.name= $name return m.title")
Flux<String> getPersonMovies(String name);
}
@Service
static class PersonService {
private final PersonRepository personRepository;
PersonService(PersonRepository personRepository) {
this.personRepository = personRepository;
}
public Mono<List<String>> getMoviesByActorNameLike(String namePattern) {
return personRepository.findMatchingNames(namePattern)
.flatMap(personRepository::getPersonMovies, 2)
.collectList();
}
}
}