[翻譯]Play框架1.2.7版本教程(2) - 數據模型的首次迭代

數據模型的首次迭代

接下來咱們要開始完成咱們的博客引擎的模型部分。html

JPA入門

模型層是一個Play應用的核心(對於其餘Web框架也一樣成立)。它是一個對應用操做的資源的領域特定的表示。由於咱們想要建立一個博客引擎,模型層就包括User,Post和Comment(用戶,博文和評論)。java

由於大多數模型對象須要在應用中止運行時保留下來,咱們須要把它們存儲在持久性數據庫中。一個廣泛的選擇是使用關係型數據庫。由於Java是一個面向對象的語言,咱們將使用一個ORM來減小一些繁瑣的工做。數據庫

JPA是一個給ORM定義一套標準API的Java規範。做爲一個JPA的實現,Play使用猿媛皆知的Hibernate框架。之因此使用JPA而不是原生的Hibernate API,是由於這樣全部的映射均可以用Java對象直接完成。segmentfault

若是以前用過Hibernate或JPA,你將驚訝於Play所添加的包裝。再也不須要配置什麼了;JPA與Play框架合一。瀏覽器

若是你不知道JPA,你能夠在繼續以前閱讀一些JPA實現的介紹oracle

User類

咱們首先來完成User類。建立新文件/yabe/app/models/User.java,並寫入下面的內容:app

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;
    }

}

@Entity註解(annotation)標記該類成爲託管的JPA實體(managed JPA Entity),而Model父類將自動提供一些接下來將會用到的有用的JPA輔助函數。這個類的全部成員變量都會被持久化到數據庫中。框架

默認狀況下,對應的表就是'User'。若是想要使用一個'user'是保留關鍵字的數據庫,你須要給JPA映射指定一個不一樣的表名。要想這麼作,使用@Table(name="blog_user")註解User類。ide

你的模型對象不必定得繼承自play.db.jpa.Model類。你也可使用原生JPA。但繼承自該類每每是個更好的選擇,由於它使得運用JPA變得更爲簡單。函數

若是以前用過JPA,你知道每一個JPA實體都須要提供一個@Id屬性。在這裏,Model父類已經提供了一個自動生成的ID,在大多數狀況下,這樣就好了。

不要認爲生成的id成員變量是函數變量(functional identifier),其實它是技術變量(technical identifier)。區分這兩概念一般是個好主意,記住自動生成的ID是一個技術變量(譯註:這裏我弄不懂,如下附上原文)

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 a technical identifier.

若是你寫過Java,心中可能已經敲起了警鐘,由於咱們竟然大量使用公有成員!在Java(一如其餘面嚮對象語言),最佳實踐一般是儘可能保持各成員私有,並提供getter和setter。這就是封裝,面向對象設計的基本概念之一。事實上,Play已經考慮到這一點,在自動生成getter和setter的同時保持封裝;等下咱們將看到它是怎麼作到的。

如今你能夠刷新主頁面,看一下結果。固然,除非你犯錯,不然應該什麼變化都看不到:D。Play自動編譯並加載了User類,不過這沒有給應用添加任何新特性。

寫下第一個測試

測試新增的User類的一個好方法是寫下JUnit測試用例。它會容許你增量開發的同時保證一切安好。

要運行一個測試用例,你須要在'test'模式下運行應用。中止當前正在運行的應用,打開命令行並輸入:

~$ play test

test

play test命令就像play run,不過它加載的是一個測試運行器模塊,使得你能夠直接在瀏覽器中運行測試套件。

當你在test mode中運行Play應用時,Play會自動切換到test框架ID並加載對應的application.conf。閱讀框架ID文檔來了解更多。

在瀏覽器打開http://localhost:9000/@tests頁面來看看測試運行器。嘗試選擇全部的默認測試並運行;應該所有都會是綠色……可是默認的測試其實什麼都沒測:D

test runner

咱們將使用JUnit測試來測試模型部分。如你所見,已經存在一個默認的BasicTests.java,因此讓咱們打開它(/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);
    }

}

刪除沒用的默認測試(aVeryImportantThingToTest),建立一個註冊新用戶並進行檢查的測試:

@Test
public void createAndRetrieveUser() {
    // Create a new user and save it
    new User("bob@gmail.com", "secret", "Bob").save();

    // Retrieve the user with e-mail address bob@gmail.com
    User bob = User.find("byEmail", "bob@gmail.com").first();

    // Test 
    assertNotNull(bob);
    assertEquals("Bob", bob.fullname);
}

如你所見,Model父類給咱們提供了兩個很是有用的方法:save()find()

你能夠在Play文檔中的JPA支持閱讀到Model類的更多方法。

在test runner中選擇BasicTests.java,點擊開始,看一下是否是全都變綠了。

咱們將須要在User類中添加一個方法,來檢查給用戶的用戶名和密碼是否存在了。讓咱們完成它,而且測試它。

User.java中,添加connect()方法:

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

現在測試用例成這樣:

@Test
public void tryConnectAsUser() {
    // Create a new user and save it
    new User("bob@gmail.com", "secret", "Bob").save();

    // Test 
    assertNotNull(User.connect("bob@gmail.com", "secret"));
    assertNull(User.connect("bob@gmail.com", "badpassword"));
    assertNull(User.connect("tom@gmail.com", "secret"));
}

