Introduction

Neo4j a Graph database that fits nicely in a Grails application.

The goal of GORM for Neo4j is to provide a as-complete-as-possible GORM implementation that maps domain classes and instances to the Neo4j nodespace. The following features are supported:

  • Marshalling from Neo4j Nodes to Groovy types and back again

  • Support for GORM dynamic finders, criteria and named queries

  • Session-managed transactions

  • access to Neo4j’s traversal capabilities

  • Access the Neo4j graph database via the Bolt Java Driver

  • Neo4j autoindexing

Compatibility with GORM for Hibernate

This implementation tries to be as compatible as possible with GORM for Hibernate. In general you can refer to the GORM for Hibernate documentation as the majority of features are implemented across both.

The following key features are supported by GORM for Neo4j:

  • Simple persistence methods (save,delete etc)

  • Dynamic finders

  • Criteria queries

  • Named queries

  • Inheritance

  • Embedded types

  • Query by example

  • Many-to-many associations (these can be modelled with a mapping class)

However, some features are not supported:

  • HQL queries (however Cypher Queries are)

  • Composite primary keys

  • Any direct interaction with the Hibernate API

  • Custom Hibernate user types

There may be other limitations not mentioned here so in general it shouldn’t be expected that an application based on GORM for Hibernate will "just work" without some tweaking involved. Having said that, the large majority of common GORM functionality is supported.

Release History

7.1.x

  • Support Apache Groovy 3, and Java 14

  • Neo4J Driver 4.3

  • Spring 5.3, Spring Boot 2.5

  • Autowire bean by-type in the Data Service

  • Compatible Only with Grails 5 or higher

7.0.x

  • Java 8 Minimum

  • Support for Neo4j Bolt Driver 1.7

  • Support for Neo4j 3.5.x

6.2.x

  • Support for Neo4j Bolt Driver 1.5

  • Support for Neo4j 3.3.x

6.1.x

  • Support for assigned identifiers

  • Batch inserts with UNWIND and FOREACH when using assigned ids

  • Support for mapping entities to Neo4j Relationships

  • Support for querying Neo4j Paths

  • Support for lists basic types

  • Upgrade to Neo4j Bolt Driver 1.2

6.0.x

  • Support for Neo4j 3.0.x or above

  • Rewritten for Neo4j Bolt Java Driver

  • Support for Multiple Data Sources (Connections)

  • Multi Tenancy support for DATABASE and DISCRIMINATOR approaches

  • Refactored Bootstrapping

  • Uses new DynamicAttributes trait

5.0.x

The following new features are available in this release.

  • Support for Neo4j 2.3.x or above

  • Ability to query using Cypher with the default GORM methods (find, findAll)

  • Robust Spring Transaction Management

  • Support for Lazy & Eager Loading using OPTIONAL MATCH

  • Improved Performance

  • Dirty Checking Implementation

If you are using an older version of the plugin, and looking to upgrade the following changes may impact you:

  • Neo4j JDBC is no longer used and the corresponding CypherEngine interface was removed

  • Dynamic associations are disabled by default, you can re-enable them in your entity mapping

4.0.x

  • Rewritten for Groovy traits & Grails 3

3.0.x

  • Initial support for Neo4j 2.0.x

  • Support for Cypher via Neo4j JDBC

2.0.x

  • Refinements from 1.0.x

1.0.x

  • works with Neo4j HA

  • implementing new GORM property criteria filters

  • uses Neo4j 1.8.2

  • first GORM compliant version of the plugin

  • works with embedded and REST Neo4j databases

  • exposing traversal options to domain classes

Upgrading from previous versions

Dependency Upgrades

GORM 7.1 supports Apache Groovy 3, Java 14, Neo4J Driver 4.3 and Spring 5.3.x.

Each of these underlying components may have changes that require altering your application. These changes are beyond the scope of this documentation.

Default Autowire By Type inside GORM Data Services

A Grails Service (or a bean) inside GORM DataService will default to autowire by-type, For example:

./grails-app/services/example/BookService.groovy

package example

import grails.gorm.services.Service

@Service(Book)
abstract class BookService {

    TestService testRepo

    abstract Book save(String title, String author)

    void doSomething() {
        assert testRepo != null
    }
}

