Skip to content

Commit 7dc6d40

Browse files
GH-2254 - Document BeforeBindCallbacks.
1 parent d2de50a commit 7dc6d40

File tree

4 files changed

+201
-142
lines changed

4 files changed

+201
-142
lines changed

src/main/asciidoc/faq/faq.adoc

+153-121
Original file line numberDiff line numberDiff line change
@@ -480,9 +480,129 @@ public interface MyPersonRepository extends Neo4jRepository<Person, Long> {
480480
Also, the `Pageable` should be unsorted and you should provide a stable order.
481481
We won't use the sorting information from the pageable.
482482
<.> This method returns a page. A page knows about the exact number of total pages.
483-
Therefore you must specify an additional count query.
483+
Therefore, you must specify an additional count query.
484484
All other restrictions from the second method apply.
485485

486+
[[faq.path-mapping]]
487+
== Can I map named paths?
488+
489+
A series of connected nodes and relationships is called a "path" in Neo4j.
490+
Cypher allows paths to be named using an identifer, as exemplified by:
491+
492+
[source,cypher]
493+
----
494+
p = (a)-[*3..5]->(b)
495+
----
496+
497+
or as in the infamous Movie graph, that includes the following path (in that case, one of the shortest path between two actors):
498+
499+
[[bacon-distance]]
500+
[source,cypher]
501+
.The "Bacon" distance
502+
----
503+
MATCH p=shortestPath((bacon:Person {name:"Kevin Bacon"})-[*]-(meg:Person {name:"Meg Ryan"}))
504+
RETURN p
505+
----
506+
507+
Which looks like this:
508+
509+
image::bacon-distance.png[]
510+
511+
We find 3 nodes labeled `Person` and 2 nodes labeled `Movie`. Both can be mapped with a custom queury.
512+
Assume there's a node entity for both `Person` and `Movie` as well as `Actor` taking care of the relationship:
513+
514+
515+
[source,java]
516+
."Standard" movie graph domain model
517+
----
518+
@Node
519+
public final class Person {
520+
521+
@Id @GeneratedValue
522+
private final Long id;
523+
524+
private final String name;
525+
526+
private Integer born;
527+
528+
@Relationship("REVIEWED")
529+
private List<Movie> reviewed = new ArrayList<>();
530+
}
531+
532+
@RelationshipProperties
533+
public final class Actor {
534+
535+
@Id @GeneratedValue
536+
private final Long id;
537+
538+
@TargetNode
539+
private final Person person;
540+
541+
private final List<String> roles;
542+
}
543+
544+
@Node
545+
public final class Movie {
546+
547+
@Id
548+
private final String title;
549+
550+
@Property("tagline")
551+
private final String description;
552+
553+
@Relationship(value = "ACTED_IN", direction = Direction.INCOMING)
554+
private final List<Actor> actors;
555+
}
556+
----
557+
558+
When using a query as shown in <<bacon-distance>> for a domain class of type `Person` like this
559+
560+
[source,java]
561+
----
562+
interface PeopleRepository extends Neo4jRepository<Person, Long> {
563+
@Query(""
564+
+ "MATCH p=shortestPath((bacon:Person {name: $person1})-[*]-(meg:Person {name: $person2}))\n"
565+
+ "RETURN p"
566+
)
567+
List<Person> findAllOnShortestPathBetween(@Param("person1") String person1, @Param("person2") String person2);
568+
}
569+
----
570+
571+
it will retrieve all people from the path and map them.
572+
If there are relationship types on the path like `REVIEWED` that are also present on the domain, these
573+
will be filled accordingly from the path.
574+
575+
WARNING: Take special care when you use nodes hydrated from a path based query to save data.
576+
If not all relationships are hydrated, data will be lost.
577+
578+
The other way round works as well. The same query can be used with the `Movie` entity.
579+
It then will only populate movies.
580+
The following listing shows how todo this as well as how the query can be enriched with additional data
581+
not found on the path. That data is used to correctly populate the missing relationships (in that case, all the actors)
582+
583+
[source,java]
584+
----
585+
interface MovieRepository extends Neo4jRepository<Movie, String> {
586+
587+
@Query(""
588+
+ "MATCH p=shortestPath(\n"
589+
+ "(bacon:Person {name: $person1})-[*]-(meg:Person {name: $person2}))\n"
590+
+ "WITH p, [n IN nodes(p) WHERE n:Movie] AS x\n"
591+
+ "UNWIND x AS m\n"
592+
+ "MATCH (m) <-[r:DIRECTED]-(d:Person)\n"
593+
+ "RETURN p, collect(r), collect(d)"
594+
)
595+
List<Movie> findAllOnShortestPathBetween(@Param("person1") String person1, @Param("person2") String person2);
596+
}
597+
----
598+
599+
The query returns the path plus all relationships and related nodes collected so that the movie entities are fully hydrated.
600+
601+
The path mapping works for single paths as well for multiple records of paths (which are returned by the `allShortestPath` function.)
602+
603+
TIP: Named paths can be used efficiently to populate and return more than just a root node, see <<custom-query.paths>>.
604+
605+
486606
[[faq.custom-queries-and-custom-mappings]]
487607
== Is `@Query` the only way to use custom queries?
488608

@@ -632,7 +752,7 @@ and one implementation. The implementation would than have had all three abstrac
632752
All of this applies of course to reactive repositories as well.
633753
They would work with the `ReactiveNeo4jTemplate` and `ReactiveNeo4jClient` and the reactive session provided by the driver.
634754

635-
If you have recuring methods for all repositories, you could swap out the default repository implementation.
755+
If you have recurring methods for all repositories, you could swap out the default repository implementation.
636756

637757
[[faq.custom-base-repositories]]
638758
== How do I use custom Spring Data Neo4j base repositories?
@@ -666,6 +786,37 @@ Those are
666786
* `org.springframework.data.annotation.LastModifiedBy`
667787
* `org.springframework.data.annotation.LastModifiedDate`
668788

789+
<<auditing>> gives you a general view how to use auditing in the bigger context of Spring Data Commons.
790+
The following listing presents every configuration option provided by Spring Data Neo4j:
791+
792+
[source,java,indent=0,tabsize=4]
793+
.Enabling and configuring Neo4j auditing
794+
----
795+
include::../../../../src/test/java/org/springframework/data/neo4j/integration/imperative/AuditingIT.java[tags=faq.entities.auditing]
796+
----
797+
<.> Set to true if you want the modification data to be written during creating as well
798+
<.> Use this attribute to specify the name of the bean that provides the auditor (i.e. a user name)
799+
<.> Use this attribute to specify the name of a bean that provides the current date. In this case
800+
a fixed date is used as the above configuration is part of our tests
801+
802+
The reactive version is basically the same apart from the fact the auditor aware bean is of type `ReactiveAuditorAware`,
803+
so that the retrieval of an auditor is part of the reactive flow.
804+
805+
In addition to those auditing mechanism you can add as many beans implementing `BeforeBindCallback<T>` or `ReactiveBeforeBindCallback<T>`
806+
to the context. These beans will be picked up by Spring Data Neo4j and called in order (in case they implement `Ordered` or
807+
are annotated with `@Order`) just before an entity is persisted.
808+
809+
They can modify the entity or return a completely new one.
810+
The following example adds one callback to the context that changes one attribute before the entity is persisted:
811+
812+
[source,java,indent=0,tabsize=4]
813+
.Modifying entities before save
814+
----
815+
include::../../../../src/test/java/org/springframework/data/neo4j/integration/imperative/CallbacksIT.java[tags=faq.entities.auditing.callbacks]
816+
----
817+
818+
No additional configuration is required.
819+
669820
[[faq.find-by-example]]
670821
== How do I use "Find by example"?
671822

@@ -693,125 +844,6 @@ movieExample = Example.of(
693844
movies = this.movieRepository.findAll(movieExample);
694845
----
695846

696-
[[faq.path-mapping]]
697-
== Can I map named paths?
698-
699-
A series of connected nodes and relationships is called a "path" in Neo4j.
700-
Cypher allows paths to be named using an identifer, as exemplified by:
701-
702-
[source,cypher]
703-
----
704-
p = (a)-[*3..5]->(b)
705-
----
706-
707-
or as in the infamous Movie graph, that includes the following path (in that case, one of the shortest path between two actors):
708-
709-
[[bacon-distance]]
710-
[source,cypher]
711-
.The "Bacon" distance
712-
----
713-
MATCH p=shortestPath((bacon:Person {name:"Kevin Bacon"})-[*]-(meg:Person {name:"Meg Ryan"}))
714-
RETURN p
715-
----
716-
717-
Which looks like this:
718-
719-
image::bacon-distance.png[]
720-
721-
We find 3 nodes labeled `Person` and 2 nodes labeled `Movie`. Both can be mapped with a custom queury.
722-
Assume there's a node entity for both `Person` and `Movie` as well as `Actor` taking care of the relationship:
723-
724-
725-
[source,java]
726-
."Standard" movie graph domain model
727-
----
728-
@Node
729-
public final class Person {
730-
731-
@Id @GeneratedValue
732-
private final Long id;
733-
734-
private final String name;
735-
736-
private Integer born;
737-
738-
@Relationship("REVIEWED")
739-
private List<Movie> reviewed = new ArrayList<>();
740-
}
741-
742-
@RelationshipProperties
743-
public final class Actor {
744-
745-
@Id @GeneratedValue
746-
private final Long id;
747-
748-
@TargetNode
749-
private final Person person;
750-
751-
private final List<String> roles;
752-
}
753-
754-
@Node
755-
public final class Movie {
756-
757-
@Id
758-
private final String title;
759-
760-
@Property("tagline")
761-
private final String description;
762-
763-
@Relationship(value = "ACTED_IN", direction = Direction.INCOMING)
764-
private final List<Actor> actors;
765-
}
766-
----
767-
768-
When using a query as shown in <<bacon-distance>> for a domain class of type `Person` like this
769-
770-
[source,java]
771-
----
772-
interface PeopleRepository extends Neo4jRepository<Person, Long> {
773-
@Query(""
774-
+ "MATCH p=shortestPath((bacon:Person {name: $person1})-[*]-(meg:Person {name: $person2}))\n"
775-
+ "RETURN p"
776-
)
777-
List<Person> findAllOnShortestPathBetween(@Param("person1") String person1, @Param("person2") String person2);
778-
}
779-
----
780-
781-
it will retrieve all people from the path and map them.
782-
If there are relationship types on the path like `REVIEWED` that are also present on the domain, these
783-
will be filled accordingly from the path.
784-
785-
WARNING: Take special care when you use nodes hydrated from a path based query to save data.
786-
If not all relationships are hydrated, data will be lost.
787-
788-
The other way round works as well. The same query can be used with the `Movie` entity.
789-
It then will only populate movies.
790-
The following listing shows how todo this as well as how the query can be enriched with additional data
791-
not found on the path. That data is used to correctly populate the missing relationships (in that case, all the actors)
792-
793-
[source,java]
794-
----
795-
interface MovieRepository extends Neo4jRepository<Movie, String> {
796-
797-
@Query(""
798-
+ "MATCH p=shortestPath(\n"
799-
+ "(bacon:Person {name: $person1})-[*]-(meg:Person {name: $person2}))\n"
800-
+ "WITH p, [n IN nodes(p) WHERE n:Movie] AS x\n"
801-
+ "UNWIND x AS m\n"
802-
+ "MATCH (m) <-[r:DIRECTED]-(d:Person)\n"
803-
+ "RETURN p, collect(r), collect(d)"
804-
)
805-
List<Movie> findAllOnShortestPathBetween(@Param("person1") String person1, @Param("person2") String person2);
806-
}
807-
----
808-
809-
The query returns the path plus all relationships and related nodes collected so that the movie entities are fully hydrated.
810-
811-
The path mapping works for single paths as well for multiple records of paths (which are returned by the `allShortestPath` function.)
812-
813-
TIP: Named paths can be used efficiently to populate and return more than just a root node, see <<custom-query.paths>>.
814-
815847
[[faq.spring-boot.sdn]]
816848
== Do I need Spring Boot to use Spring Data Neo4j?
817849

src/test/java/org/springframework/data/neo4j/integration/imperative/AuditingIT.java

+25-11
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,14 @@
2424
import org.junit.jupiter.api.Test;
2525
import org.neo4j.driver.Driver;
2626
import org.springframework.beans.factory.annotation.Autowired;
27+
// tag::faq.entities.auditing[]
2728
import org.springframework.context.annotation.Bean;
2829
import org.springframework.context.annotation.Configuration;
30+
import org.springframework.context.annotation.Import;
2931
import org.springframework.data.auditing.DateTimeProvider;
3032
import org.springframework.data.domain.AuditorAware;
33+
34+
// end::faq.entities.auditing[]
3135
import org.springframework.data.neo4j.config.AbstractNeo4jConfig;
3236
import org.springframework.data.neo4j.config.EnableNeo4jAuditing;
3337
import org.springframework.data.neo4j.integration.shared.common.AuditingITBase;
@@ -122,30 +126,40 @@ interface ImmutableEntityWithGeneratedIdRepository
122126
extends Neo4jRepository<ImmutableAuditableThingWithGeneratedId, String> {}
123127

124128
@Configuration
129+
@Import(AuditingConfig.class)
125130
@EnableTransactionManagement
126131
@EnableNeo4jRepositories(considerNestedRepositories = true)
127-
@EnableNeo4jAuditing(modifyOnCreate = false, auditorAwareRef = "auditorProvider",
128-
dateTimeProviderRef = "fixedDateTimeProvider")
129132
static class Config extends AbstractNeo4jConfig {
130133

131134
@Bean
132135
public Driver driver() {
133136
return neo4jConnectionSupport.getDriver();
134137
}
135138

136-
@Bean
137-
public AuditorAware<String> auditorProvider() {
138-
return () -> Optional.of("A user");
139-
}
140-
141139
@Override
142140
protected Collection<String> getMappingBasePackages() {
143141
return Collections.singleton(ImmutableAuditableThing.class.getPackage().getName());
144142
}
143+
}
144+
}
145145

146-
@Bean
147-
public DateTimeProvider fixedDateTimeProvider() {
148-
return () -> Optional.of(DEFAULT_CREATION_AND_MODIFICATION_DATE);
149-
}
146+
// tag::faq.entities.auditing[]
147+
@Configuration
148+
@EnableNeo4jAuditing(
149+
modifyOnCreate = false, // <.>
150+
auditorAwareRef = "auditorProvider", // <.>
151+
dateTimeProviderRef = "fixedDateTimeProvider" // <.>
152+
)
153+
class AuditingConfig {
154+
155+
@Bean
156+
public AuditorAware<String> auditorProvider() {
157+
return () -> Optional.of("A user");
158+
}
159+
160+
@Bean
161+
public DateTimeProvider fixedDateTimeProvider() {
162+
return () -> Optional.of(AuditingITBase.DEFAULT_CREATION_AND_MODIFICATION_DATE);
150163
}
151164
}
165+
// end::faq.entities.auditing[]

0 commit comments

Comments
 (0)