Updating your Neo4j 3.x Unmanaged Extensions to 4.x

Neo4j 4.0 has been out for a few months now, but since the whole world is on lock down, it didn’t get a chance to make a grand entrance at Graph Connect 2020. It comes loaded with some great new features but I’m not here to tell you about all that. There are plenty of better places for it. Instead I’m going to tell you about an old feature that got a bit of an update. Unmanaged Extensions. Yup, those things have been with us since dinosaurs roamed the earth and they are still in Neo4j 4.0. Why you ask? Because they let you turn Neo4j into an HTTP API style service making it super easy to integrate into your existing infrastructure. It’s still one of my favorite ways to build Neo4j applications because once you have the documentation of the API locked down, you can crank out the endpoints quickly and the service is done before you know it.

About 3 years ago or so, I went into a multi-part series on how to build a Twitter clone using Neo4j’s unmanaged extensions. Today we’re going to upgrade that 3.x code into Neo4j 4.x. To make life easier, I did it all in one big commit which you are welcome to follow along with. However the point is more to show you what you have to do to upgrade your unmanaged extensions if you want to upgrade to Neo4j 4.0.

Let’s start things off with the pom.xml file. The obvious change here is the Neo4j version. On the date of publication we are up to 4.0.3 for Neo4j. The other big change is we are no longer using jaxrs version 1.x we’ve moved up to 2.1.1. So make those changes.

 
    <properties>
        <neo4j.version>4.0.3</neo4j.version>
        <jaxrs.version>2.1.1</jaxrs.version>       
    </properties>

It’s easy to get the right neo4j dependencies if you just ask for everything and set the scope to “provided”. Make sure to pull in the test harness for testing, and the last dependency here is the jaxrs we talked about.

 
        <dependency>
            <groupId>org.neo4j</groupId>
            <artifactId>neo4j</artifactId>
            <version>${neo4j.version}</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.neo4j.test</groupId>
            <artifactId>neo4j-harness</artifactId>
            <version>${neo4j.version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>javax.ws.rs</groupId>
            <artifactId>javax.ws.rs-api</artifactId>
            <version>${jaxrs.version}</version>
        </dependency>

Let’s start off with the things that can go wrong. In the past when we encountered an unrecoverable error we would throw out an exception. These were static and just fine in jax-rs 1.1 but for whatever reason that doesn’t work in 2.x and we have to make them every time we have to throw one. So easy fix:

 
// Replace
public static final Exceptions invalidInput = new Exceptions(400, "Invalid Input");
// with 
public static final Exceptions invalidInput() {
        return new Exceptions(400, "Invalid Input");
    }

This also means your code to throw them needs a little “lisp” and by that I mean lots and lots of parenthesis:

 
// Add parenthesis to all your throws.
// This:
throw UserExceptions.missingUsernameParameter;
// becomes:
throw UserExceptions.missingUsernameParameter();

In jax-rs 1.1 exceptions took care of their own responses… for whatever reason that doesn’t work in 2.x and we have to make an ExceptionMapper to do this for us… It looks like this:

 
@Provider
public class ExceptionsMapper implements ExceptionMapper<Exceptions> {
    @Override
    public Response toResponse(Exceptions exceptions) {
        return exceptions.getResponse();
    }
}

Alright, with those out of the way let’s talk about what’s changed in Neo4j. In 3.x we only had one database to work with, so it was passed in to every call of the unmanaged extensions via GraphDatabaseService:

 
@Context GraphDatabaseService db

Since we can have many databases in Neo4j 4.x, this won’t work anymore. Instead we have a DatabaseManagementService in our Context and from that we pull the database we want.

 
@Context DatabaseManagementService dbms

Since we’re just upgrading we are going to use the default “neo4j” database, so what we can do is set it all up in the constructor. For example for the “blocks” endpoint we have our trusty GraphDatabaseService service which we initialize like so:

 
@Path("/users/{username}/blocks")
public class Blocks {

    private final GraphDatabaseService db;
    private static final ObjectMapper objectMapper = new ObjectMapper();

    public Blocks(@Context DatabaseManagementService dbms ) {
        this.db = dbms.database( "neo4j" );
    }

The next big change is where all the methods went. They used to be attached to the GraphDatabaseService object, but those are now gone a level lower. Instead we have to call them from within a Transaction. Use your IDE to search and replace all “db.” with “tx.” in every file.

 
try (Transaction tx = db.beginTx()) {
    // Before we called methods directly from the database object:
    ResourceIterator<Node> tags = db.findNodes(Labels.Tag);
    // Now we call methods from the transaction object:
    ResourceIterator<Node> tags = tx.findNodes(Labels.Tag);

Speaking of search and replace all, the Transaction “success” method has been renamed to “commit” which makes a helluva lot more sense. So make sure you change all of those as well:

 
// Replace
tx.success();
// with
tx.commit();

While we are here, it used to be very painful to do a text search with “CONTAINS” in Neo4j. We had to jump through some hoops to get access to the Index and it looked like this mess:

 
    private static ArrayList<Long> performSearch(String term) throws SchemaRuleNotFoundException, IndexNotFoundKernelException {
        ThreadToStatementContextBridge ctx = dbapi.getDependencyResolver().resolveDependency(ThreadToStatementContextBridge.class);
        KernelStatement st = (KernelStatement)ctx.get();
        ReadOperations ops = st.readOperations();
        IndexDescriptor descriptor = ops.indexGetForLabelAndPropertyKey(postLabelId, statusPropertyId);
        IndexReader reader = st.getStoreStatement().getIndexReader(descriptor);

        PrimitiveLongIterator hits = reader.containsString(term);

Now in the findNodes method you have a 4th option that lets you specify the StringSearchMode of the query. So you can do STARTS_WITH, ENDS_WITH or in our case “CONTAINS”. If an index exists on the property we are searching on, it will use it. So that ugly code above becomes:

 
ResourceIterator<Node> iterator = 
  tx.findNodes(Labels.Post, STATUS, term, StringSearchMode.CONTAINS);
  while (iterator.hasNext()) {
       results.add(iterator.next().getId());

Much nicer right? A few things have changed in our test classes as well. The Neo4jRule has been moved to .rule:

 
// Replace
import org.neo4j.harness.junit.Neo4jRule;
// with
import org.neo4j.harness.junit.rule.Neo4jRule;

That’s no big deal, here is another one. The “withExtension” method that used to hang off the Neo4jRule has been renamed to “withUnmanagedExtension” because maybe one day we will have Managed Extensions? I doubt it, but whatever just do a search and replace all in all your test files. You DID write tests? If not, now is the time to correct that.

 
public Neo4jRule neo4j = new Neo4jRule()
// Replace
.withExtension("/v1", Schema.class);
// with
.withUnmanagedExtension("/v1", Schema.class);

I think that covers it. Once again check out the commit or project if you want to see an example. If you run into anything else not covered here, mention it in the comments below.

Tagged , , , , , , , , , ,

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: