接下來咱們要開始完成咱們的博客引擎的模型部分。html
模型層是一個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類。建立新文件/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
play test
命令就像play run
,不過它加載的是一個測試運行器模塊,使得你能夠直接在瀏覽器中運行測試套件。
當你在
test mode
中運行Play應用時,Play會自動切換到test
框架ID並加載對應的application.conf
。閱讀框架ID文檔來了解更多。
在瀏覽器打開http://localhost:9000/@tests頁面來看看測試運行器。嘗試選擇全部的默認測試並運行;應該所有都會是綠色……可是默認的測試其實什麼都沒測:D
咱們將使用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
類表示博客文章。讓咱們寫下代碼:
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
類的方式十分簡單直白。
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); }
你能夠看到Post和Comments之間的聯繫並不緊密:咱們不得不經過查詢來得到全部跟某一個Post
關聯的評論。經過在Post
和Comment
類之間創建新的關係,咱們能夠改善這一點。
在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()); }
此次全綠了麼?
當你開始寫更加複雜的測試,你一般須要一些測試數據。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"