Please note that with autowire by-type as the default, when multiple beans for same type are found the application with throw Exception. Use the Spring `@Qualifier annotation for Fine-tuning Annotation Based Autowiring with Qualifiers.

Configuration Changes

The version 4 of the Neo4j driver contains changes in the configuration builder which require configuration changes to conform to them. For example grails.neo4j.options.maxSessions is now grails.neo4j.options.maxConnectionPoolSize. Some configuration defaults have also changed, so be sure to check the documentation of the driver for more information.

Bolt Java Driver and API Changes

The API has changed to accommodate the new Neo4j Bolt driver.

Therefore you need to replace the usages of the following interfaces with their bolt equivalents:

Class Replacement

org.neo4j.driver.v1.Driver

org.neo4j.driver.Driver

org.neo4j.driver.v1.Transaction

org.neo4j.driver.Transaction

org.neo4j.driver.v1.types.Node

org.neo4j.driver.types.Node

org.neo4j.driver.v1.types.Relationship

org.neo4j.driver.types.Relationship

org.neo4j.driver.v1.StatementResult

org.neo4j.driver.Result

There may be other classes that you need to replace references too. The org.neo4j.driver.v1 package has shed its v1 package, however there are also other changes.[.line-through]##

Getting Started

To get started with GORM for Neo4j you need to install the plugin into a Grails application.

For Grails 3.x and above you need to edit your build.gradle file and add the plugin as a dependency:

build.gradle
dependencies {
	compile 'org.grails.plugins:neo4j:7.1.1-SNAPSHOT'
}

If you are using a version of Grails 3 earlier than 3.3 then you may need to enforce the GORM version. If you are using Grails 3.2.7 or above this can be done by modifying the gormVersion setting in gradle.properties:

gormVersion=7.1.1-SNAPSHOT

Otherwise if you are using an earlier version of Grails you can force the GORM version by adding the following block directly above the dependencies block:

build.gradle
configurations.all {
    resolutionStrategy.eachDependency { DependencyResolveDetails details ->
        if( details.requested.group == 'org.grails' &&
            details.requested.name.startsWith('grails-datastore')) {
            details.useVersion("7.1.1-SNAPSHOT")
        }
    }
}
dependencies {
    ...
}
GORM for Neo4j requires Grails 2.5.x or above and Neo4j 3.0.x or above, if you wish to use Neo4j 2.3.x use the 5.x version of GORM

To configure the plugin for Grails 2.x edit the grails-app/conf/BuildConfig.groovy file and add the following plugin definitions:

plugins {
    compile ':neo4j:7.1.1-SNAPSHOT'
    build ':tomcat:8.22'
}
Grails 2.5.x must be configured with Tomcat 8 when using Neo4j in embedded mode, since the Neo4j server depends newer versions of the Servlet API and will not work with Tomcat 7.

By default the Grails plugin assumes you have a Neo4j instance running on port 7687, however you can run Neo4j embedded by including the following configuration in grails-app/conf/application.yml:

grails:
    neo4j:
        type: embedded

And then adding the Neo4j test harness to your provided dependencies:

provided 'org.neo4j.test:neo4j-harness:3.0.2'
The Neo4j server uses Jetty, so when you add Neo4j as embedded Grails will also use Jetty (not Tomcat) as the container since it discovered on the classpath, therefore it is not recommended to use Neo4j embedded.

To configure the Neo4j server URL you can use the grails.neo4j.url setting in grails-app/conf/application.yml:

grails:
    neo4j:
        url: bolt://localhost:7687

Neo4j Bolt Driver Configuration Options

The following options can be configured in grails-app/conf/application.yml:

  • grails.neo4j.url - The Neo4j Bolt URL

  • grails.neo4j.buildIndex - Whether to build the Neo4j index on startup (defaults to true)

  • grails.neo4j.type - The Neo4j server type. If set to embedded loads an embedded server

  • grails.neo4j.flush.mode - The flush mode to use when working with Neo4j sessions. Default to AUTO.

  • grails.neo4j.username - The username to use to authenticate

  • grails.neo4j.password - The password to use to authenticate

  • grails.neo4j.default.mapping - The default database mapping. Must be a closure configured in application.groovy

  • grails.neo4j.options - Any options to be passed to the driver

The grails.neo4j.options setting allows you to configure the properties of org.neo4j.driver.Config, for example:

grails:
    neo4j:
        options:
            maxConnectionPoolSize: 100
            connectionLivenessCheckTimeout: 200

For builder methods with 0 arguments, setting the configuration value to true will cause the method to be executed. For example the following configuration will result in the ConfigBuilder#withEncryption method to be called.

grails:
    neo4j:
        options:
            encryption: true

For builder methods with more than 1 argument, it is possible to configure values for the method using arg# syntax. For example the following configuration will result in the ConfigBuilder#withConnectionTimeout method to be called.

grails:
    neo4j:
        options:
            connectionTimeout:
                arg0: 10
                arg1: SECONDS

Using Neo4j Standalone

If you plan to use Neo4j as your primary datastore then you need to remove the Hibernate plugin by editing your BuildConfig or build.gradle (dependending on the version of Grails) and removing the Hibernate plugin definition

With this done all domain classes in grails-app/domain will be persisted via Neo4j and not Hibernate. You can create a domain class by running the regular create-domain-class command:

grails create-domain-class Person

The Person domain class will automatically be a persistent entity that can be stored in Neo4j.

Combining Neo4j And Hibernate

If you have both the Hibernate and Neo4j plugins installed then by default all classes in the grails-app/domain directory will be persisted by Hibernate and not Neo4j. If you want to persist a particular domain class with Neo4j then you must use the mapWith property in the domain class:

static mapWith = "neo4j"

Using GORM in Spring Boot

To use GORM for Neo4j in Spring Boot add the necessary dependencies to your Boot application:

compile("org.grails:gorm-neo4j-spring-boot:7.1.1-SNAPSHOT")

Ensure your Boot Application class is annotated with ComponentScan, example:

import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.EnableAutoConfiguration
import org.springframework.context.annotation.*

@Configuration
@EnableAutoConfiguration
@ComponentScan
class Application {
    static void main(String[] args) {
        SpringApplication.run Application, args
    }
}
Using ComponentScan without a value results in Boot scanning for classes in the same package or any package nested within the Application class package. If your GORM entities are in a different package specify the package name as the value of the ComponentScan annotation.

Finally create your GORM entities and ensure they are annotated with grails.persistence.Entity:

import grails.gorm.annotation.*

@Entity
class Person {
    String firstName
    String lastName
}

To configure GORM for Neo4j within Spring Boot create an application.yml file and populate your configuration options within it.

GORM for Neo4j without Grails

If you wish to use GORM for Neo4j outside of a Grails application you should declare the necessary dependencies, for example in Gradle:

compile "org.grails:grails-datastore-gorm-neo4j:7.1.1-SNAPSHOT"

Then annotate your entities with the grails.gorm.annotation.Entity annotation and implement the Node trait:

import grails.neo4j.*
import grails.gorm.annotation.*

@Entity
class Person implements Node<Person>{
    String name
}

Then you need to place the bootstrap logic somewhere in your application which uses Neo4jDatastore:

def datastore = new Neo4jDatastore(Person)

println Person.count()

For configuration you can either pass a map or an instance of the org.springframework.core.env.PropertyResolver interface:

def datastore = new Neo4jDatastore(['grails.neo4j.url':'bolt://...'], Person)

println Person.count()

If you are using Spring with an existing ApplicationContext you can instead call use Neo4jDataStoreSpringInitializer and call configureForBeanDefinitionRegistry prior to refreshing the context. You can pass the Spring Environment object to the constructor for configuration:

ApplicationContext myApplicationContext = ...
def initializer = new Neo4jDataStoreSpringInitializer(myApplicationContext.getEnvironment(), Person)
initializer.configureForBeanDefinitionRegistry(myApplicationContext)

println Person.count()

Mapping Domain Classes to Nodes

GORM for Neo4j will map each Grails domain instance to a Node in the node space. For example given the following domain class:

class Pet {
    String name
}

Each domain class will implement the Neo4jEntity trait. You can define this explicitly if you prefer (the Node trait is a subtrait of Neo4jEntity):

import grails.neo4j.Node

class Pet implements Node<Pet> {
    String name
}

When an instance of Pet is saved:

def p = new Pet(name:"Dino")
p.save(flush:true)

Then the following Cypher CREATE is issued:

CREATE (n2:Pet {props})

The properties of the class are converted to native Neo4j types and set as the props parameter to the query.

If you want to see what queries GORM for Neo4j generates, enabling logging for the org.grails.datastore.gorm.neo4j package

Neo4j ID generators

GORM by default uses a native Neo4j node identifiers for each id property of a domain class. These have a couple of disadvantages that should encourage you to use an alternative id generation strategy:

  1. Neo4j node ids are more like internal identifiers used by Neo4j to identify nodes (equivalent to the rowid in a SQL database), but can be potentially re-used if data is deleted and recreated. This makes them not appropriate for use as a public facing key.

  2. In order to obtain a Node id GORM must perform an insert (similar to the way GORM for Hibernate has to perform a SQL INSERT if the id generation strategy is an auto-increment column), which means it cannot efficiently batch up Cypher CREATE statements.

This last disadvantage can be worked around by using the saveAll method to save multiple domain classes at once:

Club.saveAll([
    new Club(name:"Manchester United"),
    new Club(name:"Arsenal")
])

However, it is generally better to consider a different id generation strategy.

Assigned Identifiers

The recommended approach is to use assigned identifiers that relate to your domain. For example:

class Owner {
    String name
    static hasMany = [pets:Pet]
    static mapping = node {
        id generator:'assigned', name:'name'
    }
}
class Pet {
    String name
    static belongsTo = [owner:Owner]
    static mapping = node {
        id generator:'assigned', name:'name'
    }
}

With assigned identifiers GORM can do a much better job of optimizing write operations. For example the following logic:

new Owner(name:"Fred")
        .addToPets(name: "Dino")
        .addToPets(name: "Joe")
        .save()
new Owner(name:"Barney")
        .addToPets(name: "Hoppy")
        .save(flush:true)

GORM will generate the following Cypher:

UNWIND {ownerBatch} as row
CREATE (owner:Owner)
SET owner += row.props
FOREACH (pets IN row.pets |
    CREATE (pet:Pet)
    SET pet += pets.props
    MERGE (owner)-[:PETS]->(pet)
)

Notice how UNWIND and FOREACH are used to create both the entity and its associations in one go, significantly improving write performance for large data sets.

Snowflake Identifiers

Another option is a custom identity generator based on the Snowflake algorithm and stores the generated identifier in a property of each Neo4j node called __id__:

import static grails.neo4j.mapping.MappingBuilder.*
class Club {
    String name
    ...
    static mapping = node {
        id {
            generator "snowflake"
        }
    }

}

Globally Changing Identity Strategy

If you wish to globally change id generation then you can do so in grails-app/conf/application.groovy (Config.groovy in Grails 2.x):

grails.neo4j.default.mapping = {
    id generator:'assigned'
}

Custom Identifiers

In addition, if you wish to use a custom identity generation strategy you can do so by specifying a class name that implements the IdGenerator interface:

grails.neo4j.default.mapping = {
    id generator:'com.foo.MyIdGenerator'
}

Understanding Association Mapping

GORM for Neo4j will create Neo4j relationships between nodes for you based on the relationship ownership rules defined in your GORM mapping. For example the following mapping:

class Owner {
    String name
    static hasMany = [pets:Pet]
}
class Pet {
    String name
    static belongsTo = [owner:Owner]
}

When you save the relationship:

new Owner(name:"Fred")
    .addToPets(name: "Dino")
    .save(flush:true)
    .discard()

The save() method will generate a Cypher relationship creation query as follows:

MATCH (from:Owner),(to:Pet) WHERE ID(from) = {start} AND ID(to) IN {end} MERGE (from)-[r:PETS]->(to)

As you can see from the query the relationship is defined as (from)-[r:PETS]->(to), with the direction of the association defined by who "owns" the association. Since Pet defines a belongTo association to Owner, this means that Owner owns the association and the relationship is from Owner to Pet.

You can customize the Neo4j relationship type and direction using the mapping block if necessary:

import static grails.neo4j.Direction.*

class Owner {
    String name
    static hasMany = [pets:Pet]

    static mapping = {
         pets type:"PETZ", direction:BOTH
    }
}

In this case a bidirectional relationship will be created in the graph such as (from)<-[r:PETZ]->(to).

For more information on defining relationships with GORM, see the corresponding guide in the GORM documentation.

Customizing the Label Strategy

The default strategy for defining labels is to use the class name, however the strategy to define labels for a given node is completely configurable. For example you can use static mapping to define you labels:

class Person {
    static mapping = {
        labels "Person", "People"
    }
}

You can also define labels dynamically. For example:

class Person {
    static mapping = {
        labels { GraphPersistentEntity pe -> "`${pe.javaClass.name}`" }
    }
}
Dynamic labels have a negative impact on write performance as GORM is unable to batch operations with dynamic labels so should be used sparingly.

Or mix static and dynamic labels:

static mapping = {
    labels "People", { GraphPersistentEntity pe -> "`${pe.javaClass.name}`" }
}

At the cost of read/write performance you can define dynamic labels based on an instance:

static mapping = {
    labels { GraphPersistentEntity pe, instance ->  // 2 arguments: instance dependent label
        "`${instance.profession}`"
    }
}

Dynamic Properties and Associations

Neo4j is a schemaless database. This means that, unlike SQL where you can only have a fixed number of rows and columns, nodes can have unlimited properties.

Most existing object mapping tools in statically typed languages don’t allow you to harness this power, but GORM for Neo4j allows you to define both statically defined properties (ie the properties of the domain class) and domain properties.

For example, take the following domain class:

class Plant {
    String name
 }

You can set both the statically defined name property, but also any arbitrary number of dynamic properties using the subscript operator in Groovy:

def p = new Plant(name:"Carrot")
  p['goesInPatch'] = true
  p.save(flush:true)

Any simple properties can be included, however if you wish to have dynamic associations you can as well by modifying the mapping:

class Plant {
    String name
    static mapping = {
        dynamicAssociations true
    }
 }

With this in place you can define dynamic associations:

def p = new Plant(name:"Carrot")
  p['related'] = [ new Plant(name:"Potato").save() ]
  p.save(flush:true)
Dynamic associations have a runtime performance cost as when you access any dynamic property GORM has to issue a separate query to retrieve that association if the value is null, use with care.

Mapping Domain Classes to Relationships

In addition to being able to map a domain class to a Neo4j Node, since 6.1 you are able to map a domain class to a Neo4j Relationship.

For example consider the following domain model:

import grails.neo4j.*

class Movie {
    String title
    static hasMany = [cast:CastMember]
}

class CastMember implements Relationship<Person, Movie> {
    List<String> roles = []
}

class Person {
    String name
    static hasMany = [appearances:CastMember]
}

The CastMember class implements the Relationship trait which takes two generic arguments: The class that represents the start of the relationship and the class that represents the end.

You can then use regular GORM methods to query the CastMember relationship. In addition because Neo4j relationships are dynamic you can assign additional properties to them at runtime. For example:

def castMember = new CastMember(
    from: new Person(name: "Keanu"),
    to: new Movie(title: "The Matrix"),
    roles: ["Neo"])

castMember['realName'] = "Thomas Anderson"
castMember.save(flush:true)

In the above example the roles property is saved as a property of the relationship, as is the dynamic realName property.

You can then query relationships using these properties:

CastMember cm = CastMember.findByRoles(['Neo'])
Person person = cm.from
println person.name

The above example will produce a Cypher query that queries the relationship rather than the nodes:

MATCH (n:Person)-[from:CASTMEMBER]->(to:Movie) WHERE ( r.roles={1} ) RETURN r as rel, from as from, to as to
 ] for params [[1:[Neo]]

Controlling the Relationship Type

You will have noticed from the previous example that the relationship type is CASTMEMBER.

By default GORM uses the class name for the relationship type. You can configure an alternative default using the mapping block if necessary:

import grails.neo4j.*
import static grails.neo4j.mapping.MappingBuilder.*

static mapping = relationship {
   type "ACTED_IN"
   direction Direction.OUTGOING
}

The above example also demonstrates configuring the direction of the relationship.

You can also assign a per instance relationship type overriding the default:

def castMember = new CastMember(
    type: "DIRECTED"
    from: new Person(name: "Lana Wachowski"),
    to: new Movie(title: "The Matrix"))
    .save(flush:true)

And then later use the type in queries:

List<CastMember> directors = CastMember.findAllByType("DIRECTED")

Querying and Relationships

Generally all types of GORM queries work with relationships. There is some special handling for the from and to properties of a relationship to make it possible to query these and use projections.

When you query any other property of a relationship the relationship’s properties are queried. When using from and to however, the nodes that form both ends of the relationship are queried instead.

Consider this query:

List keanuCastings = CastMember.where {
    from.name == "Keanu"
}.list()

The above where query will produce the following cypher query:

MATCH (from:Person)-[r:ACTED_IN]->(to:Movie) WHERE ( ( from.name={1} ) ) RETURN r as rel, from as from, to

Notice that the from end of the relationship is queried. You can also apply projections to the to and from associations. For example:

DetachedCriteria<CastMember> baseQuery = CastMember.where {
    from.name == "Keanu"
} (1)
int totalKeanuMovies = baseQuery.projections {
    countDistinct("to.title")
}.get() (2)

List<String> keanuMovieTitles = baseQuery.property('to.title').list() (3)
1 Create a DetachedCriteria query
2 Use a projection to get the total count of movie titles
3 Use a project to get only the movie titles

Querying with GORM for Neo4j

GORM for Neo4j supports all the different querying methods provided by GORM including:

However, HQL queries are not supported, instead you can use Cypher directly if you so choose.

If you want to see what queries GORM for Neo4j generates, enabling logging for the org.grails.datastore.gorm.neo4j package

Understanding Lazy Loading

When retrieving a GORM entity and its associations by default single-ended associations will only retrieve the association id, whilst associations to many objects will not retrieve the association at all until it is accessed. This is called lazy loading.

For example given the following domain model:

class League {
    String name
    static hasMany = [clubs:Club]
}
class Club {
    String name
    static belongsTo = [league:League]
    static hasMany = [teams: Team ]
}
class Team  {
    String name
    static belongsTo = [club:Club]
}

When you retrieve the Club by name:

def club = Club.findByName("Manchester United")

You will get the following Cypher query:

MATCH (n:Club) WHERE ( ID(n)={1} ) RETURN n as data

As you can see the teams association is not loaded in the query and nor is the league association. Note that if you were to make the league association nullable:

class Club {
 ...
 static belongsTo = [league:League]
 static constraints = {
    league nullable:true
 }
}

This would alter the query executed to:

MATCH (n:Club) WHERE ( ID(n) = {1} )
OPTIONAL MATCH(n)-[:LEAGUE]->(leagueNode)
RETURN n as data, collect(DISTINCT ID(leagueNode)) as leagueIds

Note that only the ID of the league association is retrieved.

The reason the id of the associated entity is retrieved is to differentiate between whether an association exists or is null. You can restore the previous behaviour by making the league association lazy:

class Club {
 ...
 static belongsTo = [league:League]
 static constraints = {
    league nullable:true
 }
 static mapping = {
    league lazy:true
 }
}

However if you were to access the league association and it were null you would get an exception.

With all of these approaches, if you then iterate over the teams you will get a second query to obtain the teams:

for(team in club.teams) {
    println team.name
}

The query generated will be:

MATCH (from:Club)-[:TEAMS]->(to:Team) WHERE ID(from) = {id} RETURN to as data

If you wish to avoid this secondary query to retrieve the data you can do so using an eager query:

// using a dynamic finder
def club = Club.findByName("Manchester United", [fetch:[teams:'join']])

// using a where queries
def query = Club.where { name == "Manchester United" }
                .join('teams')
def club = query.find()

// using criteria
def query = Club.createCriteria()
def club = query.get {
    eq 'name', "Manchester United"
    join 'teams'
}

This will instead generate the following query:

MATCH (n:Club) WHERE ( n.name={1} )
OPTIONAL MATCH(n)-[:TEAMS]->(teamsNode) WITH n, collect(DISTINCT teamsNode) as teamsNodes
RETURN n as data, teamsNodes

As you can see the associated team nodes are loaded by the query. If you prefer this to happen for every query, then this can also be configured in the mapping:

class Club {
    ...

    static mapping = {
       teams fetch:"eager"
    }
}

You can also configure the collection ids to be eagerly loaded, but the instances themselves to be lazy loaded via proxies:

class Club {
    ...

    static mapping = {
       teams fetch:"eager", lazy:true
    }
}

Querying with Cypher

To query with raw Cypher queries you can use the built in find and findAll methods:

def club = Club.find("MATCH n where n.name = {1} RETURN n", 'FC Bayern Muenchen')
def clubs = Club.findAll("MATCH n where n.name = {1} RETURN n", 'FC Bayern Muenchen')

Note that the first returned item should be the node itself. To execute cypher queries and work with the raw results use executeCypher:

Result result = Club.executeCypher("MATCH n where n.name = {1} RETURN n", ['FC Bayern Muenchen'])

Or alternatively you can use executeQuery which will return a list of results:

List<Node> nodes = Club.executeQuery("MATCH n where n.name = {1} RETURN n", ['FC Bayern Muenchen'])

When working with raw results, you can convert any org.neo4j.driver.types.Node into a domain instance using the as keyword:

Node myNode = ...
 Club club = myNode as Club
This also works for Relationship and Path types

You can also convert any org.neo4j.driver.Result instance to a list of domain classes:

Result result = ...
List<Club> clubs = result.toList(Club)

Defining the Query Index

To define which properties of your domain class should be indexed for querying you can do so in the mapping:

class Club {
    String name

    ...
    static mapping = {
        name index:true
    }
}

On startup GORM will use Cypher to create indexes as follows:

CREATE INDEX ON :Club(name)

To define a unique index use unique instead:

class Club {
    String name

    ...
    static mapping = {
        name unique:true
    }
}

Querying for Paths

Since 6.1, GORM for Neo4j features support for Neo4j path queries. To use path queries you must implement the grails.neo4j.Node trait in your domain class. For example given the following class:

@EqualsAndHashCode(includes = 'name')
class Person implements Node<Person> {
    String name
    static hasMany = [friends: Person]

    static mapping = node {
        id(generator:'assigned', name:'name')
    }
}

And the following setup data:

Person joe = new Person(name: "Joe")
Person barney = new Person(name: "Barney")
                      .addToFriends(joe)
Person fred = new Person(name: "Fred")
                      .addToFriends(barney)

Which creates a friend graph it is possible to find out the shortest path from one friend to another with the findShortestPath method which returns a grails.neo4j.Path instance:

Path<Person, Person> path = Person.findShortestPath(fred, joe, 15)
for(Path.Segment<Person, Person> segment in path) {
    println segment.start().name
    println segment.end().name
}

You can also find the shortest path without first loading the entities using proxies:

Path<Person, Person> path = Person.findShortestPath(Person.proxy("Fred"), Person.proxy("Joe"), 15)
for(Path.Segment<Person, Person> segment in path) {
    println segment.start().name
    println segment.end().name
}

Finally it is also possible to use Cypher queries to find a path:

String from = "Fred"
String to = "Joe"
Path<Person, Person> path = Person.findPath("MATCH (from:Person),(to:Person), p = shortestPath((from)-[*..15]-(to)) WHERE from.name = $from AND to.name = $to RETURN p")
for(Path.Segment<Person, Person> segment in path) {
    println segment.start().name
    println segment.end().name
}

Querying for Relationships

In addition to being able to query for paths since 6.1, GORM for Neo4j features support for Neo4j relationship queries. To use path queries you must implement the grails.neo4j.Node trait in your domain class. For example given the following class:

@EqualsAndHashCode(includes = 'name')
class Person implements Node<Person> {
    String name
    static hasMany = [friends: Person]

    static mapping = node {
        id(generator:'assigned', name:'name')
    }
}

You can find a relationship between two entities with the following query:

String from = "Fred"
String to = "Barney"
Relationship<Person, Person> rel = Person.findRelationship(Person.proxy(from), Person.proxy(to))

The arguments can be retrieved instances or unloaded proxies to nodes.

Querying for relationships in this way is not as flexible as using relationship entities. Consider mapping an entity to a relationship.

You can also find all the relationships between two entities:

String from = "Fred"
String to = "Barney"
List<Relationship<Person, Person>> rels = Person.findRelationships(Person.proxy(from), Person.proxy(to))

Or find all the relationships between two entity types:

List<Relationship<Person, Person>> rels = Person.findRelationships(Person, Person, [max:10])
for(rel in rels) {
    println("Type ${rel.type}")
    println("From ${rel.from.name} to ${rel.to.name}")
}

GORM for Neo4j Data Services

Additional support for GORM Data Services exists in GORM for Neo4j beyond what is already offered by GORM core.

For example if you declare a data service interface:

@Service(Person)
interface PersonService {
}

You can use the @Cypher annotation to automatically implement methods that execute Cypher queries:

@Cypher("""MATCH ${Person from},${Person to}, p = shortestPath(($from)-[*..15]-($to))
           WHERE $from.name = $start AND $to.name = $end
           RETURN p""")
Path<Person, Person> findPath(String start, String end)

Notice how you can use the class names within the Cypher query and it will correctly translate the declaration into the appropriate node MATCH statement.

Variable references within the @Cypher string declaration are also compile time checked.

You can also automatically implement Cypher update operations:

@Cypher("""MATCH ${Person p}
           WHERE $p.name = $name
           SET p.age = $age""")
void updatePerson(String name, int age)

Finally, support for implementing methods that find paths is also possible. For example:

Path<Person, Person> findPath(Person from, Person to)

Testing

It is relatively trivial to write unit tests that use GORM for Neo4j. If you wish to use an embedded version of Neo4j simply add the neo4j-harness dependency to your build.gradle file:

build.gradle
testRuntime "org.neo4j.test:neo4j-harness:3.5.30"

Then create a Spock specification and declare a @Shared field using the Neo4jDatastore constructor with the domain classes you wish to test:

@Shared @AutoCleanup Neo4jDatastore datastore = new Neo4jDatastore(getClass().getPackage())
The @AutoCleanup annotation will ensure the datastore is shutdown correctly after the test completes

If the test is in the same package as your domain classes then you can also setup package scanning instead of hard coding each domain class name:

@Shared @AutoCleanup Neo4jDatastore datastore
        = new Neo4jDatastore(getClass().getPackage())

Then annotate each test method with the grails.gorm.transactions.Rollback annotation:

@Rollback
void "test something"() {

}

Multiple Data Sources

GORM for Neo4j supports the notion of multiple data sources where multiple individual Bolt Driver instances can be configured and switched between.

Configuring Multiple Bolt Drivers

To configure multiple Bolt Driver connections you need to use the grails.neo4j.connections setting. For example in application.yml:

grails-app/conf/application.yml
grails:
    neo4j:
        url: bolt://localhost:7687
        connections:
            moreBooks:
                url: bolt://localhost:7688
            evenMoreBooks:
                url: bolt://localhost:7689

You can configure individual settings for each Bolt driver. If a setting is not specified by default the setting is inherited from the default Neo4j driver.

Mapping Domain Classes to Bolt Drivers

If a domain class has no specified Neo4j driver connection configuration then the default is used.

You can set the connection method in the mapping block to configure an alternate Neo4j Driver.

For example, if you want to use the ZipCode domain to use a Neo4j Driver connection called 'lookup', configure it like this:

class ZipCode {

   String code

   static mapping = {
      connection 'lookup'
   }
}

A domain class can also use two or more configured Neo4j Driver connections by using the connections method with a list of names to configure more than one, for example:

class ZipCode {

   String code

   static mapping = {
      connections(['lookup', 'auditing'])
   }
}

If a domain class uses the default connection and one or more others, you can use the ConnectionSource.DEFAULT constant to indicate that:

import org.grails.datastore.mapping.core.connections.*

class ZipCode {

   String code

   static mapping = {
      connections(['lookup', ConnectionSource.DEFAULT])
   }
}

If a domain class uses all configured DataSource instances use the value ALL:

import org.grails.datastore.mapping.core.connections.*

class ZipCode {

   String code

   static mapping = {
      connection ConnectionSource.ALL
   }
}

Switching between Bolt Drivers

You can switch to a different connection at runtime with the withConnection method:

Book.withConnection("moreBooks") {
    list()
}
The call to list() should not be prefixed with Book. as this will use the default connection

Any logic executed within the body of the closure will use the alternate connection. Once the close finishes execution GORM will switch back to the default connection automatically.

Multi-Tenancy

GORM for Neo4j supports the following multi-tenancy modes:

  • DATABASE - A separate database with a separate connection pool is used to store each tenants data.

  • DISCRIMINATOR - The same database is used with a discriminator used to partition and isolate data.

Configuring Multi Tenancy

You can configure Multi-Tenancy the same way described in the GORM for Hibernate documenation, simply specify a multi tenancy mode and resolver:

grails:
    gorm:
        multiTenancy:
            mode: DATABASE
            tenantResolverClass: org.grails.datastore.mapping.multitenancy.web.SubDomainTenantResolver

Note that if you are using Neo4j and Hibernate together the above configuration will configure both Neo4j and Hibernate to use a multi-tenancy mode of DATABASE.

If you only want to enable multi-tenancy for Neo4j only you can use the following configuration instead:

grails:
    neo4j:
        multiTenancy:
            mode: DATABASE
            tenantResolverClass: org.grails.datastore.mapping.multitenancy.web.SubDomainTenantResolver

Multi Tenancy Modes

As mentioned previously, GORM for Neo4j supports the DATABASE and DISCRIMINATOR however there are some considerations to keep in mind.

Database Per Tenant

When using the DATABASE mode, only GORM methods calls are dispatched to the correct tenant. This means the following will use the tenant id:

// switches to the correct client based on the tenant id
Book.list()

However, going directly through the Driver will not work:

@Autowired Driver boltDriver

// uses the default connection and doesn't resolve the tenant it
boltDriver.session().run("..")

If you are working directly with the Driver instance you need to make sure you obtain the correct instance. For example:

import grails.gorm.multitenancy.*

@Autowired Neo4jDatastore neo4jDatastore
...
Driver boltDriver =
        neo4jDatastore.getDatastoreForTenantId(Tenants.currentId())
                      .getBoltDriver()

Partitioned Multi-Tenancy

When using the DISCRIMINATOR approach, GORM for Neo4j will store a tenantId attribute in each Neo4j node and attempt to partition the data.

Once again this works only when using GORM methods and even then there are cases where it will not work if you use native Neo4j interfaces.

For example the following works fine:

// correctly includes the `tenantId` in the query
Book.list()

As does this:

// works automatically if you include the tenantId in the query
Book.find("MATCH (p:Book) WHERE p.title={title} AND p.publisher={tenantId} RETURN p", [name:"The Stand"]")

But this query with throw an exception due to a missing tenant id:

Book.find("MATCH (p:Book) WHERE p.title={title} RETURN p", [name:"The Stand"])

Also if you obtain the driver directly the the tenant id will not be included in the query:

@Autowired Driver boltDriver

// uses the default connection and doesn't resolve the tenant it
boltDriver.session()
          .run("MATCH (p:Book) WHERE p.title={title} RETURN p", [name:"The Stand"]")

Since you are operating directly on the Driver cannot know when you perform a query that should be multi-tenant aware.

In this case you will have to ensure to include the tenantId manually:

boltDriver.session()
          .run("MATCH (p:Book) WHERE p.title={title} AND p.publisher={tenantId} RETURN p",
                [name:"The Stand", tenantId: Tenants.currentId() ])

And the same is true of write operations such as inserts that are done with the native API.

Enhancements to Neo4j Java Driver API

GORM for Neo4j contains some enhancements to the Neo4j Bolt Driver API.

Getting Properties from Nodes

The dot operator can be used to obtain properties from a node:

Node node = ...
def value = node.myProperty

// or

def value = node['myProperty']

Cast a Node to a Map

Nodes can be cast to maps:

Node node = ...
Map myMap = node as Map

Reference

Additional Gorm Methods

cypher

Purpose

Execute a cypher query.

Example
setup:
def person = new Person(firstName: "Bob", lastName: "Builder")
def petType = new PetType(name: "snake")
def pet = new Pet(name: "Fred", type: petType, owner: person)
person.addToPets(pet)
person.save(flush: true)
session.clear()

when:
def result = person.cypher("start n=node({this}) match n-[:pets]->m return m")

then:
result.iterator().size() == 1
Description

cypher is invoked on any domain instance and returns a org.neo4j.driver.Result

The parameters passed are:

  • cypher query string. The query string might use a implicit this parameter pointing to the instance’s node

  • a optional map of cypher parameters

cypherStatic

Purpose

Execute a cypher query.

Example
setup:
new Person(lastName:'person1').save()
new Person(lastName:'person2').save()
session.flush()
session.clear()

when:
def result = Person.cypherStatic("start n=node({this}) match n-[:INSTANCE]->m where m.lastName='person1' return m")

then:
result.iterator().size()==1
Description

cypherStatic is invoked on any domain class and returns a org.neo4j.driver.Result

The parameters passed are:

  • cypher query string. The query string might use a implicit this parameter pointing to the domain class’s (aka subreference) node

  • a optional map of cypher parameters

Schemaless Attributes

Purpose

For domain classes mapped by Neo4j you can put and get arbitrary attributes on a instances by using the dot operator or map semantics on the domain instance.

Example

A simple domain class:

class Person implements Serializable {
    String firstName
    String lastName
    Integer age = 0
}
Using Map Semantics
when:
def person = new Person(lastName:'person1').save()
person['notDeclaredProperty'] = 'someValue'   // n.b. the 'dot' notation is not valid for undeclared properties
person['emptyArray'] = []
person['someIntArray'] = [1,2,3]
person['someStringArray'] = ['a', 'b', 'c']
person['someDoubleArray'] = [0.9, 1.0, 1.1]
session.flush()
session.clear()
person = Person.get(person.id)

then:
person['notDeclaredProperty'] == 'someValue'
person['lastName'] == 'person1'  // declared properties are also available via map semantics
person['someIntArray'] == [1,2,3]
person['someStringArray'] == ['a', 'b', 'c']
person['someDoubleArray'] == [0.9, 1.0, 1.1]
Using Dot Operator
when:
def person = new Person(lastName:'person1').save(flush:true)
session.clear()
person = Person.load(person.id)
person.notDeclaredProperty = 'someValue'   // n.b. the 'dot' notation is not valid for undeclared properties
person.emptyArray = []
person.someIntArray = [1,2,3]
person.someStringArray = ['a', 'b', 'c']
person.someDoubleArray= [0.9, 1.0, 1.1]
session.flush()
session.clear()
person = Person.get(person.id)

then:
person.notDeclaredProperty == 'someValue'
person.lastName == 'person1'  // declared properties are also available via map semantics
person.someIntArray == [1,2,3]
person.someStringArray == ['a', 'b', 'c']
person.emptyArray == []
person.someDoubleArray == [0.9, 1.0, 1.1]
Description

The non declared attribtes are stored a regular properties on the domain instance’s node. The values of the schemaless attributes must be a valid type for Neo4j property (String, primitives and arrays of the former two).