Home > stuff > Play 2.0 with Scala and Scaml, Part1: Setup of test infrastructure, model and persistence with Anorm

Play 2.0 with Scala and Scaml, Part1: Setup of test infrastructure, model and persistence with Anorm

In this series I’ll try to provide some help on topics I had problems with when I implemented my first Play 2.0 project. My goal is it to get a small version of the blogging engine of the 1.0 tutorial. I will not provide a complete tutorial, but only stuff that might be helpfull setting up the project.

Hint for all beginners with the 2.0 RC: One source of knowledge I was missing in the beginning is the 2.0 wiki, which is often enough quite useful.

Setting up IntelliJ

I setup IntelliJ as described in here. Everything worked except the server startup. To have it started you have to use a different main class: play.core.server.NettyServer. Server startup works like this, but the application is not loaded correctly. As this has no priority for me, I didn’t spend much time trying to solve this. If anyone knows what the problem is, please let me know.

The first model

In the tutorial three classes were defined: User, Post and Comment. From my perspective Post and User should be enough to cover all the basic topics and problems, so I’ll skip the comment. The definition of User and Post in scala is pretty easy. The Pk herein is needed as there is foreign key relationship between the tables as you’ll see later. It’s an optional parameters and thus most users will hardly ever notice that it’s there.

case class User(email: String, password: String, fullname: String, isAdmin: Boolean = false, id:Pk[Int] = NotAssigned)

case class Post(title: String,  posted: Date, content: String, author: User)

Evolutions

To persist the entities we’ll need some persistence. I will use the standard h2 in memory db. If the data is read and written mainly in whole entities by a PK, like here,  I’d normally prefer a NoSQL-DB like Mongo or Neo4J. But for the sake of simplicity I’ll stick with the standard DB here.

Play is using the concept of Evolutions for DBs. The basic idea of evolutions is to describe what must be executed to perform the evolution (Ups) and what must be done to undo the evolution (Downs). This is probably not the best and golden way, but a pretty good one especially for smaller projects. For a good wrap up about problems and solutions of DB changes have a look here.

For Post and User the first evolution might look like this and has to be defined in [PROJECT_HOME]/conf/evolutions/default/1.sql:

# --- First database schema
# --- !Ups
CREATE TABLE myuser (
id                            INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
email                         VARCHAR(255) NOT NULL,
password                      VARCHAR(255) NOT NULL,
fullname                      VARCHAR(255) ,
isAdmin                       BOOLEAN NOT NULL
);
CREATE TABLE posts(
title                         VARCHAR(255) NOT NULL,
posted                        DATE NOT NULL,
content                       VARCHAR(2550) NOT NULL,
authorId                      INT NOT NULL,
FOREIGN KEY                   (authorId) REFERENCES myuser(id)
);

# --- !Downs
DROP TABLE IF EXISTS myuser;
DROP TABLE IF EXISTS posts;

Implementing the persistence test driven with Anorm

In the standard tutorial the test mode is described, where one can open the @test url and see all tests. I haven’t found this in 2.0 yet. Right now I don’t bother too much, as I’ll run the tests from within the IDE. Anyway: If anyone knows how to get the web based tests running again, gimme a note.

I like to write test first. The implementation of integration tests for the persistence differs from version 1. Thus I’ll explain it in a little more detail. My first test for the persistence looked like this:


"The user class" should {
  "be persisted" in {
    running(FakeApplication()) {
      User.create(User("email", "pwd", null))
      User.findAll() must have size 1
    }
  }
}

I made some mistakes in the setup and needed quite a time to figure out what the problem was. One thing that really bothered me was, that the log of the test didn’t tell what the problem was. It just stated that the table MYUSER doesn’t exist. In my case there were 2 problems. First of all there was no evolution applied as I named the directory wrong. Second there was a syntax error in my SQL script. One of the real valuable benefits of test should be to get a hint where an error is located. To achieve this I wrote another test, that explicitly tested the application of the evolutions:

class DBEvolutionsTest extends Specification {

  "Evolutions" should {
    "be applied without errors" in {
      evolutionFor("default")
      running(FakeApplication()) {
        DB.withConnection {
          implicit connection =>
            SQL("select count(1) from myuser").execute()
            SQL("select count(1) from posts").execute()
        }
      }
      success
    }
  }
}

