Documentation

You are viewing the documentation for Play 1. The documentation for Play 2 is here.

A first iteration for the data model (Scala version)

Here we will start to write the model for our blog engine.

Introduction to JPA

The model layer has a central position in a play application (and in fact in all well designed applications). It is the domain-specific representation of the information on which the application operates. As we want to create a blog engine, the model layer will certainly contain classes like User, Post and Comment.

Because most model objects need to survive between application restarts, we have to save them in a persistent datastore. A common choice is to use a relational database. But because Java and Scala are object oriented languages, we will use an 'Object Relational Mapper' to help reduce the impedance mismatch.

JPA is a Java specification that defines a standard API for object relational mapping. As implementation of JPA, play uses the well-known Hibernate framework. One advantage of using JPA over the standard Hibernate API is that all the ‘mapping’ is declared directly in the Java objects.

If you have ever used Hibernate or JPA before you will be surprised by the simplicity added by play. No need to configure anything; JPA just works out of the box with play.

If you don’t know JPA, you can read some of these simple presentations before continuing.

Starting with the User class

We will start to code the blog engine by creating the User class. Create a new file /yabe/app/models/User.scala, and declare a first implementation of the User class:

package models
 
import java.util._
import javax.persistence._
 
import play.db.jpa._
 
@Entity
class User(
    var email: String,
    var password: String,
    var fullname: String
) extends Model {
 
    var isAdmin: Boolean = false
 
    def this() = this(null, null, null)
}

If you’re new to Scala then this class definition might look a bit strange. What is happening here is that the definition of 3 fields is combined with the definition of the primary constructor. Then there is a fourth field which is set to false by default.

Scala really takes care of a lot of boilerplate, here’s what this code represents:

The @Entity annotation marks this class as a managed JPA entity, and the Model superclass automatically provides a set of useful JPA helpers that we will discover later. All fields of this class will be automatically persisted to the database.

It’s not required that your model objects extend the play.db.jpa.Model class. You can work as plain JPA as well. But extending this class is a good choice in most cases as it will ease a lot the JPA stuff.

If you have already used JPA before, you know that every JPA entity must provide an @Id property. Here the Model superclass provides an automatically generated numeric id, in most cases this is good enough.

Don’t think about this provided id field as a functional identifier but as a technical identifier. It is generally a good idea to keep both concepts separated and to keep an automatically generated numeric id as technical identifier.

You can now refresh the application homepage, to see the result. In fact, unless you made a mistake, you should not see any change: play has automatically compiled and loaded the User class, but this does not provide any feature to the application.

Writing the first test

A good way to test the newly created User class is to write a JUnit test case. It will allow to incrementally complete the application model and ensure that all is fine.

To run a test case, you need to start the application in a special ‘test’ mode. Stop the currently running application, open a command line and type:

~$ play test

The 'play test' command is almost the same than 'play run', except that it loads a test runner module that allows to run test suite directly from a browser.

When you run a play application in test mode, play will automatically switch to the test framework id and load the application.conf file accordingly. Check this page for more informations.

Open a browser to the http://localhost:9000/@tests URL to see the test runner. Try to select all the default tests and run them; all should be green... But these default tests don’t really test anything.

To test the model part of the application we will use a JUnit test. As you see a default BasicTests.java already exists, so let’s open it (/yabe/test/BasicTest.scala):

import org.junit._
import org.junit.Assert._
import play.test._
import play.db.jpa.QueryFunctions._
import models._
 
class BasicTest extends UnitTest {
 
    @Test
    def aVeryImportantThingToTest() {
    	
        assertEquals(2, 1 + 1)
    }
 
}

Remove the useless default test (aVeryImportantThingToTest) and create a test that tries to create a new user and retrieve it:

@Test
def createAndRetrieveUser() {
    // Create a new user and save it
    new User("[email protected]", "secret", "Bob").save()
 
    // Retrieve the user with bob's email address
    val bob = find[User]("byEmail", "[email protected]").first
 
    // Test 
    assertNotNull(bob)
    assertEquals("Bob", bob.fullname)
}

As you can see, the Model superclass gives us the very useful save() method and the QueryFunctions import gives us the find function.
Select BasicTests in the test runner, click start and check that all is green.

