Skip to content

6.7.0

Compare
Choose a tag to compare
@michael-simons michael-simons released this 09 Jul 12:16
· 109 commits to main since this release

What's Changed

Can your JDBC driver do this?

To highlight some of the features that have accumulated in the Neo4j JDBC driver since the first commit to the 6.x branch two years ago, we decided to create two hopefully easy to read demos before we jump into the list of features. We use plain Java in the first 3 examples, no dependency management and run fully on the module path with Demo1.java. The keen reader may notices that we do use some Java 24 preview features: Why bother with a class definition? And if we have Java modules, why not use them for import statements? (Be aware, that the JDBC driver does run on Java 17, too and does not require any preview feature)

Cypher and SQL supported

First, let's download the JDBC bundle:

wget https://repo.maven.apache.org/maven2/org/neo4j/neo4j-jdbc-full-bundle/6.7.0/neo4j-jdbc-full-bundle-6.7.0.jar

And run the following class

import module java.sql;

void main() throws SQLException {

    try (
        var connection = DriverManager.getConnection("jdbc:neo4j://localhost:7687/movies", "neo4j", "verysecret");
        var stmt = connection.prepareStatement("""
            MERGE (g:Genre {name: $1})
            MERGE (m:Movie {title: $2, released: $3}) -[:HAS]->(g)
            FINISH"""))
    {
        stmt.setString(1, "Science Fiction");
        stmt.setString(2, "Dune");
        stmt.setInt(3, 2021);
        stmt.addBatch();

        stmt.setString(1, "Science Fiction");
        stmt.setString(2, "Star Trek Generations");
        stmt.setInt(3, 1994);
        stmt.addBatch();

        stmt.setString(1, "Horror");
        stmt.setString(2, "Seven");
        stmt.setInt(3, 1995);
        stmt.addBatch();

        stmt.executeBatch();
    }

    // Not happy with Cypher? Enable SQL translations
    try (
        var connection = DriverManager.getConnection("jdbc:neo4j://localhost:7687/movies?enableSQLTranslation=true", "neo4j", "verysecret");
        var stmt = connection.createStatement();
        var rs = stmt.executeQuery("""
            SELECT m.*, collect(g.name) AS genres
            FROM Movie m NATURAL JOIN HAS r NATURAL JOIN Genre g
            WHERE m.title LIKE 'S%'    
            ORDER BY m.title LIMIT 20"""))
    {
        while (rs.next()) {
            var title = rs.getString("title");
            var genres = rs.getObject("genres", List.class);
            System.out.println(title + " " + genres);
        }
    }
}

like this:

java --enable-preview --module-path neo4j-jdbc-full-bundle-6.7.0.jar Demo1.java

The output of the program will be similar to this. Notice how the Cypher has been rewritten to be proper batched:

Juli 09, 2025 12:52:54 PM org.neo4j.jdbc.PreparedStatementImpl executeBatch
INFORMATION: Rewrite batch statements is in effect, statement MERGE (g:Genre {name: $1})
MERGE (m:Movie {title: $2, released: $3}) -[:HAS]->(g)
FINISH has been rewritten into UNWIND $__parameters AS __parameter MERGE (g:Genre {name: __parameter['1']})
MERGE (m:Movie {title: __parameter['2'], released: __parameter['3']}) -[:HAS]->(g)
FINISH
Seven [Horror]
Star Trek Generations [Science Fiction]
Horror
Science Fiction

Bonus example As JDBC 4.3, section 13.2 specifies that only ? are allowed as positional parameters, we do of course handle those. The above example uses named Cypher parameters $1, $2 to align them with the indexes that the PreparedStatement requires. If we would switch languages here, using SQL for inserting and Cypher for querying, you see the difference. Also take now that you can unwrap the PreparedStatement into a Neo4jPreparedStatement that allows you to use named parameters proper:

import module java.sql;
import module org.neo4j.jdbc;

void main() throws SQLException {

    try (
        var connection = DriverManager.getConnection("jdbc:neo4j://localhost:7687/movies?enableSQLTranslation=true", "neo4j", "verysecret");
        var stmt = connection.prepareStatement("""
            INSERT INTO Movie (title, released) VALUES (?, ?) 
            ON CONFLICT DO NOTHING
            """))
    {
        stmt.setString(1, "Dune: Part Two");
        stmt.setInt(2, 2024);
        stmt.addBatch();

        stmt.executeBatch();
    }

    try (
        var connection = DriverManager.getConnection("jdbc:neo4j://localhost:7687/movies", "neo4j", "verysecret");
        var stmt = connection.prepareStatement("""
            MATCH (n:$($label)) 
            WHERE n.released = $year 
            RETURN DISTINCT n.title AS title""")
            .unwrap(Neo4jPreparedStatement.class))
    {
        stmt.setString("label", "Movie");
        stmt.setInt("year", 2024);

        var rs = stmt.executeQuery();
        while (rs.next()) {
            var title = rs.getString("title");
            System.out.println(title);
        }
    }
}

As we importing the whole Neo4j JDBC Driver module at the top of the class, we must explicitly add it to the module path. Run the program as follows, so that you don't have to define a module-info.java for that anonymous class:

java --enable-preview --module-path neo4j-jdbc-full-bundle-6.7.0.jar --add-modules org.neo4j.jdbc Demo5.java

Cypher backed views

Your ETL tool just let you run plain selects but you do want to have some more complex Cypher? Don't worry, defined a Cypher-backed view like in demo 2 and we got you covered:

import module java.sql;

