Steven Haines
Contributor

Big data analytics with Neo4j and Java, Part 2

how-to
May 3, 201816 mins

Write a Java application that connects to Neo4j and executes Cypher queries

JavaWorld - JW - big data analytics - social graph
Credit: Rafiq Phillips

The first part of this article introduced Neo4j and its Cypher Query Language. If you’ve read Part 1, you’ve seen for yourself why Neo4j and other graph databases are especially popular for social graphing, or modeling relationships between users in a network. You also got Neo4j setup in your development environment, and you got an overview of the basic concepts of working with this data store–namely nodes and relationships.

We then used the Cypher Query Language to model a family in Neo4j, including personal attributes like age, gender, and the relationships between family members. We created some friends to broaden our social graph, then added key/value pairs to generate a list of movies that each user had seen. Finally, we queried our data, using graph analytics to search for a movie that one user had not seen but might enjoy.

Cypher Query Language is different from traditional data query languages like SQL. Rather than thinking about things like tables and foreign key relationships, Cypher forces you to think about nodes, natural relationships between nodes, and the various traversals that can be made between nodes across their relationships. Using Cypher, you create your own mental model about how real-world entities relate to one another. It takes some practice to get good at writing Cypher queries, but once you understand how they work, even very complicated queries will make sense.

Once you’ve modeled a social graph in Neo4j and written queries against that social graph using the Cypher Query Language, writing the Java code to execute queries to that graph is pretty easy. In this article you’ll learn how to integrate Neo4j with a Java web client application, which you can use to query the social graph we created in Part 1.

Setup your Neo4j project

Our first step is to create a new Maven project:


mvn archetype:generate -DgroupId=com.geekcap.javaworld -DartifactId=neo4j-example -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false

Open your pom.xml file and add the Neo4j driver, which at the time of this writing is version 1.4.1:


        <dependency>
            <groupId>org.neo4j.driver</groupId>
            <artifactId>neo4j-java-driver</artifactId>
            <version>1.4.1</version>
        </dependency>    

Create a Neo4j driver

Next, create a Neo4j Driver, as follows:


Driver driver = GraphDatabase.driver( "bolt://localhost:7687", AuthTokens.basic("neo4j", "neo4j"));

The GraphDatabase class has a static method called driver() that accepts a URL to Neo4j and an AuthToken. You can create a basic AuthToken using the default username and password of “neo4j”.

The Driver facilitates communication with Neo4j. We execute queries by asking the Driver to create a Session object, as follows:


Session session = driver.session();

The Session interface

The org.neo4j.driver.v1.Session interface executes transactions against Neo4j. In its simplest form, we can execute the run() method that Session inherits from org.neo4j.driver.v1.StatementRunner. The Session will then begin a transaction, run our statement, and commit that transaction.

The StatementRunner interface defines a few variations of the run() method. Here’s the one we’ll use:


StatementResult run(String statementTemplate, Map<String,Object> statementParameters)

Statement parameters

The statementTemplate is a String that contains our Cypher query, but also includes named parameters that we’ll resolve using statementParameters. For example, we might want to create a new Person with a specified name and age:


session.run("CREATE (person: Person {name: {name}, age: {age}})",
parameters("name", person.getName(), "age", person.getAge()));

{name} and {age} are named parameters that can be resolved to values by passing in a Map of Strings. Each String contains the name of a property and must match what is in the String template to values. The values are Objects that the Session will properly format in the Cypher query. The parameters method is typically statically imported from the Values object:


import static org.neo4j.driver.v1.Values.parameters

Managing transactions

After a Session has completed, you are required to close it by invoking the close() method. As a convenience, the Session object implements the java.lang.AutoCloseable interface, so starting in Java 7 you can execute it in a try-with-resources statement, such as:


try (Session session = driver.session()) {
    session.run("CREATE (person: Person {name: {name}, age: {age}})",
    parameters("name", person.getName(), "age", person.getAge()));
}

Finally, if you are executing multiple statements that you want to constrain to a single transaction, you are free to bypass the Session‘s run() method’s automatic transaction management and explicitly manage a transaction yourself. For example:


try (Session session = driver.session()) {
    try (Transaction tx = session.beginTransaction()) {
        tx.run("CREATE (person: Person {name: {name}, age: {age}})",
                parameters("name", person.getName(), "age", person.getAge()));
        tx.success();
    }
}

The call to Session.beginTransaction() returns a Transaction object that can be used to run Cypher statements. After executing the Cypher statements, you must call tx.success() or the try-with-resources statement will roll back the transaction. The Transaction interface is AutoCloseable. If the transaction is marked as successful (by calling success()) then the transaction is committed; otherwise the transaction is rolled back. You can explicitly fail a transaction by invoking the Transaction‘s failure() method.

Record objects

You may have observed that the run() method in both the Session and Transaction classes returns a StatementResult instance. The StatementResult interface provides access to a list of Record objects and each Record object can have one or more Value objects.

Similar to retrieving values from a JDBC ResultSet, a Record allows you to retrieve values either by index or by name. The Value object that is returned can be converted to a Neo4j Node by calling its asNode() method or to a primitive, such as a String or an integer, by invoking one of its other asXXX() methods. Examples in the previous sections mostly returned nodes, but the last example returned a person’s name as a String. This is why the Value object affords flexibility in its return types.

download
Get the source code for the example application used in this article. Created by Steven Haines for JavaWorld.

Example application in Java

Now we’ll take what we’ve learned so far and put together an example application in Java. Building on our modeling and querying example in Part 1, this application creates Person objects, finds all Person objects, finds all friends of a Person, and finds all movies that a Person has seen.

Listing 1 and Listing 2 create Java classes defining a Person and a Movie. Listing 3 shows the source code for our test class: Neo4jClient.

Listing 1. Person.java


package com.geekcap.javaworld.neo4j.model;

public class Person {
    private String name;
    private int age;

    public Person() {
    }

    public Person(String name) {
        this.name = name;
    }

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

Listing 2. Movie.java


package com.geekcap.javaworld.neo4j.model;

public class Movie {
    private String title;
    private int rating;

    public Movie() {
    }

    public Movie(String title) {
        this.title = title;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public int getRating() {
        return rating;
    }

    public void setRating(int rating) {
        this.rating = rating;
    }
}

Listing 3. Neo4jClient.java


package com.geekcap.javaworld.neo4j;

import com.geekcap.javaworld.neo4j.model.Movie;
import com.geekcap.javaworld.neo4j.model.Person;
import org.neo4j.driver.v1.*;
import org.neo4j.driver.v1.types.Node;

import java.util.HashSet;
import java.util.Set;

import static org.neo4j.driver.v1.Values.parameters;

public class Neo4jClient {

    /**
     * Neo4j Driver, used to create a session that can execute Cypher queries
     */
    private Driver driver;

    /**
     * Create a new Neo4jClient. Initializes the Neo4j Driver.
     */
    public Neo4jClient() {
        // Create the Neo4j driver
        driver = GraphDatabase.driver( "bolt://localhost:7687", AuthTokens.basic("neo4j", "neo4j"));
    }

    /**
     * Create a new Person.
     * @param person    The Person to create
     */
    public void createPerson(Person person) {
        // Create a Neo4j session. Because the Session object is AutoCloseable, we can use a try-with-resources statement
        try (Session session = driver.session()) {

            // Execute our create Cypher query
            session.run("CREATE (person: Person {name: {name}, age: {age}})",
                    parameters("name", person.getName(), "age", person.getAge()));
        }
    }

    /**
     * Finds all Person objects in the Neo4j database.
     * @return  A set of all Person objects in the Neo4j database.
     */
    public Set<Person> findAllPeople() {
        // Create a set to hold our people
        Set<Person> people = new HashSet<>();

        // Create a Neo4j session
        try (Session session = driver.session()) {

            // Execute our query for all Person nodes
            StatementResult result = session.run("MATCH(person:Person) RETURN person");

            // Iterate over the response
            for (Record record: result.list()) {
                // Load the Neo4j node from the record by the name "person", from our RETURN statement above
                Node person = record.get("person").asNode();

                // Build a new person object and add it to our result set
                Person p = new Person();
                p.setName(person.get("name").asString());
                if (person.containsKey("age")) {
                    p.setAge(person.get("age").asInt());
                }
                people.add(p);
            }
        }

        // Return the set of people
        return people;
    }

    /**
     * Returns the friends of the requested person.
     *
     * @param person    The person for which to retrieve all friends
     * @return          A Set that contains all Person objects for which there is a FRIEND relationship from
     *                  the specified person
     */
    public Set<Person> findFriends(Person person) {
        // A Set to hold our response
        Set<Person> friends = new HashSet<>();

        // Create a session to Neo4j
        try (Session session = driver.session()) {
            // Execute our query
            StatementResult result = session.run("MATCH (person: Person {name: {name}})-[:FRIEND]-(friend: Person) RETURN friend",
                    parameters("name", person.getName()));

            // Iterate over our response
            for (Record record: result.list()) {

                // Create a Person
                Node node = record.get("friend").asNode();
                Person friend = new Person(node.get("name").asString());

                // Add the person to the friend set
                friends.add(friend);
            }
        }

        // Return the set of friends
        return friends;
    }

    /**
     * Find all movies (with rating) seen by the specified Person.
     *
     * @param person    The Person for which to find movies seen
     * @return          A Set of Movies (with ratings)
     */
    public Set<Movie> findMoviesSeenBy(Person person) {
        Set<Movie> movies = new HashSet<>();
        try (Session session = driver.session()) {
            // Execute our query
            StatementResult result = session.run("MATCH (person: Person {name: {name}})-[hasSeen:HAS_SEEN]-(movie:Movie) RETURN movie.title, hasSeen.rating",
                    parameters("name", person.getName()));

            // Iterate over our response
            for (Record record: result.list()) {

                Movie movie = new Movie(record.get("movie.title").asString());
                movie.setRating(record.get("hasSeen.rating").asInt());
                movies.add(movie);
            }
        }
        return movies;
    }

    /**
     * Helper method that prints a person set to the standard output.
     * @param people    The set of Person objects to print to the standard output
     */
    public static void printPersonSet(Set<Person> people) {
        for (Person person: people) {
            StringBuilder sb = new StringBuilder("Person: ");
            sb.append(person.getName());
            if (person.getAge()>0) {
                sb.append(" is " + person.getAge() + " years old");
            }
            System.out.println(sb);
        }
    }


    /**
     * Test methods
     */
    public static void main(String ... args) {
        Neo4jClient client = new Neo4jClient();
        client.createPerson(new Person("Duke", 22));

        Set<Person> people = client.findAllPeople();
        System.out.println("ALL PEOPLE");
        printPersonSet(people);

        Set<Person> friendsOfMichael = client.findFriends(new Person("Michael"));
        System.out.println("FRIENDS OF MICHAEL");
        printPersonSet(friendsOfMichael);

        Set<Movie> moviesSeenByMichael = client.findMoviesSeenBy(new Person("Michael"));
        System.out.println("MOVIES MICHAEL HAS SEEN:");
        for (Movie movie: moviesSeenByMichael) {
            System.out.println("Michael gave the movie " + movie.getTitle() + " a rating of " + movie.getRating());
        }
    }
}

Example app: The Neo4j client class

The Neo4jClient class creates a Neo4j Driver in its constructor. Its methods then use that Driver to create a Session object in order to execute Cypher queries. The createPerson() method executes a CREATE (person:Person {...}) Cypher query with named parameters for “name” and “age.” The parameters() method binds those parameters to the specified Person‘s name and age properties.

The findAllPeople() method finds all Person objects in the database. Note that this method returns all people, so if you have a lot of people you might want to add a LIMIT to the response. Here’s an example:

MATCH (person:Person) RETURN person LIMIT 25

In this case, we’re returning full Person nodes, so we retrieve each node from the Record by requesting the “person” and converting it to a Node with the asNode() method.

The findFriends() method does the same thing, but it executes a different Cypher query:

MATCH (person: Person {name: {name}})-[:FRIEND]-(friend: Person) RETURN friend

We’re requesting the person with a specified name, then following that person’s FRIEND relationships to all Person nodes, giving each node the name “friend.” So, when we retrieve the response from the Record, we ask for the “friend” and convert it to a Node.

Finally, the findMoviesSeenBy() method executes the following Cypher query:

MATCH (person: Person {name: {name}})-[hasSeen:HAS_SEEN]-(movie:Movie) RETURN movie.title, hasSeen.rating

This query starts from the named person and follows all HAS_SEEN relationships to Movie nodes. It then returns the movie title property as movie.title and the rating as hasSeen.rating.

In order to do this we had to specify a variable name, hasSeen, in our HAS_SEEN relationship. Because we asked for the movie title, which is a String, and the rating, which is an integer, we retrieved both individually from the Record::


record.get("movie.title").asString()
record.get("hasSeen.rating").asInt()

The main() method creates a new Neo4jClient, creates a Person named “Duke,” who is 22 years old (hint: just like Java). It finds both Michael’s friends and the movies that he’s seen.

Listing 4 shows the Maven pom.xml file, which we use to build and run our app.

Listing 4. Maven POM for the Neo4jClient app


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.geekcap.javaworld</groupId>
    <artifactId>neo4j-example</artifactId>
    <packaging>jar</packaging>
    <version>1.0-SNAPSHOT</version>
    <name>neo4j-example</name>
    <url>http://maven.apache.org</url>
    <properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <!-- Import Neo4j -->
        <dependency>
            <groupId>org.neo4j.driver</groupId>
            <artifactId>neo4j-java-driver</artifactId>
            <version>1.4.1</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>2.0.2</version>
                <configuration>
                    <source>${java.version}</source>
                    <target>${java.version}</target>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <configuration>
                    <archive>
                        <manifest>
                            <addClasspath>true</addClasspath>
                            <classpathPrefix>lib/</classpathPrefix>
                            <mainClass>com.geekcap.javaworld.neo4j.Neo4jClient</mainClass>
                        </manifest>
                    </archive>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-dependency-plugin</artifactId>
                <executions>
                    <execution>
                        <id>copy</id>
                        <phase>install</phase>
                        <goals>
                            <goal>copy-dependencies</goal>
                        </goals>
                        <configuration>
                            <outputDirectory>${project.build.directory}/lib</outputDirectory>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

This pom.xml file imports the neo4j-java-driver dependency and then defines three plugins:

  • maven-compiler-plugin sets the Java build version to 1.8.
  • maven-jar-plugin makes the resultant JAR file executable with the main class set to com.geekcap.javaworld.neo4j.Neo4jClient and includes all files in the lib directory in the JAR file’s CLASSPATH.
  • maven-dependency-plugin copies all dependencies to the project build directory’s lib folder.

Build and run your Neo4j client application

You can now build the Neo4j client application using the following command:

mvn clean install

You can run it from the target directory with the following command:

java -jar neo4j-example-1.0-SNAPSHOT.jar

You should see output similar to the following:


ALL PEOPLE
Person: Steven is 45 years old
Person: Jordyn
Person: Michael is 16 years old
Person: Katie
Person: Koby
Person: Duke is 22 years old
Person: Grant
Person: Rebecca is 7 years old
Person: Linda
Person: Charlie is 16 years old
FRIENDS OF MICHAEL
Person: Charlie
Person: Grant
Person: Koby
MOVIES MICHAEL HAS SEEN:
Michael gave movie Avengers a rating of 5

Be sure to navigate over to your Neo4j web interface and execute few queries! You should see that Duke was created and be able to validate the results.

Conclusion to Part 2

Neo4j is a graph database that manages highly related data. We started this exploration by reviewing the need for graph databases, especially for querying more than three degrees of separation in relationships. After getting set up with Neo4j in a development environment, we spent most of Part 1 getting to know Neo4j’s Cypher Query Language. We modeled a web of family relationships and queried those relationships using Cypher. Our focus in that article was learning how to think graphically. That is the power of Neo4j, and also the most challenging feature for most developers to master.

In Part 2, you’ve learned how to write a Java application that connects to Neo4j and executes Cypher queries. We took the simplest (manual) approach to integrating Java with Neo4j. Once you’ve got the basics down, you may want to explore more advanced ways of integrating Java with Neo4j–such as by using Neo4j’s Object-Graph-Mapping (OGM) library, Neo4j-OGM, and Spring Data.

Steven Haines

Steven Haines is a senior technologist, accomplished architect, author, and educator. He currently is a principal software engineer at Veeva Systems, where he builds Spring-based Java applications. Steven previously worked on two startups: Chronosphere, where he helped customers design and implement large-scale observability strategies, and Turbonomic, where he was a principal software architect for cloud optimization products. He's also worked for Disney as a technical architect and lead solution architect, building out the next generation of Disney's guest experience and other Disney solutions. Steven specializes in performance and scalability, cloud-based architectures, high-availability, fault tolerance, business analytics, and integration with new and emerging technologies.

As an author, he has written two books on Java programming and more than 500 articles for publications such as InfoWorld, InformIT.com (Pearson Education), JavaWorld, and Dr. Dobb's Journal. He has also written over a dozen white papers and ebooks on performance management and cloud-based architectures.

Steven has taught computer science and Java programming at Learning Tree University and the University of California, Irvine. He also maintains a personal website dedicated to helping software developers and architects grow in their knowledge: www.geekcap.com (by Geeks for Geeks).

More from this author