neotypes: Akka-http + neo4j
I haven’t been writing posts for a while and this is going to be my first blog post for this year. Today, I’m going to introduce you to my most recent pet project - neotypes.
We’re going to be talking about graph databases today. If you’re not familiar with graph databases and neo4j particularly, please check this article.
I’ve been using neo4j for about a year now and I absolutely love it. Of course, it’s not a silver bullet but it’s something new that helps to model your domain on a completely different level. Since I also love Scala and functional programming, I was missing a “native” Scala driver. So I decided to build it by myself.
neotypes is a Scala lightweight, type-safe, asynchronous driver (not opinionated on side-effect implementation) for neo4j. Let’s break this sentence down into distinct properties of this driver.
- Scala - the driver provides you with support for all standard Scala types without the need to convert Scala <-> Java types back and forth and you can easily add your types.
- Lightweight - the driver depends on
shapeless
andneo4j Java driver
- Type-safe - the driver leverages typeclasses to derive all needed conversions at the compile time.
- Asynchronous - the driver sits on top of asynchronous Java driver.
- Not opinionated on side-effect implementation - you can use it with any implementation of side-effects of your chose (scala.Future, cats-effect IO, Monix Task, etc)
Ok, let’s jump to a real example.
Akka-http + neo4j
We will be building a sample application using Akka-http as a web server and neo4j as a database. Akka-http is pretty popular framework in Scala ecosystem for building http applications such as sites, http APIs, microservices, etc. Akka and Akka-http are based on scala.concurrent.Future
, so using a neo4j Java driver would be a challenge. What you would do is you would deal with tons of java.util.concurrent.CompletionStage
to scala.concurrent.Future
conversion and try to map query outputs to your domain classes manually. neotypes offers a better approach.
Our application is going to be a movie catalog based on the Movie graph dataset which comes with neo4j. There is an existing application built by neo4j team as an example of java driver usage. So we can rewrite it to Scala.
Environment configuration
To start, we need a neo4j instance to run our queries against. I’m a docker fan, so we going to use a docker compose to run my local instance of the database.
My docker-compose.yml
looks as follows
Execute docker-compose up
to start the database. After it’s up and running, go to http://localhost:7474/browser/ and execute :play movie-graph
in the query window. Now we have the sample data.
If you run a query MATCH p=()-->() RETURN p LIMIT 25
, you will see something similar to:
Data access layer
One of the queries that we want to run is to find a movie cast by its title. The query that is used in java application looks as follows:
MATCH (movie:Movie {title: $title})
OPTIONAL MATCH (movie)<-[r]-(person:Person)
RETURN movie.title as title,
collect({name:person.name, job:head(split(lower(type(r)),'_')), role:head(r.roles)}) as cast
LIMIT 1
We need to come up with a model that the result of this query will be mapped to. We could use anonymous types such as Tuple
or HList
but it’s easier to map the result to a case class and then convert it to json object as-is. In our case, we can use these two case classes:
Since the query output has aliases for each element, they will be mapped to corresponding fields of a case class.
Now we can write some queries using neotypes. For simplicity, a service layer is incapsulating both business logic and data access logic (do not do it this way in your production code).
Let’s take a look at this code. In order to access neo4j, we need to obtain an instance of a Driver. For modularity and testing purposes, the driver is injected via the constructor. As you can see, we’re passing an instance of neotypes.Driver
parametrized by [Future]
which means that we’re using neotypes
wrapper for org.neo4j.driver.v1.Driver
and all side effects will be encapsulated in scala.concurrent.Future
s.
neotypes.Driver
has a very handy method readSession
(and the same write
alternative) for automatic resource management. Using this method, you don’t need to think about session closing as well as transaction commits/rollbacks - it’s all done by the library.
c"""MATCH (movie:Movie {title: $title})...
is custom string interpolator. It means that $title
is not regular interpolation. neotypes converts all variables used in a query into neo4j supported types and passes values via statement parameters so that the actual query will look like MATCH (movie:Movie {title: $p1})...
and the actual title value will be passed as p1 -> [value]
Now we’re coming to the magic part. query[Option[MovieCast]]
defines our mapping. As you can see, we haven’t explicitly specified the mapping rules because neotypes can infer those rules for you. Wrapping of your target type into Option
helps us to deal with no result cases.
Akka-http
Let’s now define a simple router. Our goal is to handle /movie/[movie name]
urls.
it’s pretty straightforward. Since we’re dealing with scala Futures, we don’t need to convert our service layer output, we just map it to a proper http response.
Wrap it up
Now we just need to glue everything together.
In the beginning, we create regular org.neo4j.driver.v1.Driver
, and then wrap it into neotypes Driver
by using .asScala[Future]
method. That’s it!
To demonstrate the application, we can use curl
to call endpoints.
$ curl http://localhost:9000/movie/The%20Matrix
{"title":"The Matrix","cast":[{"name":"Emil Eifrem","job":"acted","role":"Emil"},{"name":"Joel Silver","job":"produced","role":"null"},{"name":"Lana Wachowski","job":"directed","role":"null"},{"name":"Lilly Wachowski","job":"directed","role":"null"},{"name":"Hugo Weaving","job":"acted","role":"Agent Smith"},{"name":"Laurence Fishburne","job":"acted","role":"Morpheus"},{"name":"Carrie-Anne Moss","job":"acted","role":"Trinity"},{"name":"Keanu Reeves","job":"acted","role":"Neo"}]}%
I also borrowed a UI part of the java sample application so that you can enjoy a nice looking visual representation.
Conclusion
As you could see, neotypes drastically simplifies interaction with neo4j from Scala. If you want to learn more and help the project, please:
- Star the project on GitHub - https://github.com/neotypes/neotypes
- Read the official documentation - https://neotypes.github.io/neotypes/docs.html
- Check the sources of the application we built today - https://github.com/neotypes/examples