Skip to content

Commit 29ea1ee

Browse files
GH-2254 - Document BeforeBindCallbacks.
Closes #2254.
1 parent 7c496d3 commit 29ea1ee

File tree

4 files changed

+201
-141
lines changed

4 files changed

+201
-141
lines changed

src/main/asciidoc/faq/faq.adoc

+152-120
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,126 @@ public interface MyPersonRepository extends Neo4jRepository<Person, Long> {
553553
Therefore, you must specify an additional count query.
554554
All other restrictions from the second method apply.
555555

556+
[[faq.path-mapping]]
557+
== Can I map named paths?
558+
559+
A series of connected nodes and relationships is called a "path" in Neo4j.
560+
Cypher allows paths to be named using an identifer, as exemplified by:
561+
562+
[source,cypher]
563+
----
564+
p = (a)-[*3..5]->(b)
565+
----
566+
567+
or as in the infamous Movie graph, that includes the following path (in that case, one of the shortest path between two actors):
568+
569+
[[bacon-distance]]
570+
[source,cypher]
571+
.The "Bacon" distance
572+
----
573+
MATCH p=shortestPath((bacon:Person {name:"Kevin Bacon"})-[*]-(meg:Person {name:"Meg Ryan"}))
574+
RETURN p
575+
----
576+
577+
Which looks like this:
578+
579+
image::bacon-distance.png[]
580+
581+
We find 3 nodes labeled `Person` and 2 nodes labeled `Movie`. Both can be mapped with a custom queury.
582+
Assume there's a node entity for both `Person` and `Movie` as well as `Actor` taking care of the relationship:
583+
584+
585+
[source,java]
586+
."Standard" movie graph domain model
587+
----
588+
@Node
589+
public final class Person {
590+
591+
@Id @GeneratedValue
592+
private final Long id;
593+
594+
private final String name;
595+
596+
private Integer born;
597+
598+
@Relationship("REVIEWED")
599+
private List<Movie> reviewed = new ArrayList<>();
600+
}
601+
602+
@RelationshipProperties
603+
public final class Actor {
604+
605+
@Id @GeneratedValue
606+
private final Long id;
607+
608+
@TargetNode
609+
private final Person person;
610+
611+
private final List<String> roles;
612+
}
613+
614+
@Node
615+
public final class Movie {
616+
617+
@Id
618+
private final String title;
619+
620+
@Property("tagline")
621+
private final String description;
622+
623+
@Relationship(value = "ACTED_IN", direction = Direction.INCOMING)
624+
private final List<Actor> actors;
625+
}
626+
----
627+
628+
When using a query as shown in <<bacon-distance>> for a domain class of type `Person` like this
629+
630+
[source,java]
631+
----
632+
interface PeopleRepository extends Neo4jRepository<Person, Long> {
633+
@Query(""
634+
+ "MATCH p=shortestPath((bacon:Person {name: $person1})-[*]-(meg:Person {name: $person2}))\n"
635+
+ "RETURN p"
636+
)
637+
List<Person> findAllOnShortestPathBetween(@Param("person1") String person1, @Param("person2") String person2);
638+
}
639+
----
640+
641+
it will retrieve all people from the path and map them.
642+
If there are relationship types on the path like `REVIEWED` that are also present on the domain, these
643+
will be filled accordingly from the path.
644+
645+
WARNING: Take special care when you use nodes hydrated from a path based query to save data.
646+
If not all relationships are hydrated, data will be lost.
647+
648+
The other way round works as well. The same query can be used with the `Movie` entity.
649+
It then will only populate movies.
650+
The following listing shows how todo this as well as how the query can be enriched with additional data
651+
not found on the path. That data is used to correctly populate the missing relationships (in that case, all the actors)
652+
653+
[source,java]
654+
----
655+
interface MovieRepository extends Neo4jRepository<Movie, String> {
656+
657+
@Query(""
658+
+ "MATCH p=shortestPath(\n"
659+
+ "(bacon:Person {name: $person1})-[*]-(meg:Person {name: $person2}))\n"
660+
+ "WITH p, [n IN nodes(p) WHERE n:Movie] AS x\n"
661+
+ "UNWIND x AS m\n"
662+
+ "MATCH (m) <-[r:DIRECTED]-(d:Person)\n"
663+
+ "RETURN p, collect(r), collect(d)"
664+
)
665+
List<Movie> findAllOnShortestPathBetween(@Param("person1") String person1, @Param("person2") String person2);
666+
}
667+
----
668+
669+
The query returns the path plus all relationships and related nodes collected so that the movie entities are fully hydrated.
670+
671+
The path mapping works for single paths as well for multiple records of paths (which are returned by the `allShortestPath` function.)
672+
673+
TIP: Named paths can be used efficiently to populate and return more than just a root node, see <<custom-query.paths>>.
674+
675+
556676
[[faq.custom-queries-and-custom-mappings]]
557677
== Is `@Query` the only way to use custom queries?
558678

@@ -718,7 +838,7 @@ and one implementation. The implementation would than have had all three abstrac
718838
All of this applies of course to reactive repositories as well.
719839
They would work with the `ReactiveNeo4jTemplate` and `ReactiveNeo4jClient` and the reactive session provided by the driver.
720840

721-
If you have recuring methods for all repositories, you could swap out the default repository implementation.
841+
If you have recurring methods for all repositories, you could swap out the default repository implementation.
722842

723843
[[faq.custom-base-repositories]]
724844
== How do I use custom Spring Data Neo4j base repositories?
@@ -752,6 +872,37 @@ Those are
752872
* `org.springframework.data.annotation.LastModifiedBy`
753873
* `org.springframework.data.annotation.LastModifiedDate`
754874

875+
<<auditing>> gives you a general view how to use auditing in the bigger context of Spring Data Commons.
876+
The following listing presents every configuration option provided by Spring Data Neo4j:
877+
878+
[source,java,indent=0,tabsize=4]
879+
.Enabling and configuring Neo4j auditing
880+
----
881+
include::../../../../src/test/java/org/springframework/data/neo4j/integration/imperative/AuditingIT.java[tags=faq.entities.auditing]
882+
----
883+
<.> Set to true if you want the modification data to be written during creating as well
884+
<.> Use this attribute to specify the name of the bean that provides the auditor (i.e. a user name)
885+
<.> Use this attribute to specify the name of a bean that provides the current date. In this case
886+
a fixed date is used as the above configuration is part of our tests
887+
888+
The reactive version is basically the same apart from the fact the auditor aware bean is of type `ReactiveAuditorAware`,
889+
so that the retrieval of an auditor is part of the reactive flow.
890+
891+
In addition to those auditing mechanism you can add as many beans implementing `BeforeBindCallback<T>` or `ReactiveBeforeBindCallback<T>`
892+
to the context. These beans will be picked up by Spring Data Neo4j and called in order (in case they implement `Ordered` or
893+
are annotated with `@Order`) just before an entity is persisted.
894+
895+
They can modify the entity or return a completely new one.
896+
The following example adds one callback to the context that changes one attribute before the entity is persisted:
897+
898+
[source,java,indent=0,tabsize=4]
899+
.Modifying entities before save
900+
----
901+
include::../../../../src/test/java/org/springframework/data/neo4j/integration/imperative/CallbacksIT.java[tags=faq.entities.auditing.callbacks]
902+
----
903+
904+
No additional configuration is required.
905+
755906
[[faq.find-by-example]]
756907
== How do I use "Find by example"?
757908

@@ -779,125 +930,6 @@ movieExample = Example.of(
779930
movies = this.movieRepository.findAll(movieExample);
780931
----
781932

782-
[[faq.path-mapping]]
783-
== Can I map named paths?
784-
785-
A series of connected nodes and relationships is called a "path" in Neo4j.
786-
Cypher allows paths to be named using an identifer, as exemplified by:
787-
788-
[source,cypher]
789-
----
790-
p = (a)-[*3..5]->(b)
791-
----
792-
793-
or as in the infamous Movie graph, that includes the following path (in that case, one of the shortest path between two actors):
794-
795-
[[bacon-distance]]
796-
[source,cypher]
797-
.The "Bacon" distance
798-
----
799-
MATCH p=shortestPath((bacon:Person {name:"Kevin Bacon"})-[*]-(meg:Person {name:"Meg Ryan"}))
800-
RETURN p
801-
----
802-
803-
Which looks like this:
804-
805-
image::bacon-distance.png[]
806-
807-
We find 3 nodes labeled `Person` and 2 nodes labeled `Movie`. Both can be mapped with a custom queury.
808-
Assume there's a node entity for both `Person` and `Movie` as well as `Actor` taking care of the relationship:
809-
810-
811-
[source,java]
812-
."Standard" movie graph domain model
813-
----
814-
@Node
815-
public final class Person {
816-
817-
@Id @GeneratedValue
818-
private final Long id;
819-
820-
private final String name;
821-
822-
private Integer born;
823-
824-
@Relationship("REVIEWED")
825-
private List<Movie> reviewed = new ArrayList<>();
826-
}
827-
828-
@RelationshipProperties
829-
public final class Actor {
830-
831-
@Id @GeneratedValue
832-
private final Long id;
833-
834-
@TargetNode
835-
private final Person person;
836-
837-
private final List<String> roles;
838-
}
839-
840-
@Node
841-
public final class Movie {
842-
843-
@Id
844-
private final String title;
845-
846-
@Property("tagline")
847-
private final String description;
848-
849-
@Relationship(value = "ACTED_IN", direction = Direction.INCOMING)
850-
private final List<Actor> actors;
851-
}
852-
----
853-
854-
When using a query as shown in <<bacon-distance>> for a domain class of type `Person` like this
855-
856-
[source,java]
857-
----
858-
interface PeopleRepository extends Neo4jRepository<Person, Long> {
859-
@Query(""
860-
+ "MATCH p=shortestPath((bacon:Person {name: $person1})-[*]-(meg:Person {name: $person2}))\n"
861-
+ "RETURN p"
862-
)
863-
List<Person> findAllOnShortestPathBetween(@Param("person1") String person1, @Param("person2") String person2);
864-
}
865-
----
866-
867-
it will retrieve all people from the path and map them.
868-
If there are relationship types on the path like `REVIEWED` that are also present on the domain, these
869-
will be filled accordingly from the path.
870-
871-
WARNING: Take special care when you use nodes hydrated from a path based query to save data.
872-
If not all relationships are hydrated, data will be lost.
873-
874-
The other way round works as well. The same query can be used with the `Movie` entity.
875-
It then will only populate movies.
876-
The following listing shows how todo this as well as how the query can be enriched with additional data
877-
not found on the path. That data is used to correctly populate the missing relationships (in that case, all the actors)
878-
879-
[source,java]
880-
----
881-
interface MovieRepository extends Neo4jRepository<Movie, String> {
882-
883-
@Query(""
884-
+ "MATCH p=shortestPath(\n"
885-
+ "(bacon:Person {name: $person1})-[*]-(meg:Person {name: $person2}))\n"
886-
+ "WITH p, [n IN nodes(p) WHERE n:Movie] AS x\n"
887-
+ "UNWIND x AS m\n"
888-
+ "MATCH (m) <-[r:DIRECTED]-(d:Person)\n"
889-
+ "RETURN p, collect(r), collect(d)"
890-
)
891-
List<Movie> findAllOnShortestPathBetween(@Param("person1") String person1, @Param("person2") String person2);
892-
}
893-
----
894-
895-
The query returns the path plus all relationships and related nodes collected so that the movie entities are fully hydrated.
896-
897-
The path mapping works for single paths as well for multiple records of paths (which are returned by the `allShortestPath` function.)
898-
899-
TIP: Named paths can be used efficiently to populate and return more than just a root node, see <<custom-query.paths>>.
900-
901933
[[faq.spring-boot.sdn]]
902934
== Do I need Spring Boot to use Spring Data Neo4j?
903935

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

+26-12
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.core.DatabaseSelectionProvider;
@@ -127,32 +131,21 @@ interface ImmutableEntityWithGeneratedIdRepository
127131
extends Neo4jRepository<ImmutableAuditableThingWithGeneratedId, String> {}
128132

129133
@Configuration
134+
@Import(AuditingConfig.class)
130135
@EnableTransactionManagement
131136
@EnableNeo4jRepositories(considerNestedRepositories = true)
132-
@EnableNeo4jAuditing(modifyOnCreate = false, auditorAwareRef = "auditorProvider",
133-
dateTimeProviderRef = "fixedDateTimeProvider")
134137
static class Config extends AbstractNeo4jConfig {
135138

136139
@Bean
137140
public Driver driver() {
138141
return neo4jConnectionSupport.getDriver();
139142
}
140143

141-
@Bean
142-
public AuditorAware<String> auditorProvider() {
143-
return () -> Optional.of("A user");
144-
}
145-
146144
@Override
147145
protected Collection<String> getMappingBasePackages() {
148146
return Collections.singleton(ImmutableAuditableThing.class.getPackage().getName());
149147
}
150148

151-
@Bean
152-
public DateTimeProvider fixedDateTimeProvider() {
153-
return () -> Optional.of(DEFAULT_CREATION_AND_MODIFICATION_DATE);
154-
}
155-
156149
@Bean
157150
public BookmarkCapture bookmarkCapture() {
158151
return new BookmarkCapture();
@@ -166,3 +159,24 @@ public PlatformTransactionManager transactionManager(Driver driver, DatabaseSele
166159
}
167160
}
168161
}
162+
163+
// tag::faq.entities.auditing[]
164+
@Configuration
165+
@EnableNeo4jAuditing(
166+
modifyOnCreate = false, // <.>
167+
auditorAwareRef = "auditorProvider", // <.>
168+
dateTimeProviderRef = "fixedDateTimeProvider" // <.>
169+
)
170+
class AuditingConfig {
171+
172+
@Bean
173+
public AuditorAware<String> auditorProvider() {
174+
return () -> Optional.of("A user");
175+
}
176+
177+
@Bean
178+
public DateTimeProvider fixedDateTimeProvider() {
179+
return () -> Optional.of(AuditingITBase.DEFAULT_CREATION_AND_MODIFICATION_DATE);
180+
}
181+
}
182+
// end::faq.entities.auditing[]

0 commit comments

Comments
 (0)