We will need a method on User that checks if a user with a specified username and password exists. But in Scala, methods that don’t act on a specific instance need to be defined in a companion object. Let’s write it and test it.

In the User.scala source, add an import statement (either at the top or just before the new code), the User object definition and the connect() method:

import play.db.jpa.QueryFunctions._
 
object User {
    def connect(email: String, password: String) = {
        find[User]("byEmailAndPassword", email, password).first
    }
}

And now the test case:

@Test
def tryConnectAsUser() {
    // Create a new user and save it
    new User("[email protected]", "secret", "Bob").save()
 
    // Test 
    assertNotNull(User.connect("[email protected]", "secret"))
    assertNull(User.connect("[email protected]", "badpassword"))
    assertNull(User.connect("[email protected]", "secret"))
}

Each time you make a modification you can run all the tests from the play test runner to make sure you didn’t break anything.

The Post class

The Post class will represent blog posts. Let’s write a first implementation:

package models
 
import java.util._
import javax.persistence._
import play.db.jpa._
 
@Entity
class Post(
    @ManyToOne
    var author: User,
    var title: String,
    @Lob
    var content: String
) extends Model {
 
    var postedAt: Date = new Date()
 
    def this() = this(null, null, null)
}

Here we use the @Lob annotation to tell JPA to use a large text database type to store the post content. We have declared the relation to the User class using @ManyToOne. That means that each Post is authored by a single User, and that each User can author several Posts.

We will write a new test case to check that the Post class works as expected. But before to write more tests, we need to do something in the JUnit class. In the current test, the database content is never deleted, each new run creating more and more objects. It will become problematic soon when more advanced test will start to count objects to check that all is fine.

So let’s write a JUnit setup() method to delete the database before each test:

public class BasicTest extends UnitTest {
 
    @Before
    def setup() {
        Fixtures.deleteAll()
    }
    
    ...
 
}

The @Before concept is a core concept of the JUnit testing tool.

As you can see, the Fixtures class is a helper to deal with your database during tests. Run the test again to check that you don’t have broken anything, and start to write the next test:

@Test
def createPost() {
    // Create a new user and save it
    val bob: User = new User("[email protected]", "secret", "Bob").save()
 
    // Create a new post
    new Post(bob, "My first post", "Hello world").save()
 
    // Test that the post has been created
    assertEquals(1L, count[Post])
 
    // Retrieve all post created by bob
    val bobPosts = find[Post]("byAuthor", bob).fetch
 
    // Tests
    assertEquals(1, bobPosts.length)
    val firstPost = bobPosts.head
    assertNotNull(firstPost)
    assertEquals(bob, firstPost.author)
    assertEquals("My first post", firstPost.title)
    assertEquals("Hello world", firstPost.content)
    assertNotNull(firstPost.postedAt)
}

Finish with Comment

The last thing that we need to add at this first model draft is the ability to attach comments to posts.

The creation of the Comment class is pretty straightforward.

package models
 
import java.util._
import javax.persistence._
import play.db.jpa._
 
@Entity
class Comment(
    @ManyToOne
    var post: Post,
    var author: String,
    @Lob
    var content: String
) extends Model {
 
    var postedAt: Date = new Date()
 
    def this() = this(null, null, null)
}

Let’s write a first test case:

@Test
def postComments() {
    // Create a new user and save it
    val bob: User = new User("[email protected]", "secret", "Bob").save()
 
    // Create a new post
    val bobPost: Post = new Post(bob, "My first post", "Hello world").save()
 
    // Post a first comment
    new Comment(bobPost, "Jeff", "Nice post").save()
    new Comment(bobPost, "Tom", "I knew that !").save()
 
    // Retrieve all comments
    val bobPostComments = find[Comment]("byPost", bobPost).fetch
 
    // Tests
    assertEquals(2, bobPostComments.length)
 
    val firstComment = bobPostComments(0)
    assertNotNull(firstComment)
    assertEquals("Jeff", firstComment.author)
    assertEquals("Nice post", firstComment.content)
    assertNotNull(firstComment.postedAt)
 
    val secondComment = bobPostComments(1)
    assertNotNull(secondComment)
    assertEquals("Tom", secondComment.author)
    assertEquals("I knew that !", secondComment.content)
    assertNotNull(secondComment.postedAt)
}

