Freewind @ Thoughtworks scala java javascript dart 工具 编程实践 月结 math python english [comments admin] [feed]

(2013-01-01) 2. 数据模型(Data Model)

广告: 云梯:翻墙vpn (省10元) 土行孙:科研用户翻墙http proxy (有优惠)

(本文译者是胡波)

第一次亲密接触数据模型(Data Model)

让我们用model开始我们的blog之旅。

JPA概述

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的话,还是恶补一下再继续吧。

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.

comments powered by Disqus