1. Introduction

This article is about Neo4j – one of the most mature and full-featured graph databases on the market today. Graph databases approach the task of data modeling with the view that many things in life lend themselves to being represented as a collection of nodes (V) and connections between them called edges (E).

2. Embedded Neo4j

The easiest way to get started with Neo4j is to use the embedded version in which Neo4j runs in the same JVM as your application.

First, we need to add a Maven dependency:

<dependency>
    <groupId>org.neo4j</groupId>
    <artifactId>neo4j</artifactId>
    <version>5.8.0</version>
</dependency>

You can check this link to download the latest version.

Next, let’s create a DatabaseManagementService:

DatabaseManagementService managementService = new 
  DatabaseManagementServiceBuilder(new File("data/cars").toPath())
  .setConfig(GraphDatabaseSettings.transaction_timeout, Duration.ofSeconds( 60 ) )
  .setConfig(GraphDatabaseSettings.preallocate_logical_logs, true ).build();

This includes an embedded database. Finally, we create a GraphDatabaseService:

GraphDatabaseService graphDb = managementService.database( DEFAULT_DATABASE_NAME );

Now the real action can begin! First, we need to create some nodes in our graph and for that, we need to start a transaction since Neo4j will reject any destructive operation unless a transaction has been started:

Transaction transaction = graphDb.beginTx();

Each operation that we execute like createNode/execute should run in the context of the created transaction and use that object.

Once we have a transaction in progress, we can start adding nodes:

Node car = transaction.createNode(Label.label("Car"));
car.setProperty("make", "tesla");
car.setProperty("model", "model3");

Node owner = transaction.createNode(Label.label("Person"));
owner.setProperty("firstName", "baeldung");
owner.setProperty("lastName", "baeldung");

Here we added a node Car with properties make and model as well as node Person with properties firstName and lastName

Now we can add a relationship:

owner.createRelationshipTo(car, RelationshipType.withName("owner"));

The statement above added an edge joining the two nodes with an owner label. We can verify this relationship by running a query written in Neo4j’s powerful Cypher language:

Result result = transaction.execute(
  "MATCH (c:Car) <-[owner]- (p:Person) " +
  "WHERE c.make = 'tesla'" +
  "RETURN p.firstName, p.lastName");

Here we ask to find a car owner for any car whose make is tesla and give us back his/her firstName and lastName. Unsurprisingly, this returns: {p.firstName=baeldung, p.lastName=baeldung}

3. Cypher Query Language

Neo4j provides a very powerful and pretty intuitive querying language which supports the full range of features one would expect from a database. Let us examine how we can accomplish that standard create, retrieve, update and delete tasks.

3.1. Create Node

Create keyword can be used to create both nodes and relationships.

CREATE (self:Company {name:"Baeldung"})
RETURN self

Here we’ve created a company with a single property name. A node definition is marked by parentheses and its properties are enclosed in curly braces. In this case, self is an alias for the node and Company is a node label.

3.2. Create Relationship

It is possible to create a node and a relationship to that node all in a single query:

Result result = transaction.execute(
  "CREATE (baeldung:Company {name:\"Baeldung\"}) " +
  "-[:owns]-> (tesla:Car {make: 'tesla', model: 'modelX'})" +
  "RETURN baeldung, tesla");

Here we’ve created nodes baeldung and tesla and established an ownership relationship between them. Creating relationships to pre-existing nodes is, of course, also possible.

3.3. Retrieve Data

MATCH keyword is used to find data in combination with RETURN to control which data points are returned. The WHERE clause can be utilized to filter out only those nodes that have the properties we desire.

Let us figure out the name of the company that owns tesla modelX:

Result result = transaction.execute(
  "MATCH (company:Company)-[:owns]-> (car:Car)" +
  "WHERE car.make='tesla' and car.model='modelX'" +
  "RETURN company.name");

3.4. Update Nodes

SET keyword can be used for updating node properties or labels. Let us add mileage to our tesla:

Result result = transaction.execute("MATCH (car:Car)" +
  "WHERE car.make='tesla'" +
  " SET car.milage=120" +
  " SET car :Car:Electro" +
  " SET car.model=NULL" +
  " RETURN car");

Here we add a new property called milage, modify labels to be both Car and Electro and finally, we delete model property altogether.

3.5. Delete Nodes

DELETE keyword can be used for permanent removal of nodes or relationships from the graph:

transaction.execute("MATCH (company:Company)" +
  " WHERE company.name='Baeldung'" +
  " DELETE company");

Here we deleted a company named Baeldung.

3.6. Parameter Binding

In the above examples, we have hard-coded parameter values which isn’t the best practice. Luckily, Neo4j provides a facility for binding variables to a query:

Map<String, Object> params = new HashMap<>();
params.put("name", "baeldung");
params.put("make", "tesla");
params.put("model", "modelS");

Result result = transaction.execute("CREATE (baeldung:Company {name:$name}) " +
  "-[:owns]-> (tesla:Car {make: $make, model: $model})" +
  "RETURN baeldung, tesla", params);

4. Java Driver

So far we’ve been looking at interacting with an embedded Neo4j instance, however, in all probability for production, we’d want to run a stand-alone server and connect to it via a provided driver. First, we need to add another dependency in our maven pom.xml:

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

You can follow this link to check for the latest version of this driver.

To simulate a production setup we will use the test containers that will start a neo4j server in a docker container. For this we will use the dependency of test containers for neo4j.

<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>neo4j</artifactId>
    <version>${testcontainers.version}</version>
    <scope>test</scope>
    <exclusions>
        <exclusion>
            <groupId>org.jetbrains</groupId>
            <artifactId>annotations</artifactId>
        </exclusion>
        <exclusion>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-compress</artifactId>
        </exclusion>
        <exclusion>
            <groupId>javax.xml.bind</groupId>
            <artifactId>jaxb-api</artifactId>
        </exclusion>
    </exclusions>
</dependency>

Now we can establish a connection using a neo4j container:

boolean containerReuseSupported = TestcontainersConfiguration.getInstance().environmentSupportsReuse();

Neo4jContainer neo4jServer = new Neo4jContainer<>(imageName).withReuse(containerReuseSupported);
Driver driver = GraphDatabase.driver(
  neo4jServer.getBoltUrl(), AuthTokens.basic("neo4j", "12345"));

Then, create a session:

Session session = driver.session();

Finally, we can run some queries:

session.run("CREATE (baeldung:Company {name:\"Baeldung\"}) " +
  "-[:owns]-> (tesla:Car {make: 'tesla', model: 'modelX'})" +
  "RETURN baeldung, tesla");

Once we are done with all our work we need to close both session and the driver:

session.close();
driver.close();

5. JDBC Driver

It is also possible to interact with Neo4j via a JDBC driver. Yet another dependency for our pom.xml:

<dependency>
    <groupId>org.neo4j</groupId>
    <artifactId>neo4j-jdbc-driver</artifactId>
    <version>4.0.9</version>
</dependency>

You can follow this link to download the latest version of this driver.

Next, let’s establish a JDBC connection that will connect to an existing instance of neo4j server which runs in a test conainer as we presented in the previous section:

String uri = "jdbc:neo4j:" + neo4jServer.getBoltUrl() + "/?user=neo4j,password=" + DEFAULT_PASSWORD + ",scheme=basic";
Connection con = DriverManager.getConnection(uri);

Here con is a regular JDBC connection which can be used for creating and executing statements or prepared statements:

try (Statement stmt = con.
  stmt.execute("CREATE (baeldung:Company {name:\"Baeldung\"}) "
  + "-[:owns]-> (tesla:Car {make: 'tesla', model: 'modelX'})"
  + "RETURN baeldung, tesla")

    ResultSet rs = stmt.executeQuery(
      "MATCH (company:Company)-[:owns]-> (car:Car)" +
      "WHERE car.make='tesla' and car.model='modelX'" +
      "RETURN company.name");

    while (rs.next()) {
        rs.getString("company.name");
    }
}

6. Object-Graph-Mapping

Object-Graph-Mapping or OGM is a technique which enables us to use our domain POJOs as entities in the Neo4j database. Let us examine how this works. The first step, as usually, we add new dependencies to our pom.xml:

<dependency>
    <groupId>org.neo4j</groupId>
    <artifactId>neo4j-ogm-core</artifactId>
    <version>4.0.5</version>
</dependency>

<dependency> 
    <groupId>org.neo4j</groupId>
    <artifactId>neo4j-ogm-embedded-driver</artifactId>
    <version>3.2.39</version>
</dependency>

You can check the OGM Core Link and OGM Embedded Driver Link to check for the latest versions of these libraries.

Second, we annotate our POJO’s with OGM annotations:

@NodeEntity
public class Company {
    private Long id;

    private String name;

    @Relationship(type="owns")
    private Car car;
}

@NodeEntity
public class Car {
    private Long id;

    private String make;

    @Relationship(direction = INCOMING)
    private Company company;
}

@NodeEntity informs Neo4j that this object will need to be represented by a node in the resulting graph. @Relationship communicates the need to create a relationship with a node representing the related type. In this case, a company owns a car.

Please note that Neo4j requires each entity to have a primary key, with a field named id being picked up by default. An alternatively named field could be used by annotating it with @Id @GeneratedValue.

Then, we need to create a configuration that will be used to bootstrap Neo4j‘s OGM. For this we will use the test container to simulate a neo4j server:

Configuration.Builder baseConfigurationBuilder = new Configuration.Builder()
  .uri(NEO4J_URL)
  .verifyConnection(true)
  .withCustomProperty(CONFIG_PARAMETER_BOLT_LOGGING, Logging.slf4j())
  .credentials("neo4j", Optional.ofNullable(System.getenv(SYS_PROPERTY_NEO4J_PASSWORD)).orElse("").trim());

From the above configuration, we will configure the driver that will be passed to the SessionFactory:

Driver driver = new org.neo4j.ogm.drivers.bolt.driver.BoltDriver();
driver.configure(baseConfigurationBuilder.build());

After that, we initialize SessionFactory with the driver that we created and a package name in which our annotated POJOs reside:

SessionFactory factory = new SessionFactory(getDriver(), "com.baeldung.neo4j.domain");

Finally, we can create a Session and begin using it:

Session session = factory.openSession();
Car tesla = new Car("tesla", "modelS");
Company baeldung = new Company("baeldung");

baeldung.setCar(tesla);
session.save(baeldung);

Here we initiated a session, created our POJO’s and asked OGM session to persist them. Neo4j OGM runtime transparently converted objects to a set of Cypher queries which created appropriate nodes and edges in the database.

If this process seems familiar, that is because it is! That is precisely how JPA works, the only difference being whether object gets translated into rows that are persisted to an RDBMS, or a series of nodes and edges persisted to a graph database.

7. Conclusion

This article looked at some basics of a graph-oriented database Neo4j.

As always, the code in this write-up is all available over on Github.

Course – LSD (cat=Persistence)

Get started with Spring Data JPA through the reference Learn Spring Data JPA course:

>> CHECK OUT THE COURSE
res – Persistence (eBook) (cat=Persistence)
Comments are open for 30 days after publishing a post. For any issues past this date, use the Contact form on the site.