每次修改以後,你均可以從Play測試運行器運行全部的測試,來確保沒有什麼被破壞了。

Post類

Post類表示博客文章。讓咱們寫下代碼:

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();
    }

}

這裏咱們使用@Lob註解告訴JPA來使用字符大對象類型(clob)來存儲文章內容。咱們也聲明跟User類的關係是@ManyToOne。這意味着每一個Post對應一個User,而每一個User能夠有多個Post

PostgreSQL的最近版本不會將@Lob註解的String成員存儲成字符大對象類型,除非你額外用@Type(type = "org.hibernate.type.TextType")註解該成員。

咱們將寫一個新的測試用例來檢查Post類可否正常工做。但在寫下更多測試以前,咱們須要修改下JUnit測試類。在當前測試中,數據庫的內容永不刪除,因此每次運行測試都會建立愈來愈多的對象。假如未來咱們須要測試對象的數目是否正確,這將會是一個問題。

因此先寫一個JUnit的setup()方法在每次測試以前清空數據庫:

public class BasicTest extends UnitTest {

    @Before
    public void setup() {
        Fixtures.deleteDatabase();
    }

    …
}

@Before是JUnit測試工具的一個核心概念

如你所見,Fixtures類是一個在測試時幫助處理數據庫的類。再次運行測試並檢查是否一切安好。以後接着下下一個測試:

@Test
public void createPost() {
    // Create a new user and save it
    User bob = new User("bob@gmail.com", "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 posts 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);
}

不要忘記導入java.util.List,不然你會獲得一個編譯錯誤。

添加Comment類

最後,咱們須要給博文添加評論功能。

建立Comment類的方式十分簡單直白。

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();
    }

}

讓咱們寫下第一個測試用例:

@Test
public void postComments() {
    // Create a new user and save it
    User bob = new User("bob@gmail.com", "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);
}

你能夠看到PostComments之間的聯繫並不緊密:咱們不得不經過查詢來得到全部跟某一個Post關聯的評論。經過在PostComment類之間創建新的關係,咱們能夠改善這一點。

Post類添加comments成員:

...
@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();
}
...

注意如今咱們用mappedBy屬性來告訴JPAComment類的post成員是維持這個關係的一方。當你用JPA定義一個雙向關係時,須要指定哪一方來維持這個關係。在這個例子中,由於Comment示例依賴於Post,咱們按Comment.post的反向來定義關係。

咱們也設置了cascade屬性來告訴JPA,咱們但願Post的刪除將級聯影響到comments。也便是,若是你刪除一個博文時,全部相關的評論也將一併刪除。

因爲有了這個新關係,咱們能夠給Post類添加一個輔助方法來簡化評論的添加:

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

讓咱們寫多一個測試檢查它可否工做:

@Test
public void useTheCommentsRelation() {
    // Create a new user and save it
    User bob = new User("bob@gmail.com", "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();

    // Check that all comments have been deleted
    assertEquals(1, User.count());
    assertEquals(0, Post.count());
    assertEquals(0, Comment.count());
}

此次全綠了麼?

new test

使用Fixtures來寫更復雜的測試

當你開始寫更加複雜的測試,你一般須要一些測試數據。Fixtures容許你在一個YAML文件中描述你的模型,並在測試開始前加載。

編輯/yabe/test/data.yml並開始描述一個User:

User(bob):
    email: bob@gmail.com
    password: secret
    fullname: Bob

...

呃,由於data.yml有點大,你能夠在這裏下載它。

如今咱們能夠建立一個加載數據並對它運行一些斷言的測試用例:

@Test
public void fullTest() {
    Fixtures.loadModels("data.yml");

    // Count things
    assertEquals(2, User.count());
    assertEquals(3, Post.count());
    assertEquals(3, Comment.count());

    // Try to connect as users
    assertNotNull(User.connect("bob@gmail.com", "secret"));
    assertNotNull(User.connect("jeff@gmail.com", "secret"));
    assertNull(User.connect("jeff@gmail.com", "badpassword"));
    assertNull(User.connect("tom@gmail.com", "secret"));

    // Find all of Bob's posts
    List<Post> bobPosts = Post.find("author.email", "bob@gmail.com").fetch();
    assertEquals(2, bobPosts.size());

    // Find all comments related to Bob's posts
    List<Comment> bobComments = Comment.find("post.author.email", "bob@gmail.com").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());
}

你能夠在YAML manual page中閱讀更多關於Play和YAML的內容。

保存你的成果

如今咱們已經完成了博客引擎的大部分模型層。既然已經建立並測試好了模型層,咱們能夠開始開發這個Web應用了。

不過在繼續前進以前,是時候用Bazaar保存你的成果。打開命令行,輸入bzr st來看看在前一個提交以後作的修改:

$ bzr st

如你所見,一些新文件不在版本控制之中。test-result目錄不須要加入到版本控制,因此就忽略它。

$ bzr ignore test-result

經過bzr add向版本控制加入其餘文件。

$ bzr add

你如今能夠提交你的改動了。

$ bzr commit -m "The model layer is ready"
相關文章
相關標籤/搜索