(本文译者是胡波)
让我们用model开始我们的blog之旅。
Model层在Play应用程序中可谓举足轻重,实际上所有细粒度的应用程序设计都是如此。对信息处理的应用程序来说,Model只是对特定领域的一种表述。举例来说,当我们想要创建一个Blog,Model层自然而然的包括诸如 User, Post 以及 Comment 这样的类文件。
大多数情况下,一旦应用程序重启,内存中的model对象可能就不复存在了。若要想继续使用,必须存储到数据库里。通常选择的是关系型数据库,但Java本身是一门面向对象的编程语言,我们会去用ORM去减少两者之间的阻抗。
JPA 是Java的持久层规范,它定义了一系列ORM的API接口。Play作为JPA的一个实现,底层实际上用的是闻名遐迩的Hibernate。使用JPA标准接口的一个好处就是,所有的'mapping'直接可以声明在Java对象里。
如果你以前有用过Hibernate或JPA,你会惊讶Play竟然简化的如此神奇——无任何配置,JPA开箱即用。
要是一点都不懂JPA的话,还是恶补一下再继续吧。
我们从创建一个User类开始我们的Blog之旅。文件路径为 /yabe/app/models/User.java, 初次实现代码如下:
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** 表示此类为JPA托管的实体,而Play提供的父类Model又封装了一系列开箱即用的给力方法(稍后会说明)。
> 默认情况下,表的名字也是'User'。如果你所使用的数据库'User'是关键字的话(比如SQL Server),通过JPA的标注**@Table(name=“blog_user”)**指定一个新的表名即可。
当然不是说非得继承Play的**play.db.jpa.Model**,也使用普通的JPA。不过继承Model有很多激动人心的扩展,能更加方便,容易的使用JPA未尝不是一件好事? 如果以前用过JPA,那么肯定知道每个实体都要求有个标注**@Id**来表示主键。这里我们继承的Model默认采用的是绝大多数适用的数据库自增ID。
在这里我要重申一下,这个Id字段是非业务性标识符,而只是个技术处理的手段。什么意思呢?我们不应该拿业务对象本身的Id作为主键,应该创建一个新的主键(可能是自增,序列或者哈希)来满足系统。因此,将两者的概念区分开来非常重要。(做过数据迁徙的童鞋,一定深有体会。)
如果你是一名有过Java开发经验的程序员,肯定会摆出一副不屑一顾,却又嗤之以鼻的姿态,然后开始大谈什么是OO,什么是封装——不要将成员变量公开化,应该通过get/set方法来访问他们。 实际上, Play会自动帮你创建get/set方法,在后面的章节中会谈到它具体怎么工作的。 现在刷新一下浏览器看看首页结果,如果不出意外的话,首页是没有任何变化的——尽管Play后台自动的编译、加载User类,但是对当前应用无任何影响。 #### 开始第一个测试 如何测试刚创建的User类呢?使用JUnit是不错的选择。它用一种渐进式的方法来边开发,边测试,最后确保所有方法都没问题。 想要进行单元测试,得用特殊的方式(测试模式)来启动你的应用程序。先停了当前的应用,在命令行重新输入: ~$ play test [![image](/user_images/1299-1.png "image")](/user_images/1299-1.png) 发现了吧,和运行模式差不多,只是多了个可以直接通过浏览器进行测试的模块。
在测试模式下运行,Play会自动切换Framework ID,并且从application.conf中加载对应的配置信息。猛击此处查看更多关于Framework ID的信息。
用浏览器打开 [http://localhost:9000/@tests,看看测试用例。](http://localhost:9000/@tests%EF%BC%8C%E7%9C%8B%E7%9C%8B%E6%B5%8B%E8%AF%95%E7%94%A8%E4%BE%8B%E3%80%82) 选择所有的默认测试用例并运行它们,应该会全亮绿色。实际上,默认的测试用例什么都没有做。 测试应用程序的Model,我们用的是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) **方法,创建一个新的方法来测试创建和读取User的操作: @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()方法多给力。 想了解更多Model的内置方法,可以参考Play的JPA章节。 在重新选择 BasicTests.java 进行测试,看看结果是不是都是绿条。 现在我们需要做一个验证用户名和密码是否正确的功能。在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")); } 如果有修改到代码,你可以运行所有的测试用例保证代码没有因为你的改变而遭到破坏。 #### The Post class 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来进行存储。因为User和Post的关系是多对一,我们使用标注@ManyToOne进行关联。这样每个Post归属于一个User,一个User可以有多个Post。
最近的几个PostgreSQL版本即使你用了@Lob也不会存储字符串到数据库中。没关系,加上Hibernate的扩展标注@Type(type = “org.hibernate.type.TextType”).就可以搞定。
现在我们来编写一个新的测试来看看Post类是否没问题。在测试之前,我们需要做些准备工作——因为每次的测试都会往数据库里写入新的记录。这对其它类似测试统计记录个数的方法会产生干扰,所以我们在JUnit的setup()方法中先删除整个数据库: public class BasicTest extends UnitTest { @Before public void setup() { Fixtures.deleteDatabase(); } … }
标注@Before是JUnit的核心概念之一,感兴趣的话可自己去查看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,否则无法通过编译。
#### 增加评论 最后我们要给我们的Blog增加评论的功能。 那么现在就开始来创建我们的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和Comment关联关系不是很简单:在给Post加Comment之前,首先要得到所有的Comment对象。不过别急,要想简化的话,我们可以把在Post类中加入新的的关联关系: 给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(); } ... 这里我们还是使用了JPA的标注@OneToMany,其中属性mappedBy表示用Comment类的post属性是控制端,负责关联关系的管理。当你定义了双向关联后,指定哪方来维护关联关系非常重要。在这个例子中,因为每一个Comment都属于一个Post,所以Comment上的post属性表示Comment于Post的反转关系。 我们还定了了级联删除 cascade=CascadeType.ALL。当我们删除Post时,级联下的Comment统统都会跟着一起删除。 有了新定义的关联关系,我们可以添加一个简化后的方法来为Post增加Comment: 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()); } 你是不是也全是绿条呢? [![image](/user_images/1299-3.png "image")](/user_images/1299-3.png) #### Using Fixtures to write more complicated tests When you start to write more complex tests, you often need a set of data to test on. Fixtures let 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: bob@gmail.com password: secret fullname: Bob ... Well, because the data.yml file is a litle big, you can download it here. Now we create a test case that loads this data and runs some assertions over it: @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()); }
You can read more about Play and YAML in the YAML manual page.
#### 用Fixtures编写更多复杂测试 在开始复杂的单元测试测试之前,通常需要准备一系列的测试数据。Fixtures 允许你用YAML文件来构建你的测试数据,并且还可以任意次的加载。 编辑文件 **/yabe/test/data.yml**,创建我们的测试数据: User(bob): email: bob@gmail.com password: secret fullname: Bob ... 该文件比较大,这里已经有编写好的data.yml文件,你也可以直接[点击此处下载](http://www.playframework.org/documentation/1.2.5/files/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()); }
想了解更多关于Play+YAML的话,会在后面的章节YAML中详细介绍。
#### 用Fixtures编写更多复杂测试 在开始复杂的单元测试测试之前,通常需要准备一系列的测试数据。Fixtures 允许你用YAML文件来构建你的测试数据,并且还可以任意次的加载。 编辑文件 **/yabe/test/data.yml**,创建我们的测试数据: User(bob): email: bob@gmail.com password: secret fullname: Bob ... 该文件比较大,这里已经有编写好的data.yml文件,你也可以直接[点击此处下载](http://www.playframework.org/documentation/1.2.5/files/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()); }
想了解更多关于Play+YAML的话,会在后面的章节YAML中详细介绍。
到现在为止,我们已经完成了大部分的Blog开发。我们创建了所需的Model,并且对他们进行了单元测试。接下来可以开始Web开发之旅了。
But before continuing, it’s time to save your work using Bazaar. Open a command line and type bzr st to see the modifications made since the last 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 all the 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 Building the first screen.