Show Navigation

Building a Graph application with Grails and Neo4j

This guide with demonstrate how to build the Neo4j Movies example application with Grails and GORM

Authors: Graeme Rocher

Grails Version: 3.3.0

1 Grails Training

Grails Training - Developed and delivered by the folks who created and actively maintain the Grails framework!.

2 Getting Started

In this guide you are going to learn how to build the Neo4j Movies example application, which is implemented in a variety of languages as the official example application for using Neo4j.

For those unfamiliar with Neo4j, it is a Graph database that optimizes Graph traversal, something that is relatively slow on a traditional relational database.

The goal of GORM for Neo4j is to make it easy to map an existing Groovy domain model with Neo4j nodes and relationships.

2.1 What you will need

To complete this guide, you will need the following:

  • Some time on your hands

  • A decent text editor or IDE

  • JDK 1.7 or greater installed with JAVA_HOME configured appropriately

You will also need to Download and Install the "Community Edition" of Neo4j.

For the Mac, Neo4j comes as a DMG containing the Neo4j server and can simply be installed directly into your Applications folder:

neo4j application
For Windows the executable installer will install the Neo4j server and make it available from the start menu. For *nix operating systems, you can use the Unix console application.

Once the Neo4j server application is running specify a data directory and click the "Start" button:

neo4j start

This will start Neo4j on the default port which is 7474, you can now access the Neo4j administrative UI using the link provided by the application:

neo4j localhost

Now you can login to Neo4j with the default username and password, which are neo4j and neo4j:

neo4j password

Once you are prompted to login you will be asked to change the password. Change the value to "movies" for this tutorial:

neo4j setpassword

2.2 How to complete the guide

To get started do the following:

or

The Grails guides repositories contain two folders:

  • initial Initial project. Often a simple Grails app with some additional code to give you a head-start.

  • complete A completed example. It is the result of working through the steps presented by the guide and applying those changes to the initial folder.

To complete the guide, go to the initial folder

  • cd into grails-guides/neo4j-movies/initial

and follow the instructions in the next sections.

You can go right to the completed example if you cd into grails-guides/neo4j-movies/complete

3 Populating the Data Model

The first thing to do before we can get going is populate the data model. To do this head over to the Neo4j browser at http://localhost:7474/browser/ and type :play movies:

neo4j playmovies

This will bring up a preview of the domain model that represents the movies database represented by a series of Cypher statements CREATE statements.

Cypher is the query language used by Neo4j. The following statement for example creates two new Neo4j nodes in the graph (one that represents a Movie and another that represents a Person):

CREATE (TheMatrix:Movie {title:'The Matrix', released:1999, tagline:'Welcome to the Real World'})
CREATE (Keanu:Person {name:'Keanu Reeves', born:1964})

The value of the left side of the expression TheMatrix:Movie is the name of the node, like a variable name and is not stored in the database.

The value of the right side is the Node label (in this case Movie).

The values within the curly brackets are the node attributes.

The names of the nodes can be used in later statements to create relationships between the nodes:

CREATE (Keanu)-[:ACTED_IN {roles:['Neo']}]->(TheMatrix)

The above CREATE statement creates a relationship between the Keanu node and the TheMatrix node. The relationship type is ACTED_IN and like nodes relationships can have attributes (in this case a roles attribute).

Click on the code to populate it into Neo4j browser editor:

neo4j clickcode

This will place the necessary Cypher statements into the Neo4j browser editor that will populate the example database. You can then press the "Play" button to run the code and in the window below will appear a Graph visualization of the database model:

neo4j runcode

4 Configure GORM for Neo4j

Firstly, this guide is built with GORM 6.1.6.RELEASE, so in order to complete the guide we must first configure Grails to use GORM 6.1.6.RELEASE.

To do so set the gormVersion property in gradle.properties to 6.1.6.RELEASE:

gradle.properties
grailsVersion=3.3.0
gormVersion=6.1.6.RELEASE
neo4jVersion=3.1.2
gradleWrapperVersion=3.5

build.gradle already contains a dependency to neo4j plugin. You will need to modify the neo4j dependency to also use GORM 6.1.6.RELEASE:

build.gradle
    compile "org.grails.plugins:neo4j"

Now modify the already existing neo4j configuration block within the grails-app/conf/application.yml file. Configure the Neo4j connection settings to use the "movies" password you set earlier:

grails-app/conf/application.yml
grails:
    neo4j:
        url: bolt://localhost
        username: "neo4j"
        password: "movies"

5 Writing the Application

As mentioned previously in this guide you are going to implement the Neo4j Movies example application. The Neo4j website describes the example as follows:

It is a simple, one-page webapp, that uses Neo4j’s movie demo database (movie, actor, director) as data set. The same front-end web page in all applications consumes 3 REST endpoints provided by backend implemented in the different programming languages and drivers.
— Neo4j.com

The REST endpoints to be implemented are:

  • single movie listing by title

  • movie search by title

  • graph visualization of the domain

5.1 Define the GORM Domain Model

Now that you have configured GORM correctly, the next step is to define a domain model that maps the Neo4j graph.

To do so we are going to create domain classes to represent the two types of nodes in the domain model. You can use the create-domain-class command of the CLI to do this, or your favourite IDE or text editor:

$ grails create-domain-class neo4j.movies.Person
$ grails create-domain-class neo4j.movies.Movie

In addition, we will create a domain class to model the relationship between the two domain classes called CastMember:

$ grails create-domain-class neo4j.movies.CastMember

Now open up the Person domain class located at grails-app/domain/neo4j/movies/Person.groovy and modify the contents to look like the following:

grails-app/domain/neo4j/movies/Person.groovy
package neo4j.movies

import grails.compiler.GrailsCompileStatic

/**
 * Models a Person node in the graph database
 */
@GrailsCompileStatic
class Person {
    String name
    int born

    static hasMany = [appearances: CastMember]

    static constraints = {
        name blank:false
        born min:1900
    }
}

As you can see a Person has a name, a year of birth and an association to CastMember (more on that later).

Now open up the Movie domain class located at grails-app/domain/neo4j/movies/Movie.groovy and modify the contents to look like the following:

grails-app/domain/neo4j/movies/Movie.groovy
package neo4j.movies

import grails.compiler.GrailsCompileStatic

/**
 * Models a movie node in the graph database
 */
@GrailsCompileStatic
class Movie {
    String title
    String tagline
    int released

    static hasMany = [cast: CastMember]

    static constraints = {
        released min:1900
        title blank:false
    }
}

A Movie has a title, a tagline, release year and also features an association to CastMember.

The CastMember domain class is going to model the relationship between a Person and a Movie. To do that we are going to use the grails.neo4j.Relationship trait, which allows us to map a domain class to a Neo4j relationship instead of a node:

grails-app/domain/neo4j/movies/CastMember.groovy
package neo4j.movies

import grails.neo4j.Relationship
import groovy.transform.CompileStatic

/**
 * Models a relationship between a Person and a Movie
 */
@CompileStatic
class CastMember implements Relationship<Person, Movie> { (1)

    List<String> roles = [] (2)

    CastMember() {
        type = RoleType.ACTED_IN (3)
    }

    static enum RoleType { (4)
        ACTED_IN, DIRECTED
    }
}
1 The Relationship trait takes 2 generic arguments. The from entity and the to entity.
2 Relationship entities can have attributes too, in this case a roles attribute
3 We set the default relationship type to ACTED_IN
4 The relationship types are represented by a RoleType enum

5.2 Implementing the REST Endpoints

As mentioned previously, the REST endpoints to be implemented are:

  • single movie listing by title

  • movie search by title

  • graph visualization of the domain

To implement these various REST endpoints first create a controller called MovieController. You can do so with the grails CLI or via your favourite text editor or IDE by creating a class within the grails-app/controllers/neo4j/movies directory whose name ends with Controller:

$ grails create-controller neo4j.movies.MovieController

The initial contents of the controller should look like the following:

grails-app/controllers/neo4j/movies/MovieController.groovy
@CompileStatic
class MovieController {
    static responseFormats = ['json', 'xml']
   ...
}

To map the various endpoints to the controller add the following to the grails-app/controllers/neo4j/movies/UrlMappings.groovy file:

grails-app/controllers/neo4j/movies/UrlMappings.groovy
        "/movie/$title"(controller: 'movie', action: 'show') (1)
        '/search'(controller: 'movie', action: 'search') (2)
        '/graph'(controller: 'movie', action: 'graph') (3)
1 Maps the /movie/{title} URI to an action called show of MovieController
2 Maps the /search URI to an action called search of MovieController
3 Maps the /graph URI to an action called graph of MovieController

Of course, none of these controller actions are implemented yet. Let’s start with the first requirement.

5.2.1 The Find By Title Endpoint

The implement finding a Movie by title we’re first going to create a GORM Data Service called MovieService to encapsulate the data access logic and interaction with Neo4j.

You can do so with the grails CLI or via your favourite text editor or IDE by creating a class within the grails-app/services/neo4j/movies directory whose name ends with Service:

$ grails create-service neo4j.movies.MovieService

Make the service abstract and add the @Service annotation to it to tell GORM that the service should be implemented automatically:

grails-app/services/neo4j/movies/MovieService.groovy
import grails.gorm.services.Service
import groovy.transform.CompileDynamic
import groovy.transform.CompileStatic

@SuppressWarnings(['UnusedVariable', 'SpaceAfterOpeningBrace', 'SpaceBeforeClosingBrace'])
@CompileStatic
@Service(Movie)
abstract class MovieService {
   ...
}

Now add an abstract method called find to the MovieService that takes the title as an argument:

grails-app/services/neo4j/movies/MovieService.groovy
@Join('cast')
abstract Movie find(String title)

The method will be automatically implemented for you by GORM, but note that we use the @Join annotation to indicate we want to fetch the cast association using the same query.

Now let’s implement the controller action that will invoke this method. First inject the service into the MovieController class:

grails-app/controllers/neo4j/movies/MovieController.groovy
MovieService movieService

This uses Spring to inject the dependency and make the service implementation available. Next add an action called show that invokes the find method and responds with the result:

grails-app/controllers/neo4j/movies/MovieController.groovy
def show(String title) {
    respond movieService.find(title)
}

If you now run the application and go to http://localhost:8080/movie/The%20Matrix%20Reloaded you will see a response as follows (shortened for brevity):

{
  "cast": [...],
  "id": 9,
  "released": 2003,
  "tagline": "Free your mind",
  "title": "The Matrix Reloaded"
}
If you wish to debug the Cypher query GORM executed then enable debug logging for the org.grails.datastore.gorm.neo4j package in grails-app/conf/logback.groovy

Although valid JSON, unfortunately this is not the exact format required to implement the Neo4j example application.

To customize the JSON create a grails-app/views/movie/_movie.gson JSON View and populate it with the following contents:

grails-app/views/movie/_movie.gson
import groovy.transform.Field
import neo4j.movies.Movie

@Field Movie movie (1)

json {
    title movie.title (2)
    cast tmpl.cast( 'castMember', movie.cast ) (3)
}
1 Define the movie to be rendered in the model
2 Output the Movie title as JSON
3 Render another template for each member of the cast

The call to tmpl.cast(..) requires the definition of a second template. The template namespace uses the method name to invoke a template of the same of name. So in this case we need to create a grails-app/views/movies/_cast.gson template.

grails-app/views/movie/_cast.gson
import groovy.transform.Field
import neo4j.movies.CastMember

@Field CastMember castMember

json {
    job castMember.type.split("_")[0].toLowerCase()
    name castMember.from.name
    role castMember.roles
}

The _cast.gson template formats the CastMember relationship as JSON in the required format. Now if you run the application and visit the same URL as previously described, the resulting output is in the correct JSON format (shortened for brevity):

{
  "title": "The Matrix Reloaded",
  "cast": [
    {
      "job": "acted",
      "name": "Carrie-Anne Moss",
      "role": [
        "Trinity"
      ]
    },
    {
      "job": "acted",
      "name": "Keanu Reeves",
      "role": [
        "Neo"
      ]
    },
    ...
  ]
}

With the first endpoint done, let’s implement search!

5.2.2 The Search Endpoint

The search endpoint allows clients to search for movies by title, without knowing the exact title. To implement the persistence logic add a new method to MovieService that implements the search logic:

grails-app/services/neo4j/movies/MovieService.groovy
List<Movie> search(String q, int limit = 100) { (1)
    List<Movie> results
    if (q) {
        results = Movie.where {
            title ==~ "%${q}%"  (2)
        }.list(max:limit)
    }
    else {
        results = [] (3)
    }
    results
}
1 The search method takes a query parameter and a limit parameter for maximum results
2 A Where Query is used in combination with a like expression which GORM translates into a Cypher CONTAINS query
3 If no query is specified an empty list is returned

When the query is executed the following Cypher is produced:

MATCH (n:Movie) WHERE ( n.title CONTAINS {1} ) RETURN n as data LIMIT {2}

