Play 2.4 supports Compile Time Dependency Injection. This post describes how to inject your own Cassandra repository object into a controller at compile time, while also initializing and closing a Cassandra connection session during application startup and shutdown, respectively.
The code of the final application is available at https://github.com/Galeria-Kaufhof/play2-compiletime-cassandra-di.
At the end of this post, we have created a small Play 2.4.6 Scala application with which will be able to serve the name of a product with a given id by reading information from a Cassandra database. We will be able to verify the correct behaviour of our application using a real database with an integration test and, because we will be able to mock the repository that is injected into the controller, we will also be able to verify the correct application behaviour without using a database.
The resulting code will be runnable and realistic, but also a bit simplistic - lacking error handling, for example - in order to be tractable.
This post is aimed at readers who have already written Scala applications with Play2 and know how to work with sbt and Cassandra.
In order to compile and run the code, you need a Java 8 SE Development Kit, and you need a recent version of sbt.
Last but not least, you need to set up a Cassandra cluster - a one-node local setup is sufficient for the application that we'll create.
The post is written from the perspective of a Mac OS X system user with Homebrew installed, but should be adaptable for any Scala-capable environment with minor modifications.
Let's start by creating an sbt-based Play 2.4 project using the Typesafe Activator, which we install via Homebrew: brew install typesafe-activator
.
We can then use Activator to set up the Play2 project: activator new play2-compiletime-cassandra-di play-scala
.
The first thing to do now is to switch from specs2 to ScalaTest as our testing framework, as described in Play2: Switching from specs2 to ScalaTest. Please change files build.sbt
, test/ApplicationSpec.scala
, and test/IntegrationSpec.scala
as described there.
Running sbt test
afterwards should just work. At this point, your codebase should look like the reference repository at 3a96b61.
We are now going to integrate the Datastax Java Driver for Apache Cassandra, roughly following the steps outlined in Setting up a Scala sbt multi-project with Cassandra connectivity and migrations (but without tests and the migrations stuff to keep the codebase small for this post).
This means adding the Cassandra driver as a dependency to file build.sbt
, creating a utility class for connection URIs in file app/cassandra/CassandraConnectionUri.scala
, and adding an object that handles database connections in file app/cassandra/CassandraConnector.scala
. See the resulting codebase on GitHub at 0fa30e4 or view the differences from the previous version of the codebase.
With this, we can finally start to work on the actual Cassandra repository and learn how to inject it into a controller.
Again, I'm trying to keep things simple and the codecase lean. We will inject the repository into the existing Application
controller in file app/controllers/Application.scala
.
Our repository has only one job: It allows controllers to retrieve exactly one row from the Cassandra table it manages using a single-column primary key. Imagine we have a Cassandra table called products, with the following structure:
+----+-------+
| id | name |
+----+-------+
| 1 | Chair |
| 2 | Fork |
| 3 | Lamp |
+----+-------+
We can create the according table structure (and its keyspace) as follows:
CREATE KEYSPACE IF NOT EXISTS test WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 };
USE test;
CREATE TABLE products (id INT PRIMARY KEY, name TEXT);
Let's make some assumptions: We expect our repository to provide a method getOneById(id: Int): ProductModel
. Given a product id, the repository takes care of the heavy lifting that is required to return a ProductModel
object which carries the data for this product as retrieved from the database.
We can start by declaring the model. Its a simple case class that lives in app/models/ProductModel.scala
:
package models
case class ProductModel(id: Int, name: String)
Next, we start building a repository structure which keeps the API towards clients of repositories abstract, and allows to create concrete implementations that work with a Cassandra database.
The first step is to define a trait that all repositories, be it concrete Cassandra-based implementations or lightweight mocks in test, will share. To do so, we put the following in file app/repositories/Repository.scala
:
package repositories
abstract trait Repository[M, I] {
def getOneById(id: I): M
}
Simple enough. This ensures that repository implementations will provide a getOneById
method, and type parametrization allows to declare the type of parameter id and the type of the model object that has to be returned.
We know that we have to query the repository for integer values, and we know that we want to retrieve a ProductModel
in return. Thus, we can already declare the fact that our Application controller depends on such a repository, in file app/controllers/Application.scala
:
package controllers
import models.ProductModel
import play.api._
import play.api.mvc._
import repositories.Repository
class Application(productsRepository: Repository[ProductModel, Int]) extends Controller {
def index = Action {
Ok(views.html.index("Your new application is ready."))
}
}
At this point (commit d58d681 in the repo, diff), the application can no longer run and the existing test cases fail, because we have not yet implemented the mechanisms needed to actually inject the declared dependency into the controller. We will fix this later - first, we write a generic Cassandra repository implementation and use it to create a concrete implementation for the Repository[ProductModel, Int]
type.
To do so, we create an abstract CassandraRepository
class that does the heavy lifting, in file app/repositories/CassandraRepository.scala
:
package repositories
import com.datastax.driver.core.querybuilder.QueryBuilder
import com.datastax.driver.core.querybuilder.QueryBuilder._
import com.datastax.driver.core.{Row, Session}
abstract class CassandraRepository[M, I](session: Session, tablename: String, partitionKeyName: String)
extends Repository[M, I] {
def rowToModel(row: Row): M
def getOneRowBySinglePartitionKeyId(partitionKeyValue: I): Row = {
val selectStmt =
select()
.from(tablename)
.where(QueryBuilder.eq(partitionKeyName, partitionKeyValue))
.limit(1)
val resultSet = session.execute(selectStmt)
val row = resultSet.one()
row
}
override def getOneById(id: I): M = {
val row = getOneRowBySinglePartitionKeyId(id)
rowToModel(row)
}
}
Some notes on this class: in addition to the type parameters for the model and id field, a concrete class that extends this abstract CassandraRepository needs to provide a session representing the connection to a cassandra cluster, the name of the table that is to be wrapped, and the name of the field that is to be queried via getOneById. Furthermore, implementations must override the rowToModel
method, because only a concrete implementation knows how to create a valid model from the values of a table row.
Finally, getOneRowBySinglePartitionKeyId
is where stuff happens: using the session and the means provided by the DataStax driver, the database is queried and the resulting row is returned. Because CassandraRepository extends the Repository trait, it must override getOneById
- in this case, that's simply a matter of retrieving the row using the code from the abstract class itself, and transforming it into a model using the to-be-overridden rowToModel method.
With this in place, the concrete implementation of a Cassandra-backed ProductsRepository
class that matches the Repository[ProductModel, Int]
type looks like this, in file app/repositories/ProductsRepository.scala
:
package repositories
import com.datastax.driver.core.{Row, Session}
import models.ProductModel
class ProductsRepository(session: Session)
extends CassandraRepository[ProductModel, Int](session, "products", "id") {
override def rowToModel(row: Row): ProductModel = {
ProductModel(
row.getInt("id"),
row.getString("name")
)
}
}
With these changes in place (commit cb7a9b4 in the repo, diff), we can approach the injection. How can we control the way the Application
controller is created, i.e. how can we control compile time dependency injection? In Play 2.4, this happens by providing a class that extends the play.api.ApplicationLoader
trait.
To do so, create file app/AppLoader.scala
with the following content:
import components.CassandraRepositoryComponents
import play.api.ApplicationLoader.Context
import play.api.routing.Router
import play.api.{Application, ApplicationLoader, BuiltInComponentsFromContext}
import router.Routes
class AppLoader extends ApplicationLoader {
override def load(context: ApplicationLoader.Context): Application =
new AppComponents(context).application
}
class AppComponents(context: Context) extends BuiltInComponentsFromContext(context) with CassandraRepositoryComponents {
lazy val applicationController = new controllers.Application(productsRepository)
lazy val assets = new controllers.Assets(httpErrorHandler)
override def router: Router = new Routes(
httpErrorHandler,
applicationController,
assets
)
}
Play2 is not able to find out about this AppLoader itself, which is why we need to configure it in file conf/application.conf
by adding the line play.application.loader="AppLoader"
.
As you can see, we are overriding the part of Play2 that creates a runnable play.api.Application
by instantiating the Application controller ourselves (while injecting the products repository, more on this later), and by overriding router creation. In order to get access to a products repository instance, the AppComponents class extends the CassandraRepositoryComponents
trait. Within this trait, we connect to the database, set up the repository, and hook into the application lifecycle in order to shut down the database connection when the application shuts down. The code for all this goes into app/components/CassandraRepositoryComponents.scala
:
package components
import cassandra.{CassandraConnector, CassandraConnectionUri}
import com.datastax.driver.core.Session
import models.ProductModel
import play.api.inject.ApplicationLifecycle
import play.api.{Configuration, Environment, Mode}
import repositories.{Repository, ProductsRepository}
import scala.concurrent.Future
trait CassandraRepositoryComponents {
// These will be filled by Play's built-in components; should be `def` to avoid initialization problems
def environment: Environment
def configuration: Configuration
def applicationLifecycle: ApplicationLifecycle
lazy private val cassandraSession: Session = {
val uriString = environment.mode match {
case Mode.Test => "cassandra://localhost:9042/test"
case _ => "cassandra://localhost:9042/prod"
}
val session: Session = CassandraConnector.createSessionAndInitKeyspace(
CassandraConnectionUri(uriString)
)
// Shutdown the client when the app is stopped or reloaded
applicationLifecycle.addStopHook(() => Future.successful(session.close()))
session
}
lazy val productsRepository: Repository[ProductModel, Int] = {
new ProductsRepository(cassandraSession)
}
}
As you can see, this approach also allows to adapt to the application environment: In our case, we connect to a different Cassandra cluster URI if we are running in the test environment.
At this point (commit 4e707cb, diff, dependency injection is in place and the application is runnable again.
What still doesn't run, however, are the tests. Also, it's a bit sad that we did all those things that give our controller a repository, and then it doesn't even use it. Let's fix both issues.
We start with the integration spec in file test/IntegrationSpec.scala
. We are going to extend this quite a bit:
import java.io.File
import cassandra.{CassandraConnector, CassandraConnectionUri}
import org.scalatest.BeforeAndAfter
import org.scalatestplus.play._
import play.api
import play.api.{Mode, Environment, ApplicationLoader}
class IntegrationSpec extends PlaySpec with OneBrowserPerSuite with OneServerPerSuite with HtmlUnitFactory with BeforeAndAfter {
before {
val uri = CassandraConnectionUri("cassandra://localhost:9042/test")
val session = CassandraConnector.createSessionAndInitKeyspace(uri)
val query = "INSERT INTO products (id, name) VALUES (1, 'Chair');"
session.execute(query)
session.close()
}
override implicit lazy val app: api.Application =
new AppLoader().load(
ApplicationLoader.createContext(
new Environment(
new File("."), ApplicationLoader.getClass.getClassLoader, Mode.Test)
)
)
"Application" should {
"work from within a browser and tell us about the first product" in {
go to "http://localhost:" + port
pageSource must include ("Your new application is ready. The name of product #1 is Chair.")
}
}
}
We need to override the implicit app
value, where we ask our new AppLoader to create an application for the Test environment. Furthermore, we extend our specification and now expect our app to not only greet us, but to also tell us about the name of the product with ID 1, which we insert in the new before step of our specification.
We could do the same with the ApplicationSpec, but let's go one step further and mock the ProductRepository, which gives us a specification that in contrast to the integration spec doesn't need an actual Cassandra database to work, and only verifies the behaviour of the Application controller itself, not its dependencies. To do so, we change file test/ApplicationSpec.scala
as follows:
import java.io.File
import models.ProductModel
import play.api
import play.api.{Mode, Environment, ApplicationLoader}
import play.api.ApplicationLoader.Context
import play.api.test._
import play.api.test.Helpers._
import org.scalatestplus.play._
import repositories.Repository
class MockProductsRepository extends Repository[ProductModel, Int] {
override def getOneById(id: Int): ProductModel = {
ProductModel(1, "Mocked Chair")
}
}
class FakeApplicationComponents(context: Context) extends AppComponents(context) {
override lazy val productsRepository = new MockProductsRepository()
}
class FakeAppLoader extends ApplicationLoader {
override def load(context: Context): api.Application =
new FakeApplicationComponents(context).application
}
class ApplicationSpec extends PlaySpec with OneAppPerSuite {
override implicit lazy val app: api.Application = {
val appLoader = new FakeAppLoader
appLoader.load(
ApplicationLoader.createContext(
new Environment(
new File("."), ApplicationLoader.getClass.getClassLoader, Mode.Test)
)
)
}
"Application" should {
"send 404 on a bad request" in {
val Some(wrongRoute) = route(FakeRequest(GET, "/boum"))
status(wrongRoute) mustBe NOT_FOUND
}
"render the index page and tell us about the first product" in {
val Some(home) = route(FakeRequest(GET, "/"))
status(home) mustBe OK
contentType(home) mustBe Some("text/html")
contentAsString(home) must include ("Your new application is ready. The name of product #1 is Mocked Chair.")
}
}
}
</pre>
Of course, both specs will only pass if we change the behaviour of the Application controller in file app/controllers/Application.scala
and make us of the injected repository:
package controllers
import models.ProductModel
import play.api._
import play.api.mvc._
import repositories.Repository
class Application(productsRepository: Repository[ProductModel, Int]) extends Controller {
def index = Action {
val product = productsRepository.getOneById(1)
Ok(views.html.index(s"Your new application is ready. The name of product #1 is ${product.name}."))
}
}
And that's it. At commit 796b6c6, (see the diff), we have a working Play 2.4 app with a compile time injected Cassandra repository.
Who are we and what do we do at Galeria Kaufhof and HBC Europe? We are a passionate and highly motivated team of developers located in Cologne, Germany. We are whole-heartedly committed to modern project workflows, agile ideas and open-source software. We are constantly improving our services to satisfy our beloved customers. We promote openness and always love to share our technology findings with the world. Sounds good? Come and join us!