You see that the navigation between Post and Comments is not very easy: we need to use a query to retrieve all comments attached to Post. We can do better by setting up the other side of the relationship to the Post class.

Add the comments field to the Post class:

...
    @OneToMany(mappedBy="post", cascade=Array(CascadeType.ALL))
    var comments: List[Comment] = new ArrayList[Comment]
...

Note how we have used the mappedBy attribute to tell JPA that the Post class maintains the relationship. When you define bi-directional relation with JPA it is very important to tell which side will maintain the relationship. In this case, since the Comments belong to the Post it’s better that the Comment class maintains the relationship.

We have set the casacade property to tell JPA that we want that the Post deletion be cascaded to comments. This way, if you delete a post, all related comments will be deleted as well.

With this new relationship, we will add a helper method to the Post class to simplify adding comments:

def addComment(author: String, content: String): Post = {
    val newComment: Comment = new Comment(this, author, content).save()
    this.comments.add(newComment)
    return this
}

Let’s write another test case to check that:

@Test
def useTheCommentsRelation() {
    // Create a new user and save it
    val bob: User = new User("[email protected]", "secret", "Bob").save()
 
    // Create a new post
    var bobPost: Post = new Post(bob, "My first post", "Hello world").save()
 
    // Post a first comment
    bobPost.addComment("Jeff", "Nice post")
    bobPost.addComment("Tom", "I knew that !")
 
    // Count things
    assertEquals(1L, count[User])
    assertEquals(1L, count[Post])
    assertEquals(2L, count[Comment])
 
    // Retrieve Bob's post
    bobPost = find[Post]("byAuthor", bob).first
    assertNotNull(bobPost)
 
    // Navigate to comments
    assertEquals(2, bobPost.comments.size())
    assertEquals("Jeff", bobPost.comments.get(0).author)
 
    // Delete the post
    bobPost.delete();
 
    // Check the all comments have been deleted
    assertEquals(1L, count[User])
    assertEquals(0L, count[Post])
    assertEquals(0L, count[Comment])
}

Is it green?

Using Fixtures to write more complicated test

When you start to write more complex tests, you often need a set of data to test on. Fixtures lets you describe your model in a YAML file and load it at any time before a test.

Edit the /yabe/test/data.yml file and start to describe a User:


User(bob):
    email: [email protected]
    password: secret
    fullname: Bob
 
...
 

Well, because the data.yml file is a litle big, you can download it here.

Now we create create a test case that loads this data and run some assertions over it:

@Test
def fullTest() {
    Fixtures.load("data.yml")
 
    // Count things
    assertEquals(2L, count[User])
    assertEquals(3L, count[Post])
    assertEquals(3L, count[Comment])
 
    // Try to connect as users
    assertNotNull(User.connect("[email protected]", "secret"))
    assertNotNull(User.connect("[email protected]", "secret"))
    assertNull(User.connect("[email protected]", "badpassword"))
    assertNull(User.connect("[email protected]", "secret"))
 
    // Find all bob posts
    val bobPosts = find[Post]("author.email", "[email protected]").fetch
    assertEquals(2, bobPosts.length)
 
    // Find all comments related to bob posts
    val bobComments = find[Comment]("post.author.email", "[email protected]").fetch
    assertEquals(3, bobComments.length)
 
    // Find the most recent post
    val frontPost = find[Post]("order by postedAt desc").first
    assertNotNull(frontPost)
    assertEquals("About the model layer", frontPost.title)
 
    // Check that this post has two comments
    assertEquals(2, frontPost.comments.size())
 
    // Post a new comment
    frontPost.addComment("Jim", "Hello guys")
    assertEquals(3, frontPost.comments.size())
    assertEquals(4L, count[Comment])
}

Save your work

We have now finished an huge part on the blog engine. With all this things created and tested, we can now start to develop the web application itself.

But before continuing, it’s time to save your work in bazaar. Open a command line an type bzr st to see the modifications made since the latest commit:

$ bzr st

As you can see, some new files are not under version control. The test-result folder doesn’t need to be versioned, so let’s ignore it.

$ bzr ignore test-result

Add other files to version control using bzr add.

$ bzr add

You can now commit your project.

$ bzr commit -m "The model layer is ready"

Go to the next part.