Documentation

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

A first iteration for the data model

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 application). 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 contains 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 is an object oriented language, 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 by the User class

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

package models;
 
import java.util.*;
import javax.persistence.*;
 
import play.db.jpa.*;
 
@Entity
public class User extends Model {
 
    public String email;
    public String password;
    public String fullname;
    public boolean isAdmin;
    
    public User(String email, String password, String fullname) {
        this.email = email;
        this.password = password;
        this.fullname = fullname;
    }
 
}

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.

Now if you’re a Java developer with any experience at all, warning sirens are probably clanging like mad at the sight of a public variable. In Java (as in other object-oriented languages), best practice says to make all fields private and provide accessors and mutators. This is to promote encapsulation, a concept critical to object oriented design. In fact, play takes care of that for you and automatically generates getters and setters while preserving encapsulation; we will see how it works later in this tutorial.

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

import org.junit.*;
import play.test.*;
import models.*;
 
public class BasicTest extends UnitTest {
 
    @Test
    public void 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
public void createAndRetrieveUser() {
    // Create a new user and save it
    new User("[email protected]", "secret", "Bob").save();
    
    // Retrieve the user with bob username
    User bob = User.find("byEmail", "[email protected]").first();
    
    // Test 
    assertNotNull(bob);
    assertEquals("Bob", bob.fullname);
}

As you can see, the Model superclass gives us two first very useful methods : save() and find(). Select the BasicTests.java in the test runner, click start and check that all is green.

We will need a method on the User class that checks if a user with a specified username and password exists. Let’s write it and test it.

In the User.java source, add the connect() method:

public static User connect(String email, String password) {
    return find("byEmailAndPassword", email, password).first();
}

And now the test case:

@Test
public void 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
public class Post extends Model {
 
    public String title;
    public Date postedAt;
    
    @Lob
    public String content;
    
    @ManyToOne
    public User author;
    
    public Post(User author, String title, String content) {
        this.author = author;
        this.title = title;
        this.content = content;
        this.postedAt = new Date();
    }
 
}

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
    public void 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
public void createPost() {
    // Create a new user and save it
    User bob = 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(1, Post.count());
    
    // Retrieve all post created by bob
    List<Post> bobPosts = Post.find("byAuthor", bob).fetch();
    
    // Tests
    assertEquals(1, bobPosts.size());
    Post firstPost = bobPosts.get(0);
    assertNotNull(firstPost);
    assertEquals(bob, firstPost.author);
    assertEquals("My first post", firstPost.title);
    assertEquals("Hello world", firstPost.content);
    assertNotNull(firstPost.postedAt);
}

Don’t forget to import the java.util.List or will get a compilation error.

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
public class Comment extends Model {
 
    public String author;
    public Date postedAt;
     
    @Lob
    public String content;
    
    @ManyToOne
    public Post post;
    
    public Comment(Post post, String author, String content) {
        this.post = post;
        this.author = author;
        this.content = content;
        this.postedAt = new Date();
    }
 
}

Let’s write a first test case:

@Test
public void postComments() {
    // Create a new user and save it
    User bob = new User("[email protected]", "secret", "Bob").save();
 
    // Create a new post
    Post bobPost = 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
    List<Comment> bobPostComments = Comment.find("byPost", bobPost).fetch();
 
    // Tests
    assertEquals(2, bobPostComments.size());
 
    Comment firstComment = bobPostComments.get(0);
    assertNotNull(firstComment);
    assertEquals("Jeff", firstComment.author);
    assertEquals("Nice post", firstComment.content);
    assertNotNull(firstComment.postedAt);
 
    Comment secondComment = bobPostComments.get(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=CascadeType.ALL)
public List<Comment> comments;
 
public Post(User author, String title, String content) { 
    this.comments = new ArrayList<Comment>();
    this.author = author;
    this.title = title;
    this.content = content;
    this.postedAt = new Date();
}
...

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 setted 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:

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

Let’s write another test case to check that:

@Test
public void useTheCommentsRelation() {
    // Create a new user and save it
    User bob = new User("[email protected]", "secret", "Bob").save();
 
    // Create a new post
    Post bobPost = 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(1, User.count());
    assertEquals(1, Post.count());
    assertEquals(2, Comment.count());
 
    // Retrieve Bob's post
    bobPost = Post.find("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();
    
    // Chech the all comments have been deleted
    assertEquals(1, User.count());
    assertEquals(0, Post.count());
    assertEquals(0, Comment.count());
}

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
public void fullTest() {
    Fixtures.load("data.yml");
 
    // Count things
    assertEquals(2, User.count());
    assertEquals(3, Post.count());
    assertEquals(3, Comment.count());
 
    // 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
    List<Post> bobPosts = Post.find("author.email", "[email protected]").fetch();
    assertEquals(2, bobPosts.size());
 
    // Find all comments related to bob posts
    List<Comment> bobComments = Comment.find("post.author.email", "[email protected]").fetch();
    assertEquals(3, bobComments.size());
 
    // Find the most recent post
    Post frontPost = Post.find("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(4, Comment.count());
}

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.