What happens here? First we tell the testing Helpers class that it should apply the evolutions for the DB default. The connection for this DB is defined in the application.conf (db.default.driver etc.) and the evolution as already described in the 1.sql, where the db name default is defined in the path. Then a fake application is created and a simple SQL is executed to check, that the table exists.

Written like this, logs and error messages have the verbosity I prefer. You can now see in your test log how the evolution is applied. If there is a syntax error it’ll be shown. If you haven’t put the evolution at the right place (as I did) you will at least see, that the evolution is not applied.

You might have noticed that I have one nullable column in my schema definition. Normally I’m not a great friend of null values and try to avoid them wherever possible. In this case I’d definitely make it not nullable too. But to have examples of all the “standard” cases, I thought it would be a good idea to have at least one nullable column in the example.

I’ll skip the rest of the tests as they are pretty straight forward from here on. The resulting User object used for persistence looked like this in the end. I used the SqlParser of Anorm to create a parser that could be used in the data access methods later on. As you can see here, the nullable column is PITA again. You’ll have to access it via an option and have to provide a value in case there is none given. Otherwise there will be an UnexpectedNullableFound exception be thrown. To be “consistent” I’ve choosen null to be set in User again .. argh.

The Pk of a new entity is set after insertion of a new User via SELECT SCOPE_IDENTITY(). I’m not sure if this is the way to go and if it is thread safe, but it works for this little example at least. The Pk definition is needed for the persistence of the posts again, as we have a foreign key relationship between the two.

object User  {
  val parser = {
      get[Pk[Int]]("id") ~
      get[String]("email") ~
      get[String]("password")~
      get[Option[String]]("fullname")~
      get[Boolean]("isAdmin") map {
      case pk ~ mail ~ name ~ fullname ~ isAdmin => User(mail, name, fullname.getOrElse(null), isAdmin, pk)
    }
  }

  def findBy(id: Pk[Int]): User = {
    DB.withConnection {
      implicit connection =>
        SQL("select * from myuser where id = {id}").on("id" -> id.get).using(parser).single()
    }
  }

  def findAll(): Seq[User] = {
    DB.withConnection {
      implicit connection =>
        SQL("select * from myuser").as(User.parser *)
    }
  }

  def create(user: User): User = {
    DB.withConnection {
      implicit connection =>
        SQL("insert into myuser(email, password, fullname, isadmin) values ({email}, {password}, {fullname}, {isAdmin});").on(
          'email -> user.email,
          'password -> user.password,
          'fullname -> user.fullname,
          'isAdmin -> user.isAdmin
        ).executeUpdate()
        val id = SQL("SELECT SCOPE_IDENTITY()")().collect {
          case Row(id: Int) => id
        }.head
        return User(user.email, user.password, user.fullname, user.isAdmin, new Id(id))
    }
  }
}

In the following definition of the post object you can see the why the Pk of the user is needed after creation.

object Post{
  val parser = {
      get[String]("title") ~
      get[Date]("posted") ~
      get[String]("content")~
      get[Pk[Int]]("authorId") map {
      case title ~ posted ~ content ~ author => Post(title,  posted, content, User.findBy(author))
    }
  }

  def findAll() = {
    DB.withConnection {
      implicit connection =>
        SQL("select * from posts").as(parser *)
    }
  }

  def create(post: Post): Unit = {
    DB.withConnection {
      implicit connection =>
        SQL("insert into posts(title, posted, content, authorId) values ({title}, {posted}, {content}, {authorId})").on(
          'title -> post.title,
          'posted -> post.posted,
          'content -> post.content,
          'authorId -> post.author.id
        ).executeUpdate()
    }
  }
}

Conclusion

Setup of the project and integration in the IDE worked like a charm. Same holds for setting up the test infrastructure if you have found where its documented. The docs will be made more visible when 2.0 is officially released, so no problem. I like the verbosity of the test helpers. It becomes really obvious what is happening, without making the tests too chatty. I really love the basic idea about the Anorm framework for database access. It’s clear and concise and again it becomes clear in the code what is happening. I’m not sure what I think about the lack of the Magic trait. Having to write words like password 7 times is a major PITA for me. I’ definitely love seeing it again in one of the upcoming versions.

About these ads
  1. No comments yet.
  1. No trackbacks yet.

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 )

Twitter picture

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

Facebook photo

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

Google+ photo

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

Connecting to %s

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: