This chapter covers the fundamentals of the programming model behind Spring Data Graph. It discusses the AspectJ features used and the annotations provided by Spring Data Graph and how to use them. Examples for this section are taken from the imdb project of Spring Data Graph examples.
Behind the scenes, Spring Data Graph leverages AspectJ aspects to modify the behavior of simple annotated POJO entities (see Chapter 25, AspectJ details). Each node entity is backed by a graph node that holds its properties and relationships to other entities. AspectJ is used for intercepting field access, so that Spring Data Graph can retrieve the information from the entity's backing node or relationship in the database.
The aspect introduces some internal fields and some public methods
(see Section 19.9, “Introduced methods”) in the entities, such as
entity.getPersistentState()
and entity.relateTo
.
It also introduces repository methods like
find(Class<? extends NodeEntity>, TraversalDescription)
,
and equals()
and hashCode
delegation, making equals()
honor the backing state.
Spring Data Graph internally uses an abstraction called EntityState
that the field
access and instantiation advices of the aspect delegate to. This way, the aspect code is kept to a
minimum, focusing mainly on the pointcuts and delegation code. The EntityState
then uses
a number of FieldAccessorFactories
to create a FieldAccessor
instance per
field that does the specific handling needed for the concrete field type. There are various layers of
caching involved as well, so it handles repeat instantiation efficiently.
As Spring Data Graph uses some advanced features of AspectJ, users may experience issues with their IDE reporting errors where there in fact are none. Features that might be reported wrongfully include: introduction of methods to interfaces, declaration of additional interfaces for annotated classes, and generified introduced methods.
IDE's not providing the full AJ support might mark parts of your code as errors.
You should rely on your build-system and test to verify the correctness of the code. You might also have
your Entities (or their interfaces) implement the NodeBacked
and RelationshipBacked
interfaces directly to benefit from completion support and error checking.
Eclipse and STS support AspectJ via the AJDT plugin which can be installed from the update-site: http://download.eclipse.org/tools/ajdt/36/update/ (it might be necessary to use the latest development snapshot of the plugin http://download.eclipse.org/tools/ajdt/36/dev/update). The current version that does not show incorrect errors is AspectJ 1.6.12.M1 (included in STS 2.7.0.M2), previous versions are reported to mislead the user.
The AspectJ support in IntelliJ IDEA lacks some of the features. JetBrains is working on improving
the situation in their upcoming 10.5 release of their popular IDE. Their latest work is available
under their early access program (EAP). Building the project with the AspectJ compiler
ajc
works in IDEA (Options -> Compiler -> Java Compiler should show ajc). Make sure to
give the compiler at least 512 MB of RAM.
Node entities are declared using the @NodeEntity
annotation. Relationship entities use
the @RelationshipEntity
annotation.
The @NodeEntity
annotation is used to turn a POJO class into an entity backed by a node
in the graph database. Fields on the entity are by default mapped to properties of the node. Fields
referencing other node entities (or collections thereof) are linked with relationships. If the
useShortNames
attribute overridden to false, the property and relationship names will
have the class name of the entity prepended.
@NodeEntity
annotations are inherited from super-types and interfaces. It is not necessary
to annotate your domain objects at every inheritance level.
If the partial
attribute is set to true, this entity takes part in a cross-store setting,
where the entity lives in both the graph database and a JPA data source. See
Chapter 21, Cross-store persistence for more information.
Entity fields can be annotated with @GraphProperty
, @RelatedTo
,
@RelatedToVia
, @Indexed
, @GraphId
and
@GraphTraversal
.
It is not necessary to annotate data fields, as they are persisted by default; all fields that
contain primitive values are persisted directly to the graph. All fields convertible to String
using the Spring conversion services will be stored as a string. Spring Data Graph includes a
custom conversion factory that comes with converters for Enum
s and Date
s.
Transient fields are not persisted.
Currently there is no support for handling arbitrary collections of primitive or convertable values. Support for this will be added by the 1.1. release.
This annotation is typically used with cross-store persistence. When a node entity is configured
as partial, then all fields that should be persisted to the graph must be explicitly annotated
with @GraphProperty
.
The @Indexed annotation can be declared on fields that are intended to be indexed by the Neo4j indexing facilities. The resulting index can be used to later retrieve nodes or relationships that contain a certain property value, e.g. a name. Often an index is used to establish the start node for a traversal. Indexes are accessed by a repository for a particular node or relationship entity type. See Section 19.4, “Indexing” and Section 19.5, “CRUD with repositories” for more information.
The @GraphQuery
annotation leverages the delegation infrastructure used by the
Spring Data Graph aspects. It provides dynamic fields which, when accessed, return the values
selected by the provided query language expression. The provided query must contain a placeholder
for the id of the current entity start n=(%d) match n-[:FRIEND]->friend return friend
As graph queries can return variable number of entities the annotation can be put onto fields
with a single value, an Iterable of a type or an Iterable of Map<String,Object>
.
The class of the resulting node entities must right now provided with the elementClass
attribute.
Additional parameters are added to the query with Java's String.format substitution.
Example 19.2. @GraphQuery from a node entity
@NodeEntity public class Group { @GraphQuery(value = "start n=(%d) match (n)-[:%s]->(friend) return friend", elementClass = Person.class, params = "FRIEND") private Iterable<Person> friends; }
The @GraphTraversal
annotation leverages the delegation infrastructure used by the
Spring Data Graph aspects. It provides dynamic fields which, when accessed, return an Iterable
of node entities that are the result of a traversal starting at the entity containing the field.
The TraversalDescription
used for this is created by the
FieldTraversalDescriptionBuilder
class defined by the traversalBuilder
attribute. The class of the resulting node entities must be provided with the
elementClass
attribute.
Example 19.3. @GraphTraversal from a node entity
@NodeEntity public class Group { @GraphTraversal(traversalBuilder = PeopleTraversalBuilder.class, elementClass = Person.class, params = "persons") private Iterable<Person> people; private static class PeopleTraversalBuilder implements FieldTraversalDescriptionBuilder { @Override public TraversalDescription build(NodeBacked start, Field field, String... params) { return new TraversalDescriptionImpl() .relationships(DynamicRelationshipType.withName(params[0])) .filter(Traversal.returnAllButStartNode()); } } }
Since relationships are first-class citizens in Neo4j, associations between node entities are represented by relationships. In general, relationships are categorized by a type, and start and end nodes (which imply the direction of the relationship). Relationships can have an arbitrary number of properties. Spring Data Graph has special support to represent Neo4j relationships as entities too, but it is often not needed.
As of Neo4j 1.4.M03, circular references are allowed. Spring Data Graph reflects this accordingly.
Every field of a node entity that references one or more other node entities is backed by relationships in the graph. These relationships are managed by Spring Data Graph automatically.
The simplest kind of relationship is a single field pointing to another node entity (1:1).
In this case, the field does not have to be annotated at all, although the annotation may be
used to control the direction and type of the relationship. When setting the field, a
relationship is created. If the field is set to null
, the relationship is removed.
Example 19.4. Single relationship field
@NodeEntity public class Movie { private Actor mostPaidActor; }
It is also possible to have fields that reference a set of node entities (1:N). These fields come in
two forms, modifiable or read-only. Modifiable fields are of the type java.util.Set<T>
,
and read-only fields are java.lang.Iterable<T>
, where T is a @NodeEntity-annotated
class. The Java implementation of generics uses type erasure, meaning that the type parameters are
typically not available at runtime. Therefore, the elementClass
attribute must be
specified on the annotation, which must always be present for 1:N fields.
Example 19.5. Node entity with relationships
@NodeEntity public class Actor { @RelatedTo(type = "mostPaidActor", direction = Direction.INCOMING, elementClass = Movie.class) private Set<Movie> mostPaidIn; @RelatedTo(type = "ACTS_IN", elementClass = Movie.class) private Set<Movie> movies; }
Fields referencing other entities should not be manually initialized, as they are managed by Spring Data Graph under the hood. 1:N fields can be accessed immediately, and Spring Data Graph will provide a java.util.Set representing the relationships. If the returned set is modified, the changes are reflected in the graph. Spring Data Graph also ensures that there is only one relationship of a given type between any two given entities.
Before an entity has been attached with persist()
for the first time, it will
not have its state managed by Spring Data Graph. For example, given the Actor class defined above,
if actor.movies
was accessed in a non-persisted entity, it would return
null
, whereas if it was accessed in a persisted entity, it would return
an empty managed set.
When you use an Interface as target type for the Set
and/or as elementClass
please make sure that it implements NodeBacked
either by extending that Super-Interface manually
or by annotating the Interface with @NodeEntity
too.
By setting direction to BOTH
, relationships are created in the outgoing direction, but when the
1:N field is read, it will include relationships in both directions. A cardinality of M:N is
not necessary because relationships can be navigated in both directions.
The relationships can also be accessed by using the aspect-introduced methods
entity.getRelationshipTo(target, type)
and
entity.relateTo(target, type)
available on each NodeEntity.
These methods find and create Neo4j relationships. It is also possible to manually remove
relationships by using entity.removeRelationshipTo(target, type)
.
Using these methods is significantly faster than adding/removing from the collection of
relationships as it doesn't have to re-synchronize a whole set of relationships with the graph.
Other collection types than Set
are not supported so far, also currently NO
Map<RelationshipType,Set<NodeBacked>>
.
To access the full data model of graph relationships, POJOs can also be annotated with
@RelationshipEntity
, making them relationship entities. Just as node entities represent
nodes in the graph, relationship entities represent relationships. As described above,
fields annotated with @RelatedTo
provide a way to link node entities together
via relationships, but it provides no way of accessing the relationships themselves.
Relationship entities cannot be instantiated directly but are rather created via
node entities, either by @RelatedToVia-annotated fields
(see Section 19.3.3, “@RelatedToVia: Accessing relationship entities”),
or by the introduced
entity.relateTo(target, relationshipClass, type)
and
entity.getRelationshipTo(target, relationshipClass, type)
methods
(see Section 19.9, “Introduced methods”).
Fields in relationship entities are, similarly to node entities, persisted as properties on
the relationship. For accessing the two endpoints of the relationship, two special annotations
are available: @StartNode
and @EndNode
. A field annotated with
one of these annotations will provide read-only access to the corresponding endpoint, depending
on the chosen annotation.
Example 19.6. Relationship entity
@NodeEntity public class Actor { public Role playedIn(Movie movie, String title) { return relatedTo(movie, Role.class, "ACTS_IN"); } } @RelationshipEntity public class Role { String title; @StartNode private Actor actor; @EndNode private Movie movie; }
To provide easy programmatic access to the richer relationship entities of the data model,
the annotation @RelatedToVia
can be added on fields of type
java.lang.Iterable<T>
, where T is a @RelationshipEntity
-annotated
class. These fields provide read-only access to relationship entities.
Example 19.7. Accessing relationship entities using @RelatedToVia
@NodeEntity public class Actor { @RelatedToVia(type = "ACTS_IN", elementClass = Role.class) private Iterable<Role> roles; public Role playedIn(Movie movie, String title) { Role role = relateTo(movie, Role.class, "ACTS_IN"); role.setTitle(title); return role; } }
The Neo4j graph database can use different so-called index providers for exact lookups and fulltext searches. Lucene is the default index provider implementation. Each named index is configured to be fulltext or exact.
When using the standard Neo4j API, nodes and relationships have to be manually indexed with
key-value pairs, typically being the property name and value. When using Spring Data Graph,
this task is simplified to just adding an @Indexed
annotation on entity fields
by which the entity should be searchable. This will result in automatic updates of the index
every time an indexed field changes.
Numerical fields are indexed numerically so that they are available for range queries. All other fields are indexed with their string representation.
The @Indexed annotation also provides the option of using a custom index. The default index name is the simple class name of the entity, so that each class typically gets its own index. It is recommended to not have two entity classes with the same class name, regardless of package.
The indexes can be queried by using a repository (see
Section 19.5, “CRUD with repositories”).
Typically, the repository is an instance of
org.springframework.data.graph.neo4j.repository.DirectGraphRepositoryFactory
.
The methods findByPropertyValue()
and findAllByPropertyValue()
work on
the exact indexes and return the first or all matches. To do range queries, use
findAllByRange()
(please note that currently both values are inclusive).
Example 19.8. Indexing entities
@NodeEntity class Person { @Indexed(indexName = "people") String name; @Indexed int age; } GraphRepository<Person> graphRepository = graphRepositoryFactory .createGraphRepository(Person.class); // Exact match, in named index Person mark = graphRepository.findByPropertyValue("people", "name", "mark"); // Numeric range query, index name inferred automatically for (Person middleAgedDeveloper : graphRepository.findAllByRange("age", 20, 40)) { Developer developer=middleAgedDeveloper.projectTo(Developer.class); }
Spring Data Graph also supports fulltext indexes. By default, indexed fields are stored in
an exact lookup index. To have them analyzed and prepared for fulltext search, the
@Indexed
annotation has the boolean fulltext
attribute.
Please note that fulltext indexes require a separate index name as the fulltext configuration
is stored in the index itself.
Access to the fulltext index is provided by the findAllByQuery()
repository method.
Wildcards like *
are allowed. Generally though, the fulltext querying rules of the
underlying index provider apply. See the
Lucene documentation for more
information on this.
Example 19.9. Fulltext indexing
@NodeEntity class Person { @Indexed(indexName = "person-name", fulltext=true) String name; } GraphRepository<Person> graphRepository = graphRepositoryFactory .createGraphRepository(Person.class); Person mark = graphRepository.findAllByQuery("people-search", "name", "ma*");
The index for a domain class is also available from GraphDatabaseContext
via
the getIndex()
method. The second parameter is optional and takes the index name
if it should not be inferred from the class name. It returns the index implementation that is
provided by Neo4j.
Example 19.10. Manual index usage
@Autowired GraphDatabaseContext gdc; // Default index Index<Node> personIndex = gdc.getIndex(Person.class); personIndex.query(new QueryContext(NumericRangeQuery.newÍntRange("age", 20, 40, true, true)) .sort(new Sort(new SortField("age", SortField.INT, false)))); // Named index Index<Node> namedPersonIndex = gdc.getIndex(Person.class, "people"); namedPersonIndex.get("name", "Mark"); // Fulltext index Index<Node> personFulltextIndex = gdc.getIndex(Person.class, "person-name", true); personFulltextIndex.query("name", "*cha*"); personFulltextIndex.query("{name:*cha*}");
Neo4jTemplate also offers index support, providing auto-indexing for fields at creation time.
There is an autoIndex
method that can also add indexes for a set of fields in one go.
For querying the index, the template offers query methods that take either the exact match
parameters or a query object/expression, and push the results wrapped uniformly as Paths to
the supplied PathMapper
to be converted or collected.
The repositories provided by Spring Data Graph build on the composable repository infrastructure in Spring Data Commons. They allow for interface based composition of repositories consisting of provided default implementations for certain interfaces and additional custom implementations for other methods.
Spring Data Graph comes with typed repository implementations that provide methods for
locating node and relationship entities. There are 3 types of basic repository interfaces
and implementations. CRUDRepository
provides basic operations,
IndexRepository
and NamedIndexRepository
delegate to Neo4j's internal
indexing subsystem for queries, and TraversalRepository
handles Neo4j traversals.
GraphRepository
is a convenience repository interface, extending CRUDRepository
,
IndexRepository
, and TraversalRepository
. Generally, it has all the
desired repository methods. If named index operations are required, then NamedIndexRepository
may also be included.
CRUDRepository
delegates to the configured TypeRepresentationStrategy
(see Section 19.8, “Entity type representation”)
for type based queries.
T findOne(id)
boolean exists(id)
Iterable<T> findAll()
(supported in future versions:
Iterable<T> findAll(Sort)
and
Page<T> findAll(Pageable)
)
Long count()
T save(T)
and Iterable<T> save(Iterable<T>)
void delete(T)
, void; delete(Iterable<T>)
,
and deleteAll()
Important to note here is that the save
, delete
, and deleteAll
methods are only there to conform to the org.springframework.data.repository.Repository
interface. The recommended way of saving and deleting entities is by using entity.persist()
and entity.remove()
.
IndexRepository
works with the indexing subsystem and provides methods to find
entities by indexed properties, ranged queries, and combinations thereof. The index key is
the name of the indexed entity field, unless overridden in the @Indexed
annotation.
Iterable<T> findAllByPropertyValue(key, value)
T findByPropertyValue(key, value)
Iterable<T> findAllByRange(key, from, to)
Iterable<T> findAllByQuery(key, queryOrQueryContext)
There is also a NamedIndexRepository
with the same methods, but with an additional index
name parameter, making it possible to query any index.
TraversalRepository
delegates to the Neo4j traversal framework.
Iterable<T> findAllByTraversal(startEntity, traversalDescription)
The Repository
instances are either created manually via a
DirectGraphRepositoryFactory
, bound to a concrete node or relationship entity class.
The DirectGraphRepositoryFactory
is configured in the Spring context and can be injected.
Example 19.11. Using GraphRepositories
GraphRepository<Person> graphRepository = graphRepositoryFactory .createGraphRepository(Person.class); Person michael = graphRepository.save(new Person("Michael", 36)); Person dave = graphRepository.findOne(123); Long numberOfPeople = graphRepository.count(); Person mark = graphRepository.findByPropertyValue("name", "mark"); Iterable<Person> devs = graphRepository.findAllByProperyValue("occupation", "developer"); Iterable<Person> middleAgedPeople = graphRepository.findAllByRange("age", 20, 40); Iterable<Person> aTeam = graphRepository.findAllByQuery("name", "A*"); Iterable<Person> davesFriends = graphRepository.findAllByTraversal(dave, Traversal.description().pruneAfterDepth(1) .relationships(KNOWS).filter(returnAllButStartNode()));
The recommended way of providing repositories is to define a repository interface per domain class. The mechanisms provided by the repository infrastructure will automatically detect them, along with additional implementation classes, and create an injectable repository implementation to be used in services or other spring beans.
Example 19.12. Composing repositories
public interface PersonRepository extends GraphRepository<Person>, PersonRepositoryExtension {} // alternatively select some of the required repositories individually public interface PersonRepository extends CRUDGraphRepository<Node,Person>, IndexQueryExecutor<Node,Person>, TraversalQueryExecutor<Node,Person>, PersonRepositoryExtension {} // provide a custom extension if needed public interface PersonRepositoryExtension { Iterable<Person> findFriends(Person person); } public class PersonRepositoryImpl implements PersonRepositoryExtension { // optionally inject default repository, or use DirectGraphRepositoryFactory @Autowired PersonRepository baseRepository; public Iterable<Person> findFriends(Person person) { return baseRepository.findAllByTraversal(person, friendsTraversal); } } // configure the repositories, preferably via the datagraph:repositories namespace // (graphDatabaseContext reference is optional) <datagraph:repositories base-package="org.springframework.data.graph.neo4j" graph-database-context-ref="graphDatabaseContext"/> // have it injected @Autowired PersonRepository personRepository; Person michael = personRepository.save(new Person("Michael",36)); Person dave=personRepository.findOne(123); Iterable<Person> devs = personRepository.findAllByProperyValue("occupation","developer"); Iterable<Person> aTeam = graphRepository.findAllByQuery( "name","A*"); Iterable<Person> friends = personRepository.findFriends(dave);
Neo4j is a transactional database, only allowing modifications to be performed within transaction boundaries. Reading data does however not require transactions.
Spring Data Graph integrates with transaction managers configured using Spring. The simplest scenario of just running the graph database uses a SpringTransactionManager provided by the Neo4j kernel to be used with Spring's JtaTransactionManager. That is, configuring Spring to use Neo4j's transaction manager.
The explicit XML configuration given below is encoded in the Neo4jConfiguration
configuration bean that uses Spring's @Configuration
feature. This greatly
simplifies the configuration of Spring Data Graph.
Example 19.13. Simple transaction manager configuration
<bean id="transactionManager" class="org.springframework.transaction.jta.JtaTransactionManager"> <property name="transactionManager"> <bean class="org.neo4j.kernel.impl.transaction.SpringTransactionManager"> <constructor-arg ref="graphDatabaseService"/> </bean> </property> <property name="userTransaction"> <bean class="org.neo4j.kernel.impl.transaction.UserTransactionImpl"> <constructor-arg ref="graphDatabaseService"/> </bean> </property> </bean> <tx:annotation-driven mode="aspectj" transaction-manager="transactionManager"/>
For scenarios with multiple transactional resources there are two options. The first option is to have Neo4j participate in the externally configured transaction manager by using the Spring support in Neo4j by enabling the configuration parameter for your graph database. Neo4j will then use Spring's transaction manager instead of its own.
Example 19.14. Neo4j Spring integration
<context:annotation-config /> <context:spring-configured/> <bean id="transactionManager" class="org.springframework.transaction.jta.JtaTransactionManager"> <property name="transactionManager"> <bean id="jotm" class="org.springframework.data.graph.neo4j.transaction.JotmFactoryBean"/> </property> </bean> <bean class="org.neo4j.kernel.EmbeddedGraphDatabase" destroy-method="shutdown"> <constructor-arg value="target/test-db"/> <constructor-arg> <map> <entry key="tx_manager_impl" value="spring-jta"/> </map> </constructor-arg> </bean> <tx:annotation-driven mode="aspectj" transaction-manager="transactionManager"/>
One can also configure a stock XA transaction manager (e.g. Atomikos, JOTM, App-Server-TM) to be
used with Neo4j and the other resources. For a bit less secure but fast 1 phase commit best effort,
use ChainedTransactionManager
, which comes bundled with Spring Data Graph. It takes a
list of transaction managers as constructor params and will handle them in order for transaction
start and commit (or rollback) in the reverse order.
Example 19.15. ChainedTransactionManager example
<bean id="jpaTransactionManager" class="org.springframework.orm.jpa.JpaTransactionManager"> <property name="entityManagerFactory" ref="entityManagerFactory"/> </bean> <bean id="jtaTransactionManager" class="org.springframework.transaction.jta.JtaTransactionManager"> <property name="transactionManager"> <bean class="org.neo4j.kernel.impl.transaction.SpringTransactionManager"> <constructor-arg ref="graphDatabaseService" /> </bean> </property> <property name="userTransaction"> <bean class="org.neo4j.kernel.impl.transaction.UserTransactionImpl"> <constructor-arg ref="graphDatabaseService" /> </bean> </property> </bean> <bean id="transactionManager" class="org.springframework.data.graph.neo4j.transaction.ChainedTransactionManager"> <constructor-arg> <list> <ref bean="jpaTransactionManager"/> <ref bean="jtaTransactionManager"/> </list> </constructor-arg> </bean> <tx:annotation-driven mode="aspectj" transaction-manager="transactionManager"/>
Node entities can be in two different persistence state: attached or detached. By default, newly created node
entities are in the detached state. When persist()
is called on the entity, it becomes
attached to the graph, and its properties and relationships are stores in the database. If
persist()
is not called within a transaction, it automatically creates an implicit
transaction for the operation.
Changing an attached entity inside a transaction will immediately write through the changes to
the datastore. Whenever an entity is changed outside of a transaction it becomes detached. The
changes are stored in the entity itself until the next call to persist()
.
All entities returned by library functions are initially in an attached state.
Just as with any other entity, changing them outside of a transaction detaches them, and they
must be reattached with persist()
for the data to be saved.
Example 19.16. Persisting entities
@NodeEntity class Person { String name; Person(String name) { this.name = name; } } // Store Michael in the database. Person p = new Person("Michael").persist();
As mentioned above, an entity simply created with the new
keyword starts out detached.
It also has no state assigned to it. If you create a new entity with new
and then throw
it away, the database won't be touched at all.
Now consider this scenario:
Example 19.17. Relationships outside of transactions
@NodeEntity class Movie { private Actor topActor; public void setTopActor(Actor actor) { topActor = actor; } } @NodeEntity class Actor { } Movie movie = new Movie(); Actor actor = new Actor(); movie.setTopActor(actor);
Neither the actor nor the movie has been assigned a node in the graph. If we were to call
movie.persist()
, then Spring Data Graph would first create a node for the movie.
It would then note that there is a relationship to an actor, so it would call actor.persist()
in a cascading fashion. Once the actor has been persisted, it will create the relationship
from the movie to the actor. All of this will be done atomically in one transaction.
Important to note here is that if actor.persist()
is called instead, then only
the actor will be persisted. The reason for this is that the actor entity knows nothing about
the movie entity. It is the movie entity that has the reference to the actor. Also note that
this behavior is not dependent on any configured relationship direction on the annotations.
It is a matter of Java references and is not related to the data model in the database.
The persist operation (merge) stores all properties of the entity to the graph database and puts the entity in attached mode. There is no need to update the reference to the Java POJO as the underlying backing node handles the read-through transparently. If multiple object instances that point to the same node are persisted, the ordering is not important as long as they contain distinct changes. For concurrent changes a concurrent modification exception is thrown (subject to be parametrizable in the future).
If the relationships form a cycle, then the entities will first all be assigned a node in
the database, and then the relationships will be created. The cascading of persist()
is however only cascaded to related entity fields that have been modified.
In the following example, the actor and the movie are both attached entites, having both been previously persisted to the graph:
In this case, even though the movie has a reference to the actor, the name change on the actor
will not be persisted by the call to movie.persist()
. The reason for this is, as
mentioned above, that cascading will only be done for fields that have been modified. Since the
movie.topActor
field has not been modified, it will not cascade the persist operation
to the actor.
There are several ways to represent the Java type hierarchy of the data model in the graph. In general, for all node and relationship entities, type information is needed to perform certain repository operations. Some of this type information is saved in the graph database.
Implementations of TypeRepresentationStrategy
take care of persisting this information on entity instance
creation. They also provide the repository methods that use this type information to perform their operations,
like findAll and count.
There are three available implementations for node entities to choose from.
IndexingNodeTypeRepresentationStrategy
Stores entity types in the integrated index. Each entity node gets indexed with its type and
any supertypes that are also@NodeEntity
-annotated. The special index used for this
is called__types__
. Additionally, in order to get the type of an entity node, each
node has a property
__type__
with the type of that entity.
SubReferenceNodeTypeRepresentationStrategy
Stores entity types in a tree in the graph representing the type hierarchy. Each entity has a INSTANCE_OF relationship to a type node representing that entity's type. The type may or may not have a SUBCLASS_OF relationship to another type node.
NoopNodeTypeRepresentationStrategy
Does not store any type information, and does hence not support finding by type, counting by type, or retrieving the type of any entity.
There are two implementations for relationship entities available, same behavior as the corresponding ones above:
IndexingRelationshipTypeRepresentationStrategy
NoopRelationshipTypeRepresentationStrategy
Spring Data Graph will by default autodetect which are the most suitable strategies for node and relationship
entities. For new data stores, it will always opt for the indexing strategies. If a data store was created
with the olderSubReferenceNodeTypeRepresentationStrategy
, then it will continue to use that
strategy for node entities. It will however in that case use the no-op strategy for relationship entities,
which means that the old data stores have no support for searching for relationship entities. The indexing
strategies are recommended for all new users.
The node and relationship aspects introduce (via AspectJ ITD - inter type declaration) several methods to the entities.
nodeEntity.persist()
nodeEntity.getNodeId()
and relationshipEntity.getRelationshipId()
entity.getPersistentState()
entity.equals()
and entity.hashCode()
nodeEntity.relateTo(targetEntity, relationshipClass, relationshipType)
nodeEntity.getRelationshipTo(targetEntity, relationshipClass, relationshipType)
nodeEntity.relateTo(targetEntity, relationshipType)
nodeEntity.getRelationshipTo(targetEnttiy, relationshipType)
nodeEntity.removeRelationshipTo(targetEntity, relationshipType)
nodeEntity.remove()
and relationshipEntity.remove()
entity.projectTo(targetClass)
nodeEntity.findAllByTraversal(targetType, traversalDescription)
EntityPath
s of the traversal result
bound to the provided start and end-node-entity types
Iterable<EntityPath> findAllPathsByTraversal(traversalDescription)
As the underlying data model of a graph database doesn't imply and enforce strict type constraints like a relational model does, it offers much more flexibility on how to model your domain classes and which of those to use in different contexts.
For instance an order can be used in these contexts: customer, procurement, logistics, billing, fulfillment and many more. Each of those contexts requires its distinct set of attributes and operations. As Java doesn't support mixins one would put the sum of all of those into the entity class and thereby making it very big, brittle and hard to understand. Being able to take a basic order and project it to a different (not related in the inheritance hierarchy or even an interface) order type that is valid in the current context and only offers the attributes and methods needed here would be very benefitial.
Spring Data Graph offers initial support for projecting node and relationship entities to different target types. All instances of this projected entity share the same backing node or relationship, so data changes are reflected immediately.
This could for instance also be used to handle nodes of a traversal with a unified (simpler) type (e.g. for reporting or auditing) and only project them to a concrete, more functional target type when the business logic requires it.
Example 19.19. Projection of entities
@NodeEntity class Trainee { String name; @RelatedTo(elementClass=Training.class); Set<Training> trainings; } for (Person person : graphRepository.findAllByProperyValue("occupation","developer")) { Developer developer = person.projectTo(Developer.class); if (developer.isJavaDeveloper()) { trainInSpringData(developer.projectTo(Trainee.class)); } }
Spring Data Graph supports property-based validation support. When a property is changed, it is
checked against the annotated constraints, e.g. @Min
, @Max
,
@Size
, etc. Validation errors throw a ValidationException
. The validation
support that comes with Spring is used for evaluating the constraints. To use this feature, a validator
has to be registered with the GraphDatabaseContext
.
Example 19.20. Bean validation
@NodeEntity class Person { @Size(min = 3, max = 20) String name; @Min(0) @Max(100) int age; }