void main() throws SQLException, IOException {

    // We do write this definition from the demo to a file, in reality it can be a 
    // module- or classpath resource, a file or a resource on a webserver.
    var viewDefinition = """
        [
          {
            "name": "movies",
            "query": "MATCH (m:Movie)-[:HAS]->(g:Genre) RETURN m, collect(g.name) AS genres",
            "columns": [
              {
                "name": "title",
                "propertyName": "m.title",
                "type": "STRING"
              },
              {
                "name": "genres",
                "type": "ANY"
              }
            ]
          }
        ]""";
    var views = Files.createTempFile("views", ".json");
    Files.writeString(views, viewDefinition);

    var url = "jdbc:neo4j://localhost:7687/movies?enableSQLTranslation=%s&viewDefinitions=%s"
        .formatted(true, "file://" + views.toAbsolutePath());
    try (
        var connection = DriverManager.getConnection(url, "neo4j", "verysecret");
        var stmt = connection.createStatement();
        var rs = stmt.executeQuery("SELECT * FROM movies"))
    {
        while (rs.next()) {
            var title = rs.getString("title");
            var genres = rs.getObject("genres", List.class);
            System.out.println(title + " " + genres);
        }
    }
}

Running

java --enable-preview --module-path neo4j-jdbc-full-bundle-6.7.0.jar Demo2.java

gives you

Dune [Science Fiction]
Star Trek Generations [Science Fiction]
Seven [Horror]

Support for the Neo4j HTTP Query API

Communication with Neo4j is usually done via the Bolt protocol, which is a binary format, running on a dedicated port. That can be problematic at times. Latest Neo4j server 2025.06 and higher have enabled the Query API, that allows Cypher over HTTP. The Neo4j JDBC driver can utilise that protocol, too, by just changing the URL like this:

import module java.sql;

void main() throws SQLException {

    // Can't use Neo4j binary protocol, let's use http…
    var url = "jdbc:neo4j:http://localhost:7474/movies?enableSQLTranslation=true";
    try (
        var connection = DriverManager.getConnection(url, "neo4j", "verysecret");
        var stmt = connection.createStatement();
        var rs = stmt.executeQuery("SELECT * FROM Genre ORDER BY name"))
    {
        while (rs.next()) {
            var genre = rs.getString("name");
            System.out.println(genre);
        }
    }

    // This issue will be fixed in one of the next patch releases
    System.exit(0);
}

Everything else stays the same, the output of the query is

Horror
Science Fiction

Retrieving Graph data as JSON objects

Last but not least, we did add some Object mapping to the driver. This is the only feature that requires an additional dependency. As we decided to go with an intermediate format, JSON, we are using Jackson Databind. With Jackson on the path, you can turn any Neo4j result into JSON by just asking the ResultSet for a JsonNode. The latter can than be converted to any Pojo or collection you desire.

You need JBang to run the last demo. It does the dependency management for us when you run jbang Demo4.java

///usr/bin/env jbang "$0" "$@" ; exit $?
//JAVA 24
//PREVIEW
//DEPS org.neo4j:neo4j-jdbc:6.7.0
//DEPS com.fasterxml.jackson.core:jackson-databind:2.19.1

import java.sql.DriverManager;
import java.sql.SQLException;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

void main() throws SQLException, JsonProcessingException {

    var objectMapper = new ObjectMapper();

    record Movie(String title, int released, List<String> genres) {
    }

    try (
        var connection = DriverManager.getConnection("jdbc:neo4j://localhost:7687/movies", "neo4j", "verysecret");
        var stmt = connection.createStatement();
        var rs = stmt.executeQuery("""
            MATCH (m:Movie)-[:HAS]->(g:Genre)
            WITH m, collect(g.name) AS genres
            RETURN m{.*, genres: genres}
            """)) {
        while (rs.next()) {
            // First let's get a JSON object from the Cypher map
            var json = rs.getObject(1, JsonNode.class);
            // Turn it into domain objects
            var movie = objectMapper.treeToValue(json, Movie.class);
            System.out.println(movie);
        }
    }
}

The output now looks like this (with the dataset created in Demo1.java):

Movie[title=Dune, released=2021, genres=[Science Fiction]]
Movie[title=Star Trek Generations, released=1994, genres=[Science Fiction]]
Movie[title=Seven, released=1995, genres=[Horror]]

The Neo4j JDBC driver is a collective effort of @gjmwoods who did most of the work on the Query API server sides, @injectives and @meistermeier who implemented the client side parts of it in the Bolt connection module and @michael-simons who joined all of this together in the 6.7.0 release of the Neo4j JDBC driver.

🚀 Features

  • 9300fd6 feat: Upgrade bolt-connection to 6.0.2 and add support for JDBC over HTTP. (#979)
  • cf13056 feat(translator): Add support for CONCAT. (#1023)
  • 8ad7285 feat: Allow the retrieval of graph data as JsonNode objects. (#1022)

🔄️ Refactorings

  • e969fcb refactor: Remove superflous module declaration.
  • 82eb600 refactor: Reduce log level of global metrics collector.
  • 9b58975 refactor: Limit maximum bolt version to 5.8.

📝 Documentation

  • 5d052d9 docs: Add database name to example urls.

🧹 Housekeeping

  • 0e760bc build(deps-dev): Bump com.github.dasniko:testcontainers-keycloak (#1031)
  • 7250aaf Bump org.hibernate.orm:hibernate-platform (#1030)
  • 41edea0 Bump org.apache.maven.plugins:maven-enforcer-plugin (#1029)
  • d692605 Bump quarkus.platform.version from 3.24.1 to 3.24.2 (#1026)
  • 3695bef Bump org.jreleaser:jreleaser-maven-plugin (#1025)
  • 6989d9f Bump org.keycloak:keycloak-authz-client (#1024)