Skip to content

Commit 70678af

Browse files
GH-2557 - Improve documentation for read-only transactions.
Closes #2557.
1 parent c95a3e9 commit 70678af

File tree

2 files changed

+178
-3
lines changed

2 files changed

+178
-3
lines changed

src/main/asciidoc/faq/faq.adoc

+155-3
Original file line numberDiff line numberDiff line change
@@ -451,15 +451,167 @@ public class CustomConfig {
451451
}
452452
----
453453

454+
[[faq.cluster]]
455+
== Using a Neo4j cluster instance from Spring Data Neo4j
456+
457+
The following questions apply to Neo4j AuraDB as well as to Neo4j on-premise cluster instances.
458+
454459
[[faq.transactions.cluster]]
455-
== Do I need specific configuration so that transactions work seamless with a Neo4j Causal Cluster?
460+
=== Do I need specific configuration so that transactions work seamless with a Neo4j Causal Cluster?
456461

457462
No, you don't.
458463
SDN uses Neo4j Causal Cluster bookmarks internally without any configuration on your side required.
459464
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.
460465

466+
[[faq.transactions.cluster.rw]]
467+
=== Is it important to use read-only transactions for Neo4j cluster?
468+
469+
Yes, it is.
470+
The Neo4j cluster architecture is a causal clustering architecture, and it distinguishes between primary and secondary servers.
471+
Primary server either are single instances or core instances. Both of them can answer to read and write operations.
472+
Write operations are propagated from the core instances to read replicas inside the cluster.
473+
Those read replicas are secondary servers.
474+
Secondary servers don't answer to write operations.
475+
476+
In a standard deployment scenario you'll have some core instances and many read replicas inside a cluster.
477+
Therefor it is important to mark operations or queries as read-only to scale your cluster in such a way that leaders are
478+
never overwhelmed and queries are propagated as much as possible to read replicas.
479+
480+
Neither Spring Data Neo4j nor the underlying Java driver do Cypher parsing and both building blocks assume
481+
write operations by default. This decision has been made to support all operations out of the box. If something in the
482+
stack would assume read-only by default, the stack might end up sending write queries to read replicas and fail
483+
on executing them.
484+
485+
NOTE: All `findById`, `findAllById`, `findAll` and predefined existential methods are marked as read-only by default.
486+
487+
Some options are described below:
488+
489+
.Making a whole repository read-only
490+
[source,java]
491+
----
492+
import org.springframework.data.neo4j.repository.Neo4jRepository;
493+
import org.springframework.transaction.annotation.Transactional;
494+
495+
@Transactional(readOnly = true)
496+
interface PersonRepository extends Neo4jRepository<Person, Long> {
497+
}
498+
----
499+
500+
.Making selected repository methods read-only
501+
[source,java]
502+
----
503+
import org.springframework.data.neo4j.repository.Neo4jRepository;
504+
import org.springframework.data.neo4j.repository.query.Query;
505+
import org.springframework.transaction.annotation.Transactional;
506+
507+
interface PersonRepository extends Neo4jRepository<Person, Long> {
508+
509+
@Transactional(readOnly = true)
510+
Person findOneByName(String name); // <.>
511+
512+
@Transactional(readOnly = true)
513+
@Query("""
514+
CALL apoc.search.nodeAll('{Person: "name",Movie: ["title","tagline"]}','contains','her')
515+
YIELD node AS n RETURN n""")
516+
Person findByCustomQuery(); // <.>
517+
}
518+
----
519+
<.> 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),
520+
we often have seen cases in which user add a custom `@Query` and implement it via a `MERGE` construct,
521+
which of course is a write operation.
522+
<.> 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.
523+
524+
.Orchestrate calls to a repository from a service
525+
[source,java]
526+
----
527+
import java.util.Optional;
528+
529+
import org.springframework.data.neo4j.repository.Neo4jRepository;
530+
import org.springframework.transaction.annotation.Transactional;
531+
532+
interface PersonRepository extends Neo4jRepository<Person, Long> {
533+
}
534+
535+
interface MovieRepository extends Neo4jRepository<Movie, Long> {
536+
List<Movie> findByLikedByPersonName(String name);
537+
}
538+
539+
public class PersonService {
540+
541+
private final PersonRepository personRepository;
542+
private final MovieRepository movieRepository;
543+
544+
public PersonService(PersonRepository personRepository,
545+
MovieRepository movieRepository) {
546+
this.personRepository = personRepository;
547+
this.movieRepository = movieRepository;
548+
}
549+
550+
@Transactional(readOnly = true)
551+
public Optional<PersonDetails> getPerson(Long id) { // <.>
552+
return this.repository.findById(id)
553+
.map(person -> {
554+
var movies = this.movieRepository
555+
.findByLikedByPersonName(person.getName());
556+
return new PersonDetails(person, movies);
557+
});
558+
}
559+
}
560+
----
561+
<.> Here, several calls to multiple repositories are wrapped in one single, read-only transaction.
562+
563+
564+
.Using Springs `TransactionTemplate` inside private service methods and / or with the Neo4j client
565+
[source,java]
566+
----
567+
import java.util.Collection;
568+
569+
import org.neo4j.driver.types.Node;
570+
import org.springframework.data.neo4j.core.Neo4jClient;
571+
import org.springframework.transaction.PlatformTransactionManager;
572+
import org.springframework.transaction.TransactionDefinition;
573+
import org.springframework.transaction.support.TransactionTemplate;
574+
575+
public class PersonService {
576+
577+
private final TransactionTemplate readOnlyTx;
578+
579+
private final Neo4jClient neo4jClient;
580+
581+
public PersonService(PlatformTransactionManager transactionManager, Neo4jClient neo4jClient) {
582+
583+
this.readOnlyTx = new TransactionTemplate(transactionManager, // <.>
584+
new TransactionDefinition() {
585+
@Override public boolean isReadOnly() {
586+
return true;
587+
}
588+
}
589+
);
590+
this.neo4jClient = neo4jClient;
591+
}
592+
593+
void internalOperation() { // <.>
594+
595+
Collection<Node> nodes = this.readOnlyTx.execute(state -> {
596+
return neo4jClient.query("MATCH (n) RETURN n").fetchAs(Node.class) // <.>
597+
.mappedBy((types, record) -> record.get(0).asNode())
598+
.all();
599+
});
600+
}
601+
}
602+
----
603+
<.> Create an instance of the `TransactionTemplate` with the characteristics you need.
604+
Of course, this can be a global bean, too.
605+
<.> Reason number one for using the transaction template: Declarative transactions don't work
606+
in package private or private methods and also not in inner method calls (imagine another method
607+
in this service calling `internalOperation`) due to their nature being implemented with Aspects
608+
and proxies.
609+
<.> The `Neo4jClient` is a fixed utility provided by SDN. It cannot be annotated, but it integrates with Spring.
610+
So it gives you everything you would do with the pure driver and without automatic mapping and with
611+
transactions. It also obeys declarative transactions.
612+
461613
[[faq.bookmarks.seeding]]
462-
== Can I retrieve the latest Bookmarks or seed the transaction manager?
614+
=== Can I retrieve the latest Bookmarks or seed the transaction manager?
463615

