本篇为playframework入门引导的第三篇,主要介绍如何使用JPA,本篇将以博客网门为例说明。
JPA介绍
model
层是Play应用程序的核心部份,它划定了我们应用中所使用的特定领域domain-specific
。比如我们创建一个博客应用,model层就会包括用户User,文章Post,评论Comment等类。
因为大部份model对象都需要在应用重启后保留下来,我们必须要把他们保存在一个持久化的数据存储中。大众的选择是使用关系型数据库,但是由于Java是一个面向对象的编程语言,我们可以使用对象关系映射object-relational mapper
来减少麻烦。
Java Persistence API,即JPA
是Java特定用于处理对象关系映射的标准API。Hibernate框架是众所周知的JPA实现。使用JPA的一个好处是,Hibernate API的映射关系是直接声明在Java对象中。
如果你用过Hibernate或者JPA,你会感到惊喜,Play将两者简单的结合在一起,不需要任何配置,JPA开箱即用。如果你未接触过JPA,可以阅读 some of these simple presentations
Model类—User
package models;
import play.db.jpa.Model;
import javax.persistence.Entity;
@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, boolean isAdmin) {
this.email = email;
this.password = password;
this.fullName = fullName;
this.isAdmin = isAdmin;
}
}
@Entity
注解标识这个类由JPA实体管理,User的父类Model
提供了很多有用的JPA帮助方法。类中所有的成员都可以自动持久化至数据库中。
表名默认是
User
,user在某些数据中是预留的关键字,可以通过使用@Table
注解来更改表名。@Table(name="blog_user")
实体类并不是必须要继承
play.db.jpa.Model
,也可以使用普通的JPA方式,但是继承Model类相对要简单很多。
如果你使用过JPA,你会知道每个JPA实体都必须要提供一个@Id
属性。play的Model类提供了自动生成主键方式,在绝大多数情况下都是够用的。
- 不要把id属性即用作功能的标识又用作技术性的标识,较好的作法是将功能性和技术性的标识分开。自动生成的ID通常作为技术情标识。
现在,如果你是一个有经验的Java开发者,你会认为所有属性都是公共的很疯狂。Java,作为一种纯面像对象的语言,最佳实践会告诉你把所有的属性都使用private
声明和提供公共的访问方式。事实上,Play会为你自动生成getter,setter保留封装。在下面的教程中,我会提及到。
测试
使用和play run
类似的命令行play test
可以在浏览器中测试应用。
访问地址可以看到所有的测试
http://localhost:9000/@tests
对于model 测试,我们还可以使用JUnit测试,在生成的项目中默认已经生成一个测试用例。BasicTests.java
import org.junit.*;
import java.util.*;
import play.test.*;
import models.*;
public class BasicTest extends UnitTest {
@Test
public void aVeryImportantThingToTest() {
assertEquals(2, 1 + 1);
}
}
现在我们来创建一个User类的测试。在BaseTest中添加如下代码
@Test
public void createUser() {
new User("cgz@gmail.com", "124", "gzPAPA", true).save();
User cgz = User.find("byEmail", "cgz@gmail.com").first();
assertNotNull(cgz);
assertEquals("gzPAPA", cgz.fullName);
}
- 这里的
save()
,find()
都是Model提供的方法
实体中的自定义方法:
在User.java
中添加较验帐号密码的方法check()
:
public static User check(String email, String password) {
return find("byEmailAndPassword", email, password).first();
}
Unit Test中
@Test
public void testCheckUser() {
new User("cgz@gmail.com", "124", "gzPAPA", true).save();
assertNotNull(User.check("cgz@gmail.com", "124"));
}
model类—Post文章类
package models;
import play.db.jpa.Model;
import javax.persistence.Entity;
import javax.persistence.Lob;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import java.util.Date;
@Entity
@Table(name = "tb_post")
public class Post extends Model {
public String title;
public Date postedAt;
@Lob
public String content;
@ManyToOne
public User author;
@OneToMany(mappedBy = "post", cascade = CascadeType.ALL)
public List<Comment> comments;
public Post(String title, Date postedAt, String content, User author) {
this.comments = new ArrayList<String>();
this.title = title;
this.postedAt = postedAt;
this.content = content;
this.author = author;
}
public Post previous() {
return Post.find("postedAt < ? order by postedAt desc", postedAt).first();
}
public Post next() {
return Post.find("postedAT > ? order by postedAt asc", postedAt).first();
}
public Post addComment(String author, String content) {
Comment comment = new Comment(this, author, content).save();
this.comments.add(comment);
this.save();
return this;
}
}
- 这里的
@Lob
注解告诉JPA使用大文本类型存储文章的内容。使用@ManyToOne
意味着每加载一篇文章都会加载一个用户实体。创建新的用例去测试Post类,但是在此之前,数据库的内容并不会被删掉,所以新加入的model,需要先执行deteted database
@Before
public void setup() {
Fixtures.deleteDatabase();
}
- @Before注解使在执行所有用例之前先执行此标注的方法。
Fixtures
类是一个处理测试数据库的工具。
测试代码:
@Test
public void testCreatePost() {
User cgz = new User("cgz@gmail.com", "124", "gzPAPA", true);
Logger.info("before save id:" + cgz.id);
cgz.save();
new Post(cgz, "my first post", "hello world", new Date()).save();
Logger.info("after save id:" + cgz.getId());
new Post(cgz, "my twice post", "hello world 2", new Date()).save();
List<Post> posts = Post.find("byAuthor", cgz).fetch();
assertEquals(posts.size(), 2);
Post post = posts.get(0);
assertNotNull(post.content);
}
- 这里要将一个对象持久化,其所有的属性都必须先持久化,例如调用
Post.save()
,传进去的user实例必须是数据库对象
model—Comments 评论类
package models;
import play.db.jpa.Model;
import javax.persistence.Entity;
import javax.persistence.Lob;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import java.util.Date;
@Entity
@Table(name = "tb_comments")
public class Comment extends Model {
public String author;
public Date postedAt;
@Lob
public String content;
@ManyToOne
public Post post;
public Comment(String author, String content, Post post) {
this.author = author;
this.postedAt = new Date();
this.content = content;
this.post = post;
}
}
编写测试:
@Test
public void testPostComments() {
User cgz = new User("cgz@gmail.com", "124", "gzPAPA", true).save();
Post post = new Post(cgz, "my first post", "hello world", new Date()).save();
new Comment(post, "Jerry", "good job").save();
new Comment(post, "Tom", "labshi job").save();
List<Comment> comments = Comment.find("byPost", post).fetch();
assertNotNull(comments);
assertEquals(comments.size(), 2);
}
使用Fixtures编写更复杂的测试
当你开始写复杂用例测试时,你可能需要事先存在一些数据。Fixtures可以使用YAML
文件描述你的model
创建/app_1/test/data.yml
文件,加入以下内容
User(cgz):
email:bob@gmail.com
password:secret
fullName:gzPAPA
如果你需要更多的测试数据,可以点击这里下载
现在我们创建新的用例:
@Test
public void fullTest() {
Fixtures.loadModels("data.yml");
assertEquals(2, User.count());
assertEquals(3, Post.count());
Post frontPost = Post.find("order by postedAt desc").first();
User user = frontPost.author;
List<Comment> comments = Comment.find("byPost", frontPost).fetch();
assertEquals(2, comments.size());
long postsNum = Post.count("byAuthor", user);
assertEquals(2, postsNum);
}