The {1} and {2} are named arguments which are populated by parameters, ensuring correct escaping and preventing injection attacks.

This example also demonstrates an important concept with GORM Data Services, in that you can mix abstract methods that are implemented automatically by GORM with custom logic.

Now let’s implement the controller action that will invoke this method:

grails-app/controllers/neo4j/movies/MovieController.groovy
def search(String q) {
    respond movieService.search(q)
}

Finally, the search endpoint returns the result in a different JSON format to the show endpoint. So we create a grails-app/views/movie/search.gson view to format the JSON result:

grails-app/views/movie/search.gson
import groovy.transform.Field
import neo4j.movies.Movie

@Field Iterable<Movie> movieList = []

json(movieList) { Movie movie ->
        released movie.released
        tagline movie.tagline
        title movie.title
}

If you now go to the http://localhost:8080/search?q=Matrix URL the resulting JSON will look like:

[
  {
    "released": 1999,
    "tagline": "Welcome to the Real World",
    "title": "The Matrix"
  },
  {
    "released": 2003,
    "tagline": "Free your mind",
    "title": "The Matrix Reloaded"
  },
  {
    "released": 2003,
    "tagline": "Everything that has a beginning has an end",
    "title": "The Matrix Revolutions"
  }
]

5.2.3 The D3 Graph Endpoint

The final endpoint to implement is the graph endpoint. This endpoint outputs data about the graph in a JSON format that can be interpreted by the D3 JavaScript Library used by the example application’s UI.

The first step is to write a query for the data that we need. The nice thing about GORM for Neo4j is it has powerful integration with the Cypher query language.

To execute a Cypher query simply define an abstract method called findMovieTitlesAndCast that using the @Cypher annotation:

grails-app/services/neo4j/movies/MovieService.groovy
@Cypher("""MATCH ${Movie m}<-[:ACTED_IN]-${Person p}
           RETURN ${m.title} as movie, collect(${p.name}) as cast
           LIMIT $limit""")
protected abstract List<Map<String, Iterable<String>>> findMovieTitlesAndCast(int limit)

Using the @Cypher annotation GORM can automatically implement methods that execute Cypher queries for you. Notice how you can use class names and reference properties within the body of the query and these will be type checked ensuring the query is valid.

The next step is to convert the result of this query into a format expected by D3:

grails-app/services/neo4j/movies/MovieService.groovy
@ReadOnly
Map<String, Object> graph(int limit = 100) {
    toD3Format(findMovieTitlesAndCast(limit))
}

@SuppressWarnings('NestedForLoop')
@CompileDynamic
private static Map<String, Object> toD3Format(List<Map<String, Iterable<String>>> result) {
    List<Map<String,String>> nodes = []
    List<Map<String,Object>> rels= []
    int i = 0
    for (entry in result) {
        nodes << [title: entry.movie, label: 'movie']
        int target=i
        i++
        for (String name : (Iterable<String>) entry.cast) {
            def actor = [title: name, label: 'actor']
            int source = nodes.indexOf(actor)
            if (source == -1) {
                nodes << actor
                source = i++
            }
            rels << [source: source, target: target]
        }
    }
    [nodes: nodes, links: rels]
}

The graph method takes the Neo4j results and converts it to an appropriate format. Finally we can add a method to MovieController to return the required data:

grails-app/controllers/neo4j/movies/MovieController.groovy
def graph() {
    respond movieService.graph(params.int('limit', 100))
}

5.3 Add the Neo4j Movies UI

The final piece of the puzzle is to include the official Neo4j example application UI which is a simple HTML page.

The index.html page can be found in the complete/src/main/webapp directory of this tutorial, simply copy it into your application’s src/main/webapp directory.

Add the grails resources configuration pattern below to the grails-app/conf/application.yml file:

grails-app/conf/application.yml
grails:
    resources:
        pattern: '/**'

Modify grails-app/controllers/neo4j/movies/UrlMappings.groovy to map the root of the application to this index.html file.

grails-app/controllers/neo4j/movies/UrlMappings.groovy
'/'(uri: '/index.html')

With that done you are ready to run the application!

6 Running the Application

Before starting the application ensure the Neo4j server is running and that you have populated the Movies data as per the instructions in the Getting Started section.

To run the application use the ./gradlew bootRun command which will start the application on port 8080.

With the application running head over to http://localhost:8080 and you should see the UI of the application rendered correctly with a visualization of the graph:

neo4j ui