464616
As mentioned briefly in <<migrating.bookmarks>>, there is no need to configure anything with regard to bookmarks.
465617
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
531683
this data. If in doubt, don't do either.
532684

533685
[[faq.bookmarks.noop]]
534-
== Can I disable bookmark management?
686+
=== Can I disable bookmark management?
535687

536688
We provide a Noop bookmark manager that effectively disables bookmark management.
537689

src/main/asciidoc/introduction-and-preface/preface.adoc

+23
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,19 @@ On a lower level, you can grab the Bolt driver instance, but than you have to ma
4848
To learn more about Spring, you can refer to the comprehensive documentation that explains in detail the Spring Framework.
4949
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.
5050

51+
This documentation tries to bridge between a broad spectrum of possible users:
52+
53+
* People new to all the Spring ecosystem, including Spring Framework, Spring Data, the concrete module (in this case Spring Data Neo4j)
54+
and Neo4j.
55+
* Experienced Neo4j developers that are new to Spring Data and want to make best use of their Neo4j knowledge but are unfamiliar
56+
with declarative transactions for example and how to incorporate the latter with Neo4j cluster requirements.
57+
* Experienced Spring Data developers who are new to this specific module and Neo4j and need to learn how the building blocks
58+
interact together. While the programming paradigm of this module is very much in line with Spring Data JDBC, Mongo and others,
59+
the query language (Cypher), transactional and clustering behaviour is different and can't be abstracted away.
60+
61+
We decided to move a lot of Neo4j specific questions into the <<faq, Frequently Asked Questions>>.
62+
63+
5164
[[what-is-sdn]]
5265
== What is Spring Data Neo4j
5366

@@ -60,6 +73,16 @@ JVM primitives are mapped to node or relationship properties.
6073
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.
6174
It also provides the flexibility to the developer to supply custom queries where the queries generated by SDN are insufficient.
6275

76+
TIP: Please make sure you read the <<faq, Frequently Asked Questions>> where we address many reoccurring questions about our
77+
mapping decisions but also how interaction with Neo4j cluster instances such as https://neo4j.com/cloud/platform/aura-graph-database/[Neo4j AuraDB]
78+
and on-premise cluster deployments can be significantly improved.
79+
+
80+
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]
81+
for incorporating a proper retry mechanism such as https://github.com/spring-projects/spring-retry[Spring Retry] or
82+
https://github.com/resilience4j/resilience4j[Resilience4j] (we recommend the latter, as this knowledge is applicable outside
83+
Spring, too) and the importance of read-only vs write queries in the context of Neo4j cluster.
84+
85+
6386
[[what-is-in-the-box-sdn]]
6487
=== What's in the box?
6588

0 commit comments

Comments
 (0)