Lightbend Activator

Longevity Play Tutorial

Longevity Play Tutorial

longevityframework
Source
March 25, 2017
longevity play database nosql scala starter

longevity is a Persistence Framework for Scala and NoSQL

How to get "Longevity Play Tutorial" on your computer

There are several ways to get this template.

Option 1: Choose activator-longevity-play-tutorial in the Lightbend Activator UI.

Already have Lightbend Activator (get it here)? Launch the UI then search for activator-longevity-play-tutorial in the list of templates.

Option 2: Download the activator-longevity-play-tutorial project as a zip archive

If you haven't installed Activator, you can get the code by downloading the template bundle for activator-longevity-play-tutorial.

  1. Download the Template Bundle for "Longevity Play Tutorial"
  2. Extract the downloaded zip file to your system
  3. The bundle includes a small bootstrap script that can start Activator. To start Lightbend Activator's UI:

    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:

     C:\Users\typesafe\activator-longevity-play-tutorial> activator ui 
    This will start Lightbend Activator and open this template in your browser.

Option 3: Create a activator-longevity-play-tutorial project from the command line

If you have Lightbend Activator, use its command line mode to create a new project from this template. Type activator new PROJECTNAME activator-longevity-play-tutorial on the command line.

Option 4: View the template source

The creator of this template maintains it at https://github.com/longevityframework/simbl-play#master.

Option 5: Preview the tutorial below

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.

Preview the tutorial

Getting Started with longevity

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.

Modeling our Domain

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:

Building the User Aggregate

The user has four parts: the User, the UserProfile, and two natural keys: the Username and the Email. Let's focus on the User first. You can find the source code for User here

The User case class provides us with the four members we find in the UML in the previous section, including the relationship between User and UserProfile. There are also a couple of business methods inside: updateProfile and deleteProfile.

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 @persistent annotation.

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 User fields username and email with properties User.props.username and User.props.email.

We use keySet parameter on the @persistent annotation to tell longevity about our keys. We define keys on the username and email fields, specifying that these two member are to be unique: no two users should have the same username or email.

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

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 User.

The UserProfile has two members that are also persistent components: Uri and 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.

Username and Email

The final components of our user aggregate are Username and Email, which are the key values for our natural keys, User.keys.username and 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.

Building the Domain Model

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 @domainModel annotation.

Building the Longevity Context

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 Cassandra, InMem, Mongo, and 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.

The Play Routes

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.

These routes define an application API that might be used by a JavaScript application front-end. We haven't had the time to actually write a front end yet. If you would like to give it a shot, we would happily consider any pull requests!

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.

The User Controller

In UserController.scala, we find eight service methods here that mirror the eight user routes. 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 UserInfo.scala and 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, UserInfo and 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: createUser, retrieveUser, and updateUser.

UserController.createUser

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 get and map on it, to work with the underlying User inside.

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

UserController.retrieveUser does its work by calling userRepo.retrieve. To call this method, we have to convert from the username string to a Username, as 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.

UserController.updateUser

Let's take a look at UserController.updateUser. This method shows a variation on userRepo.retrieve: userRepo.retrieveOne. 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.

The retrieved in the for comprehension is a PState[User]. Calling 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 modified.

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.

Exercising the API

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.)

Testing CRUD Operations

Before we wrap up, we'd like to point out a useful tool that you can pull out of the LongevityContext: 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.

Exercises for the Reader

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:

  • Add a Comment aggregate to the domain model.
  • Put in service methods and routes for BlogPost and Blog.
  • Write unit tests for the Play routes.
  • Write a simple UI that uses the backing API.

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.

Happy coding!