From c257279fed9375eaa5ff3fae642d572198fa7d07 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Wed, 22 Jun 2022 15:21:57 +0200 Subject: [PATCH] GH-2557 - Improve documentation for read-only transactions. Closes #2557. --- src/main/asciidoc/faq/faq.adoc | 158 +++++++++++++++++- .../introduction-and-preface/preface.adoc | 23 +++ 2 files changed, 178 insertions(+), 3 deletions(-) diff --git a/src/main/asciidoc/faq/faq.adoc b/src/main/asciidoc/faq/faq.adoc index c6dcdebb2..61e13c856 100644 --- a/src/main/asciidoc/faq/faq.adoc +++ b/src/main/asciidoc/faq/faq.adoc @@ -451,15 +451,167 @@ public class CustomConfig { } ---- +[[faq.cluster]] +== Using a Neo4j cluster instance from Spring Data Neo4j + +The following questions apply to Neo4j AuraDB as well as to Neo4j on-premise cluster instances. + [[faq.transactions.cluster]] -== Do I need specific configuration so that transactions work seamless with a Neo4j Causal Cluster? +=== Do I need specific configuration so that transactions work seamless with a Neo4j Causal Cluster? No, you don't. SDN uses Neo4j Causal Cluster bookmarks internally without any configuration on your side required. Transactions in the same thread or the same reactive stream following each other will be able to read their previously changed values as you would expect. +[[faq.transactions.cluster.rw]] +=== Is it important to use read-only transactions for Neo4j cluster? + +Yes, it is. +The Neo4j cluster architecture is a causal clustering architecture, and it distinguishes between primary and secondary servers. +Primary server either are single instances or core instances. Both of them can answer to read and write operations. +Write operations are propagated from the core instances to read replicas inside the cluster. +Those read replicas are secondary servers. +Secondary servers don't answer to write operations. + +In a standard deployment scenario you'll have some core instances and many read replicas inside a cluster. +Therefor it is important to mark operations or queries as read-only to scale your cluster in such a way that leaders are +never overwhelmed and queries are propagated as much as possible to read replicas. + +Neither Spring Data Neo4j nor the underlying Java driver do Cypher parsing and both building blocks assume +write operations by default. This decision has been made to support all operations out of the box. If something in the +stack would assume read-only by default, the stack might end up sending write queries to read replicas and fail +on executing them. + +NOTE: All `findById`, `findAllById`, `findAll` and predefined existential methods are marked as read-only by default. + +Some options are described below: + +.Making a whole repository read-only +[source,java] +---- +import org.springframework.data.neo4j.repository.Neo4jRepository; +import org.springframework.transaction.annotation.Transactional; + +@Transactional(readOnly = true) +interface PersonRepository extends Neo4jRepository { +} +---- + +.Making selected repository methods read-only +[source,java] +---- +import org.springframework.data.neo4j.repository.Neo4jRepository; +import org.springframework.data.neo4j.repository.query.Query; +import org.springframework.transaction.annotation.Transactional; + +interface PersonRepository extends Neo4jRepository { + + @Transactional(readOnly = true) + Person findOneByName(String name); // <.> + + @Transactional(readOnly = true) + @Query(""" + CALL apoc.search.nodeAll('{Person: "name",Movie: ["title","tagline"]}','contains','her') + YIELD node AS n RETURN n""") + Person findByCustomQuery(); // <.> +} +---- +<.> Why isn't this read-only be default? While it would work for the derived finder above (which we actually know to be read-only), + we often have seen cases in which user add a custom `@Query` and implement it via a `MERGE` construct, + which of course is a write operation. +<.> Custom procedures can do all kinds of things, there's no way at the moment to check for read-only vs write here for us. + +.Orchestrate calls to a repository from a service +[source,java] +---- +import java.util.Optional; + +import org.springframework.data.neo4j.repository.Neo4jRepository; +import org.springframework.transaction.annotation.Transactional; + +interface PersonRepository extends Neo4jRepository { +} + +interface MovieRepository extends Neo4jRepository { + List findByLikedByPersonName(String name); +} + +public class PersonService { + + private final PersonRepository personRepository; + private final MovieRepository movieRepository; + + public PersonService(PersonRepository personRepository, + MovieRepository movieRepository) { + this.personRepository = personRepository; + this.movieRepository = movieRepository; + } + + @Transactional(readOnly = true) + public Optional getPerson(Long id) { // <.> + return this.repository.findById(id) + .map(person -> { + var movies = this.movieRepository + .findByLikedByPersonName(person.getName()); + return new PersonDetails(person, movies); + }); + } +} +---- +<.> Here, several calls to multiple repositories are wrapped in one single, read-only transaction. + + +.Using Springs `TransactionTemplate` inside private service methods and / or with the Neo4j client +[source,java] +---- +import java.util.Collection; + +import org.neo4j.driver.types.Node; +import org.springframework.data.neo4j.core.Neo4jClient; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.support.TransactionTemplate; + +public class PersonService { + + private final TransactionTemplate readOnlyTx; + + private final Neo4jClient neo4jClient; + + public PersonService(PlatformTransactionManager transactionManager, Neo4jClient neo4jClient) { + + this.readOnlyTx = new TransactionTemplate(transactionManager, // <.> + new TransactionDefinition() { + @Override public boolean isReadOnly() { + return true; + } + } + ); + this.neo4jClient = neo4jClient; + } + + void internalOperation() { // <.> + + Collection nodes = this.readOnlyTx.execute(state -> { + return neo4jClient.query("MATCH (n) RETURN n").fetchAs(Node.class) // <.> + .mappedBy((types, record) -> record.get(0).asNode()) + .all(); + }); + } +} +---- +<.> Create an instance of the `TransactionTemplate` with the characteristics you need. + Of course, this can be a global bean, too. +<.> Reason number one for using the transaction template: Declarative transactions don't work + in package private or private methods and also not in inner method calls (imagine another method + in this service calling `internalOperation`) due to their nature being implemented with Aspects + and proxies. +<.> The `Neo4jClient` is a fixed utility provided by SDN. It cannot be annotated, but it integrates with Spring. + So it gives you everything you would do with the pure driver and without automatic mapping and with + transactions. It also obeys declarative transactions. + [[faq.bookmarks.seeding]] -== Can I retrieve the latest Bookmarks or seed the transaction manager? +=== Can I retrieve the latest Bookmarks or seed the transaction manager? As mentioned briefly in <>, there is no need to configure anything with regard to bookmarks. It may however be useful to retrieve the latest bookmark the SDN transaction system received from a database. @@ -531,7 +683,7 @@ WARNING: There is *no* need to do any of these things above, unless your applica this data. If in doubt, don't do either. [[faq.bookmarks.noop]] -== Can I disable bookmark management? +=== Can I disable bookmark management? We provide a Noop bookmark manager that effectively disables bookmark management. diff --git a/src/main/asciidoc/introduction-and-preface/preface.adoc b/src/main/asciidoc/introduction-and-preface/preface.adoc index 044650da0..35c4ae56a 100644 --- a/src/main/asciidoc/introduction-and-preface/preface.adoc +++ b/src/main/asciidoc/introduction-and-preface/preface.adoc @@ -48,6 +48,19 @@ On a lower level, you can grab the Bolt driver instance, but than you have to ma To learn more about Spring, you can refer to the comprehensive documentation that explains in detail the Spring Framework. There are a lot of articles, blog entries and books on the matter - take a look at the Spring Framework https://spring.io/docs[home page ] for more information. +This documentation tries to bridge between a broad spectrum of possible users: + +* People new to all the Spring ecosystem, including Spring Framework, Spring Data, the concrete module (in this case Spring Data Neo4j) + and Neo4j. +* Experienced Neo4j developers that are new to Spring Data and want to make best use of their Neo4j knowledge but are unfamiliar + with declarative transactions for example and how to incorporate the latter with Neo4j cluster requirements. +* Experienced Spring Data developers who are new to this specific module and Neo4j and need to learn how the building blocks + interact together. While the programming paradigm of this module is very much in line with Spring Data JDBC, Mongo and others, + the query language (Cypher), transactional and clustering behaviour is different and can't be abstracted away. + +We decided to move a lot of Neo4j specific questions into the <>. + + [[what-is-sdn]] == What is Spring Data Neo4j @@ -60,6 +73,16 @@ JVM primitives are mapped to node or relationship properties. An OGM abstracts the database and provides a convenient way to persist your domain model in the graph and query it without having to use low level drivers directly. It also provides the flexibility to the developer to supply custom queries where the queries generated by SDN are insufficient. +TIP: Please make sure you read the <> where we address many reoccurring questions about our + mapping decisions but also how interaction with Neo4j cluster instances such as https://neo4j.com/cloud/platform/aura-graph-database/[Neo4j AuraDB] + and on-premise cluster deployments can be significantly improved. + + + Concepts that are important to understand are Neo4j Bookmarks, https://medium.com/neo4j/try-and-then-retry-there-can-be-failure-30bf336383da[the potential need] + for incorporating a proper retry mechanism such as https://github.com/spring-projects/spring-retry[Spring Retry] or + https://github.com/resilience4j/resilience4j[Resilience4j] (we recommend the latter, as this knowledge is applicable outside + Spring, too) and the importance of read-only vs write queries in the context of Neo4j cluster. + + [[what-is-in-the-box-sdn]] === What's in the box?