Chapter 21. Cross-store persistence

The Spring Data Graph project support cross-store persistence, which allows for parts of the data to be stored in a traditional JPA data store (RDBMS), and other parts in a graph store. This means that an entity can be partially stored in e.g. MySQL, and partially stored in Neo4j.

This allows existing JPA-based applications to embrace NOSQL data stores for evolving certain parts of their data model. Possible use cases include adding social networking or geospatial information to existing applications.

21.1. Partial entities

Partial graph persistence is achieved by restricting the Spring Data Graph aspects to manage only explicitly annotated parts of the entity. Those fields will be made @Transient by the aspect so that JPA ignores them.

A backing node in the graph store is only created when the entity has been assigned a JPA ID. Only then will the association between the two stores be established. Until the entity has been persisted, its state is just kept inside the POJO (in detached state), and then flushed to the backing graph database on persist().

The association between the two entities is maintained via a FOREIGN_ID field in the node, that contains the JPA ID. Currently only single-value IDs are supported. The entity class can be resolved via the TypeRepresentationStrategy that manages the Java type hierarchy within the graph database. Given the ID and class, you can then retrieve the appropriate JPA entity for a given node.

The other direction is handled by indexing the Node with the FOREIGN_ID index which contains a concatenation of the fully qualified class name of the JPA entity and the ID. The matching node can then be found using the indexing facilities, and the two entities can be reassociated.

Using these mechanisms and the Spring Data Graph aspects, a single POJO can contain some fields handled by JPA and others handles by Spring Data Graph. This also includes relationship fields persisted in the graph database.

21.2. Cross-store annotations

Cross-store persistence only requires the use of one additional annotation: @GraphProperty. See below for details and an example.

21.2.1. @NodeEntity(partial = "true")

When annotating an entity with partial = true, this marks it as a cross-store entity. Spring Data Graph will thus only manage fields explicitly annotated with @GraphProperty.

21.2.2. @GraphProperty

Fields of primitive or convertible types do not normally have to be annotated in order to be persisted by Spring Data Graph. In cross-store mode, Spring Data Graph only persists fields explicitly annotated with @GraphProperty. JPA will ignore these fields.

The following example is taken from the Spring Data Graph examples myrestaurants-social project:

Example 21.1. Cross-store node entity

@Entity
@Table(name = "user_account")
@NodeEntity(partial = true)
public class UserAccount {
    private String userName;
    private String firstName;
    private String lastName;

    @GraphProperty
    String nickname;

    @RelatedTo(type = "friends", elementClass = UserAccount.class)
    Set<UserAccount> friends;

    @RelatedToVia(type = "recommends", elementClass = Recommendation.class)
    Iterable<Recommendation> recommendations;

    @Temporal(TemporalType.TIMESTAMP)
    @DateTimeFormat(style = "S-")
    private Date birthDate;

    @ManyToMany(cascade = CascadeType.ALL)
    private Set<Restaurant> favorites;

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "id")
    private Long id;

    @Transactional
    public void knows(UserAccount friend) {
        relateTo(friend, "friends");
    }

    @Transactional
    public Recommendation rate(Restaurant restaurant, int stars, String comment) {
        Recommendation recommendation = relateTo(restaurant, Recommendation.class, "recommends");
        recommendation.rate(stars, comment);
        return recommendation;
    }

    public Iterable<Recommendation> getRecommendations() {
        return recommendations;
    }
}

21.3. Configuring cross-store persistence

Configuring cross-store persistence is done similarly to the default Spring Data Graph configuration. All you need to do is to specify an entityManagerFactory in the XML namespace config element, and Spring Data Graph will configure itself for cross-store use.

Example 21.2. Cross-store Spring configuration

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:datagraph="http://www.springframework.org/schema/data/graph"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context-3.0.xsd
        http://www.springframework.org/schema/data/graph
        http://www.springframework.org/schema/data/graph/datagraph-1.0.xsd
        ">

    <context:annotation-config/>

    <datagraph:config storeDirectory="target/config-test"
        entityManagerFactory="entityManagerFactory"/>

    <bean class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"
            id="entityManagerFactory">
        <property name="dataSource" ref="dataSource"/>
        <property name="persistenceXmlLocation" value="classpath:META-INF/persistence.xml"/>
    </bean>
</beans>