第一篇:play手把手教你创建一个博客项目-02.迭代编写数据模型代码
02.迭代编写数据模型代码
下面,我们将开始为我们的博客引擎写一个数据模型。
JPA介绍
在play应用程序(事实上也包括所有设计良好的应用程序)里,模型层是最为核心的地方。play是一个域驱动类型的数据表现框架,既然我们想要创建一个博客引擎,那么模型层所需要的类(比如User/Post和Comment)就应当首先考虑。
因为大多数模型对象都需要在应用程序重新启动后能够继续保存数据,因此,我们就必须把它们保存在一个可持久化的存储器里。通常,我们会选择一个关系数据库来保存数据。但是,由于java是面向对象的语言,因此,我们将使用对象关系映射来减少匹配阻抗。
Java Persistence API(JPA)是一个Java规范,它定义了一个标准的对象关系映射API集。作为JPA规范的实现,play使用众所周知的Hibernate框架来实现JPA。使用基于Hibernate API的JPA的一个优势就是所有映射都是在java对象里进行声明的。
如果在此之前你曾经用过Hibernate或JPA,那么在play里使用JPA你会感到非常惊讶,因为在Play里使用Hibernate或JPA实在是太简单了,它不需要进行任何配置即可正常工作~!
如果你不知道什么是JPA,哈哈,请在继续下面的课程之前先补补课some of these simple presentations。
创建第一个模型类User 接下来我们将开始为博客引擎编写代码,首先是创建User类,接下来让我们创建/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注释使User类成为一个可被管理的JPA实体,其继承的Model超类自动为其提供了一系列非常有用的JPA帮助方法(后面我们会作介绍)。所有User类的fields(以下均称为字段,不再提示)都会自动持久化到数据库里。默认情况下,数据库表的名称为 ‘User’。在某些情况下,默认的表名可能会与数据库的保留关键字冲突,这时就需要对数据库表名进行定制,比如针对User类,可以注释为@Table(name=“blog_user”)。
继承play.db.jpa.Model类并不是必须的,你可以继续使用原始的JPA。但继承Model类是一个好的选择,通过继承Model类,你可以通过它提供的帮助方法很方便的对实体类进行操作。
如果之前你用过JPA,那么你应该知道每个JPA实体都必须提供一个@Id属性。在这里,Model超类提供了一个自动生成数字型ID的功能,在很多情况下已经足够了。
注意,请不要把Model自动提供的ID当作功能性标识符使用,只能把它当成是技术性标识符使用。通常情况下,自动生成的数字ID作为一个技术标识符是个较好的主意。
如果你是一个经验丰富的java开发者,那么你就会知道在模型对象里的字段应该是private的,访问这些字段的方式是为这些字段创建getter/setter方法。但是,在play里,字段是用public修饰的,其目的是为了编写代码的量,请别担心,事实上play会小心照顾好这些字段,自动为其生成getter/setter。刷新一下应用程序主页,看看有些什么变化没。事实上,除非代码有错误,你应该看不到任何变化。play已经自动编译和加载了User类,但是这并没有为应用程序添加任何新的特征。
编写第一个测试程序 在play里,测试新创建的User类的好方法就是编写一个JUnit单元测试。这个测试将允许你对User进行测试。
要想进行测试,你得把play切换到test模式下。首先停止当前正在运行的应用程序,在命令行输入如下命令,以切换到测试模式: ~$ play test 除了加载了一个test runner模块,以允许你在浏览器直接运行测试组件外,play test命令和play run很相似。
当play运行与测试模式时,Play将自动切换到测试框架ID,并加载相应的application.conf配置文件,参考framework ID documentation了解更多信息。在浏览器打开http://localhost:9000/@tests URL地址,就可以看到test runner了。试着选择所有默认的测试并运行它们,所有的结果都应该是绿色的„其实这些测试并没有进行任何实质性的测试。为了测试User模型(适用于所有的模型类),我们打算使用JUnit测试。正如你所看到的一样,play已经提供了一个默认的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()and find()。在play管理手册的JPA support节,你可了解更多Model类的方法。在test runner里选择BasicTests.java,单击start进行测试,可以看到结果全是绿色。
接下来,我们需要在User里创建一个验证user的username和password是否正确的方法(通过username和password来找到user)。打开User.java源文件,添加connect()方法:
public static User connect(String email, String password){ return find(“byEmailAndPassword”, email, password).first();} 在BasicTests.java里添加测试代码:
@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”));} 每次修改后,你都可以直接在test runner里运行所有的测试。
创建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这个字段是一个超大文本数据库类型,用是存储发表的内容。我们同时用@ManyToOne来声明Post与User是多对一的关系。意思是每篇博客都是单个用户写的,每个用户可是发表多篇博客。现在的PostgreSQL版本还不能存储用@Lob注释的String类型字段,解决方法是使用@Type(type = “org.hibernate.type.TextType”)进行注释。接下来,我们将编写新的测试代码用于测试Post类。但是在写更多测试代码之前,我们需要在JUnit测试类里做一些工作,比如在当前的测试代码里,数据库内容并不会删除,因此,我们每运行一次,play就会增加一个新创建的对象,这些对象会越来越多,当进行更高级的测试时,这或许会成为问题。
因此,让我们在运行每个测试之前写一个JUnit setup()方法来删除数据库: public class BasicTest extends UnitTest {
@Before public void setup(){ Fixtures.deleteDatabase();}
„ } @Before是JUnit测试工具最核心的概念。
正如你所看到的,Fixtures类帮助我们在测试期间管理数据库。接下来,我们将编写下一个测试:
@Test public void createPost(){ //创建一个新的user,并保存
User bob = new User(“bob@gmail.com”, “secret”, “Bob”).save();
// 创建一个新post new Post(bob, “My first post”, “Hello world”).save();
// 测试上一步是否创建了新的post assertEquals(1, Post.count());
// 获取所有“Bob”发表的posts List
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();} } Comment的测试代码: @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
// 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之间的一多对关系。在Post类里添加一个comments域:
...@OneToMany(mappedBy=“post”, cascade=CascadeType.ALL)public List
public Post(User author, String title, String content){ this.comments = new ArrayList
定义好上述关系后,我们需要在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来编写更复杂的测试 在开发编写更复杂的测试之前,我们通常需要提前准备一系列的测试数据,Fixtures类允许我们在一个YAML文件里描述需要准备数据的模型,它允许我们在进行任何测试之前加载这个yaml文件。
编辑/yabe/test/data.yml文件,对User模型的数据进行描述:
User(bob): email: bob@gmail.com password: secret fullname: Bob...OK,这个data.yml文件有点大,我们已经提前准备好了,还是下载吧download it here。
接下来,我们将在测试代码里加载这个文件里描述的数据,并对它进行测试: @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
bobPosts = Post.find(“author.email”, “bob@gmail.com”).fetch();assertEquals(2, bobPosts.size());
// Find all comments related to Bob's posts List
// 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的内容,请阅读YAML manual page。
第二篇:play手把手教你创建一个博客项目-08.添加身份认证
08.添加身份认证
在上一节是,我们为应用程序添加了管理区域(administration area)功能,现在我们将在这些管理区域中插入一些身份认证功能。幸运的是,play已经为这个功能准备了一个模块,这个模块叫Secure(安全)。
在程序里允许使用Secure模块
在yabe/conf/application.conf文件里允许使用Secure模块,并重启程序: # Import the secure module module.secure=${play.path}/modules/secure 重启后,play应用在控制台显示模块已经成功启动的相关信息。
Secure模块带有一系列默认的路由,需要在yabe/conf/routes里引入(或定义自己的路由也行): # Import Secure routes * / module:secure 保护admin(此处指需要身份认证的)控制器
安全模块提供了一个controllers.Secure控制器,它声明了所有可能用到的拦截器。当然,我们可以以继承这个控制器的方法获得其拦截器,但是java只允许单继承,这就导致了一些问题。
为了避免单继承带来的限制,我们可以用@With来注释admin控制器,以告诉play去调用相应的拦截器:
package controllers;
import play.*;import play.mvc.*;
@With(Secure.class)public class Posts extends CRUD { } 同样用于Comments, Users和Tags控制器。
Now if you try to access any admin action, you should get a log-in page: 事实上,现在你就可以试着输入任意username/password对看看,它其实并没有对身份进行认证。
定制身份认证处理
应用程序必须提供一个controllers.Secure.Security实例来定制身份认证处理。通过继承这个类来创建我们自己版本的Secure类,我们可以指定如何对用户身份进行认证。
创建yabe/app/controllers/Security.java文件,重写authenticate()方法: package controllers;
import models.*;
public class Security extends Secure.Security {
static boolean authenticate(String username, String password){ return true;} } 既然我们已经拥有了User对象,那么就非常容易实现这个方法: static boolean authenticate(String username, String password){ return User.connect(username, password)!= null;} 现在打开http://localhost:9000/logout进行登录尝试,用户名和密码在initial-data.yml文件里定义,比如bob@gmail.com/secret。
重构管理区域(administration area)
我们之前已经使用CRUD模块来实现管理区域,但是这个管理区域仍然没有集成到博客UI里,接下来我们将在一个新的管理区域上开始工作。这个新的管理区域允许每个作者访问他自己的博客。而超级用户则继续使用完整功能的管理区域。
接下来,让我们为管理部分创建一个新Admin控制器: package controllers;
import play.*;import play.mvc.*;
import java.util.*;
import models.*;
@With(Secure.class)public class Admin extends Controller {
@Before static void setConnectedUser(){ if(Security.isConnected()){ User user = User.find(“byEmail”, Security.connected()).first();renderArgs.put(“user”, user.fullname);} }
public static void index(){ render();} } 重构路由定义yabe/conf/routes: # Administration GET /admin/? Admin.index * /admin module:crud 请注意路由的顺序,第一行就匹配了的http请求相应的action会率先使用,同时忽略在其之下的路由配置。也就是说Admin控制器必须位于第二行之上,第二条路由将匹配所有其他的/admin请求,用于调用CRUD模块页面,否则/admin/将映射到CRUD.index而不是Admin.index。
现在把yabe/app/views/main.html模块里的 ‘Log in to write something’文本修改到Admin.index控制器action:
…
…最后一件事就是为yabe/app/views/Admin/index.html模板文件完成所有的填充工作,让我们从简单的开始: Welcome ${user}!现在回到主页,单击 ‘Log in to write something’链接就回进入样的管理区域页面: 非常好!但是既然我们已经有几个管理区域的页面,那么,我们就应该有一个超级模板以重用代码,让我们创建一个yabe/app/views/admin.html模板:
#{get 'moreStyles' /}
第三篇:play手把手教你创建一个博客项目-03.构建第一个页面
03.构建第一个页面
之前,我们已经编写好了数据模型,是时候为应用程序创建第一个页面了。这个页面只显示当前发表的博文完整内容,同时显示之前发表的博文列表。下面是该页面结构示意图:
在启动时加载默认数据
事实上,在编写第一个页面之前,我们还需要做一些工作。在一个没有任何测试数据的页面上进行工作并不太好,你甚至不能进行任何测试。
为博客程序注入测试数据的一条途径就是在应用程序启动时加载一个固定文件。为了实现这个目的,我们将创建一个引导任务(Bootstrap Job)。一个play job 任务就是一在没有任何http请求的情况下执行一些特定工作,比如在应用程序启动时或指定时间间隔时使用CRON任务。
接下来,让我们创建/yabe/app/Bootstrap.java job文件,使用Fixtures加载一系列默认数据:
import play.*;import play.jobs.*;import play.test.*;
import models.*;
@OnApplicationStart public class Bootstrap extends Job {
public void doJob(){ // 检查数据库是否为空 if(User.count()== 0){ Fixtures.loadModels(“initial-data.yml”);} } } 在这里,我们使用@OnApplicationStart来注释这个Job,用于告诉play我们打算在应用程序启动时同步运行这个任务。
事实上,在DEV和PROD模式下,这个任务的运行情况有所不同。在DEV模式下,play会等待第一个请求达到时才运行任务。因此这个任务会在第一个请求到达时才同步执行,也就是说,如果这个任务失败,你会在浏览器里看到错误消息。在PROD模式里,这个任务将会在应用程序启动时就执行(与play run命令同步),以防止应用程序在启动时发生错误。
你必须在yabe/conf/下创建一个initial-data.yml文件。当然,你也可以重用我们之前在data.yml里定义的内容。
博客主页
这次,我们将真正开始编写主页代码。
还记得程序的第一个页面是如何显示的吗?首先是在routes文件里指定/ URL 将调用controllers.Application.index()action方法,然后这个index()调用render()方法渲染/yabe/app/views/Application/index.html模板。我们将保留这些组件,但是我们会在其中添加代码来加载博文列表并显示。打开/yabe/app/controllers/Application.java控制器并修改index()action来加载博文列表:
package controllers;
import java.util.*;
import play.*;import play.mvc.*;
import models.*;
public class Application extends Controller {
public static void index(){ Post frontPost = Post.find(“order by postedAt desc”).first();List
olderPosts = Post.find(“order by postedAt desc”).from(1).fetch(10);render(frontPost, olderPosts);} } 看到我们是如何向render方法传递对象的吗?通过这种方式,我们就可以在模板里使用相同的名称来访问这些对象了。在上面的代码里,我们在模板里就可以直接使用变量frontPost和olderPosts。
打开/yabe/app/views/Application/index.html并修改,用于显示这些对象: #{extends 'main.html' /} #{set title:'Home' /}
#{if frontPost}