Longevity Play Tutorial
longevity is a Persistence Framework for Scala and NoSQLTweet
longevity is a Persistence Framework for Scala and NoSQLTweet
There are several ways to get this template.
activator-longevity-play-tutorialin the Lightbend Activator UI.
Already have Lightbend Activator (get it
here)? Launch the UI then
activator-longevity-play-tutorial in the list of
activator-longevity-play-tutorialproject as a zip archive
If you haven't installed Activator, you can get the code
by downloading the template bundle
In your File Explorer, navigate into the directory that the template was extracted to, right-click on the file named "activator.bat", then select "Open", and if prompted with a warning, click to continue:
Or from a command line:
This will start Lightbend Activator and open this template in your browser.
C:\Users\typesafe\activator-longevity-play-tutorial> activator ui
activator-longevity-play-tutorialproject from the command line
If you have Lightbend Activator, use its command line mode
to create a new project from this template.
activator new PROJECTNAME activator-longevity-play-tutorial on the command line.
The creator of this template maintains it at https://github.com/longevityframework/simbl-play#master.
We've included the text of this template's tutorial below, but it may work better if you view it inside Activator on your computer. Activator tutorials are often designed to be interactive.
This tutorial walks through the basic steps needed to get started building a real-life application with longevity. The application we will be looking at here is a sample blogging application, built with longevity on the back end, and using Play for a REST API that could be used by a web client.
We will only have the chance to cover a portion of the blogging application code in this tutorial, so please feel free to explore the codebase further on your own. You can also look to the user manual for more information.
We have four types in our domain model that we want to persist: users, blogs, blog posts, and comments. The arrows in this diagram indicate relationships between them: comments are made on blog posts, blog posts are made in a blog, and blogs, blog posts and comments all have authors:
For the purposes of this tutorial, we are going to focus in on the user, which consists of two main parts: the user itself, and the user profile:
The user has four parts: the
UserProfile, and two natural keys: the
Username and the
User first. You can find the source code for
User case class provides us with the four members we find in the UML in the , including the relationship between
UserProfile. There are also a couple of business methods inside:
In longevity terminology,
Users are persistent objects - that is, objects we want to persist in their own table or collection. We tell longevity that we want to persist them by marking them with the
When we annotate
User as a persistent object, longevity creates a set of properties for us that we can use to reflect on
User fields. It puts these properties in an inner object
props in the
User companion object. Now we can talk about
keySet parameter on the
@persistent annotation to tell longevity about our keys. We define keys on the
You can have as many keys as you like, but only one of the keys - in our case,
username - can be a primary key. Primary keys perform better than other keys when you are using a distributed database, since the database can determine the node that holds the data by examining the key.
The user profile is a simple case class. In longevity, we call the
UserProfile a persistent component - a class that we persist, but not in its own table. They only get persisted along with a containing persistent object such as
UserProfile has two members that are also persistent components:
Markdown. These are simple wrapper classes for strings, which provide extra type safety, but are also places where we might add some extra functionality in the future. For instance, the
Uri constructor might throw some kind of validation exception if the provided string is not a valid URI. As you can see, we can freely nest components within our persistent object classes.
The final components of our user aggregate are
User.keys.email, respectively. Aside from being parts of our user aggregate, we can also embed them in other classes, such as
BlogPost, (see line 18), to describe a relationship between a blog post and its authors.
Once the elements we want to persist have been created, we gather them all together into a
DomainModel object. We do this in
SimblDomainModel using the
Once we have your domain model in place, we are ready to build our
LongevityContext, as we do on line 16 of
Module.scala. The longevity context provides a variety of tools that are tailored to your model. The most important of these is the
RepoPool, which contains repositories for your persistent objects. You can use these repositories to do standard CRUD operations (create/retrieve/update/delete), as well as executing queries that return more than one result.
Longevity uses Typesafe Config to configure the longevity context. Typically, the configuration is drawn from the
application.conf resource file. Here, you can find configurations for main and test databases for the various back ends. If you want to experiment with adjusting the persistence strategy to use a real database, you may need to adjust this configuration.
You also need to specify the back end in configuration property
longevity.backEnd. Your choices are currently
SQLite. We use
InMem out of the box. If you want to try Cassandra or MongoDB, you will need to set up a database system to connect to. The SQLite back end will work without any extra setup, as all you need to run SQLite is the right jar on your classpath.
Let's skip ahead to look at the Play routes. In a moment, we'll come back to our controller class to see how these routes are hooked up to the back-end repositories.
conf/routes file defines the Simple Blogging API for users and user profiles. The following routes are defined:
POST /users- creates a new user
GET /users- retrieves all the users
GET /users/$username- retrieves a single user
PUT /users/$username- updates an existing user
DELETE /users/$username- deletes an existing user
GET /users/$username/profile- retrieves a user profile
PUT /users/$username/profile- creates or updates a user profile
DELETE /users/$username/profile- deletes a user profile
Please note that the
GET /users endpoint will not work with a Cassandra persistence strategy, because Cassandra does not support unfiltered queries.
These routes are defined in the standard Play idiom, and we will not go into the details here. The important thing to note is that the work for each of these endpoints delegates to one of the methods in
UserController.scala, which we will turn to next.
UserController.scala, we find eight service methods here that mirror the eight . Each controller method is implemented asynchronously with Play method
Action.async. We use JSON body parsers, and play method
Json.toJson, to convert Scala case classes in and out of JSON.
An important thing to note here is that each of the controller methods is defined in terms of API classes
ProfileInfo.scala, and not in terms of the domain entities themselves. This is probably not necessary for such a simple application as this, but it's a good practice, because the UI typically speaks in a different language than the domain model. As a simple example, some user information, such as email or street address, should largely be considered private, and should be left out of most UI views.
As you can see,
ProfileInfo are simple case classes that should convert in and out of JSON cleanly. They also each contain a couple of methods for conversions between the API objects and the domain objects.
To do its job, the
UserController is going to have to access the user table in the database. It does this via the user repository, of type
Repo[User], that is injected into the controller by the Play framework. Because the repository methods need an execution context to run in, the Play dependency injection mechanism also provides an execution context.
There are a number of controller methods in
UserController. In this tutorial, we will focus on three:
The heart of the
UserController.createUser. is the call to
userRepo.create, inside the for comprehension.
userRepo.create returns a
Future[PState[User]]. The future is there because we want to treat the underlying database call in an asynchronous fashion. The
User is further wrapped in a
PState, or persistent state, which contains persistence information about the user that is not part of the domain model. You don't need to know much of anything about a
PState, except that you can call methods
map on it, to work with the underlying
In the yield clause of the for comprehension in this method,
created.get retrieves the
User from the
PState. This in turn is passed to a method that converts from a
User to a
UserInfo. We then convert this into JSON, and wrap it in a Play
Ok HTTP result.
One caveat here is that
userRepo.create might actually fail with a duplicate key exception. There might already be a user that has either the same username or email. So we call
recover on the resulting
Future and convert the longevity
DuplicateKeyValException into a
Conflict HTTP result.
UserController.retrieveUser does its work by calling
userRepo.retrieve. To call this method, we have to convert from the
username string to a
userRepo.retrieve takes a
KeyVal as argument.
Once again, the
User is wrapped in a
PState, so we can manipulate its persistent state if we wish. This in turn is wrapped in an
Option, as there may or may not be a user with that username. This in turn is wrapped in a
Future, as we want to treat the database call in an asynchronous fashion. This feels like a lot of layers of wrapping, but they are not too painful to work with if you use for comprehensions.
If no user was retrieved, we return
NotFound. When we do find a user, we need to convert it into a
UserInfo, convert that to JSON, and wrap it in an
Ok. This is done in two lines in the
yield clause of the for comprehension.
Let's take a look at
UserController.updateUser. This method shows a variation on
retrieveOne opens up the
Option for you, throwing a
NoSuchElementException if the
Option is empty. We handle the
NoSuchElementException in the
recover clause, returning
NotFound if the
User was not found.
retrieved in the for comprehension is a
retrieved.map produces another
PState[User] that reflects the changes produced by the function passed to
map. In this case, we call
UserInfo.mapUser, which updates a
User according to the information in the
UserInfo. The resulting
PState is stored in a local val named
We then pass
modified on to
userRepo.update. This method persists the changes, but like
userRepo.create, it might generate a
DuplicateKeyValException if we try to update the user to have a conflicting username or email. Once again, we handle this problem in the
recover clause, converting the longevity exception into a Simple Blogging service exception.
Of course, this API actually works. Feel free to play around with it with the tool of your choice. You could use a UNIX tool such as curl, or perhaps a Chrome plugin such as Postman or Advanced REST client. We have a slight preference towards the Advanced REST client at the moment. It is a little less quirky than Postman.
If you choose to use the Advanced REST client, we've exported our sample requests to arc-simbl-export.json. You can use this as a starting point. (You won't be able to view this file within Typesafe Activator, so we've provided a link to the raw file in GitHub.)
Before we wrap up, we'd like to point out a useful tool that you can pull out of the
RepoCrudSpec. This will test all of your CRUD operations for all of your persistent types against a test database. It's trivial to set up, as you can see in SimblRepoCrudSpec.scala. There's also a little framework for testing queries, and you can see an example of that in BlogPostQuerySpec.scala. You can run these for yourself using the
Test tab in the left margin, or by running
sbt test from the console.
While Simple Blogging is a working application, it has been developed for the purposes of this tutorial, and consequently, it is incomplete in a number of ways. As an exercise, you might try to enhance the application to fill in the gaps. We will be happy to consider any pull requests you make that fill in missing features. Here are some ideas for experimentations you might try:
Commentaggregate to the domain model.
Thank you very much for working through this tutorial! We hope you enjoy longevity as much as we do. If you would like to investigate further, please take a look at our user manual. Also, please write to our discussion forum to tell us about about your experience with longevity, or to ask any questions.