はじめてのモデル
ここから、ブログエンジンのモデルを書いていきます。
JPA 概論
モデル層は、Play アプリケーション (そして、実際のところはよくデザインされたすべてのアプリケーション) において中心的な位置を占めます。モデルは、アプリケーションが操作する情報のドメインに特化した表現です。ここではブログエンジンを作りたいので、モデル層は確実に User, Post そして Comment といったクラスを含むことになるでしょう。
モデルオブジェクトはアプリケーションを再起動する間も存続する必要があるので、これを永続化データストアに保存しなければなりません。一般的にはリレーショナルデータベースを使うことを選択します。しかしながら Java はオブジェクト指向言語なので、インピーダンスミスマッチの減少を手助けする オブジェクト-リレーショナルマッピングツール を使用します。
Java Persistence API (JPA) は O/R マッピングの標準的な API を定義する Java の仕様です。JPA 実装として、Play はよく知られた Hibernate フレームワークを使用します。Hibernate API を通して JPA を使うことの 1 つの利点は、すべての ‘マッピング’ が直接 Java オブジェクトに定義されることです。
もし Hibernate か JPA を以前に使ったことがあるなら、Play によって追加された簡潔さに驚くことでしょう。なにも設定する必要はありません; JPA は Play とともにすぐに使うことができます。
もし JPA を知らないのであれば、以下を続ける前に いくつかの簡単なプレゼンテーション を読むことができます。
User クラス
User クラスを作成するところからブログエンジンのコーディングを始めましょう。新しく /yabe/app/models/User.java ファイルを作成し、User クラスの最初の実装を定義します:
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 エンティティであることを印付けし、 Model スーパークラスは後述する便利な JPA ヘルパを自動的に提供します。このクラスのすべてのフィールドは、自動的にデータベースに永続化されます。
モデルオブジェクトは play.db.jpa.Model クラスを継承しなければならないわけではありません。素の JPA を使うこともできます。しかし、このクラスは JPA 周りの多くの部分を簡易にするので、多くのケースにおいて、これを継承するのは良い選択です。
もし以前に JPA を使ったことがあるなら、すべての JPA エンティティは @Id プロパティを提供しなければならないことを知っているでしょう。ここでは、Model スーパークラスが自動的に生成された数値型の id を提供しており、ほとんどの場合はこれで必要充分です。
id フィールドを 機能的な識別子 として考えず、 技術的な識別子 として考えてください。これらの概念を分けて扱い、自動的に生成された数値型の ID を技術的な識別子として保持するのは、一般的に良い考えです。
どのような経験があろうと、Java 開発者であれば public 変数を目にした途端に警告ベルがじゃんじゃん鳴り出すかもしれません。Java においては (他のオブジェクト指向言語と同様に) すべてのフィールドを private にして、アクセサとミューテータを提供するのを最も良い習慣としています。これは、オブジェクト指向デザインにおいて重要な概念であるカプセル化を促進するためのものです。実際のところ、Play はこれに対応しており、getter と setter を自動生成してカプセル化を保護します; これがどのようにして動作するのかについては、このチュートリアルの後半で紹介します。
これでアプリケーションのホームページをリフレッシュして結果を確認することができます。なにか間違いがない限り、実は何の変化もありません: Play は User クラスを自動的にコンパイルしてロードしていますが、これはアプリケーションに対して何の新機能も追加しません。
はじめてのテストの作成
新規に作成した User クラスをテストする良い方法は、JUnit テストケースを書くことです。これによりアプリケーションをくり返し仕上げ、すべてがばっちりであることを確信できるようになります。
テストケースを実行するには、アプリケーションを特別な ‘test’ モードで起動する必要があります。いま実行しているアプリケーションを停止し、コマンドラインを開いて次のようにタイプしてください:
~$ play test
play test コマンドは、ブラウザから直接テストスイートを実行できるテストランナーモジュールをロードすること以外は play run とほとんど同じです。
Paly アプリケーションを test モード で実行すると、Play は自動的にフレームワーク ID を test に切り替え、これに従った application.conf ファイルをロードします。詳しくは フレームワーク ID ドキュメント を確認してください。
ブラウザから http://localhost:9000/@tests を開いてテストランナーを見てみてください。すべてのデフォルトテストを選択して実行してみてください; すべてグリーンになるはずです... しかし、これらのデフォルトテストは実際には何もテストしません。
アプリケーションのモデル部分をテストするには、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("[email protected]", "secret", "Bob").save();
// Retrieve the user with e-mail address [email protected]
User bob = User.find("byEmail", "[email protected]").first();
// Test
assertNotNull(bob);
assertEquals("Bob", bob.fullname);
}
ご覧の通り Model スーパークラスは 2 つの便利なメソッドを提供してくれています: save() と find() です
Model クラスメソッドの更なる情報は、Play マニュアルの JPA サポート の章で読むことができます。
テストランナーで 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("[email protected]", "secret", "Bob").save();
// Test
assertNotNull(User.connect("[email protected]", "secret"));
assertNull(User.connect("[email protected]", "badpassword"));
assertNull(User.connect("[email protected]", "secret"));
}
変更を行うたびに Play テストランナーですべてのテストを実行し、なにも壊れていないことを確認することができます。
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 に告げています。 User クラスとの関連を @ManyToOne を使って宣言しました。これはそれぞれの Post は 1 つの User によって所有され、各 User は複数の Post を所有することができることを意味します。
Post クラスが期待通りに動作することを確認する新しいテストケースを作成しましょう。ただし、より多くのテストを書く前に JUnit クラスでやらなければならないことがあります。現在のテストでは、データベースの内容は決して削除されませんので、新たに実行するごとにオブジェクトがどんどん作成されます。これは、すべてのオブジェクトが適切であることを確認するためにオブジェクトを数え上げるようなより高度なテストを始めた途端に問題となるでしょう。
このため、各テストの前にデータベースを削除する JUnit setup() メソッドを書きましょう:
public class BasicTest extends UnitTest {
@Before
public void setup() {
Fixtures.deleteAll();
}
...
}
この @Before は JUnit テストツールの主要な概念です。
見ての通り、 Fixtures クラスはテスト中にデータベースを扱うヘルパです。再度テストを実行して何もおかしくなっていないことを確認し、次のテストを書き始めましょう:
@Test
public void createPost() {
// Create a new user and save it
User bob = new User("[email protected]", "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("[email protected]", "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 クラスに関連の逆端を設定することで、もっとうまくやることができます。
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 属性を使って、 Comment クラスの post フィールドが関連を管理することを JPA に伝えていることに注意してください。JPA で双方向の関連を定義する場合、どちらの端が関連を管理するかを JPA に伝えるのはとても重要です。この場合、 Comments は Post に従属するので、 Comment クラスが関連を管理するほうがベターです。
cascade プロパティを設定して、JPA に Post の削除をコメントにも連鎖するよう告げています。このようにすると、投稿を削除した場合、すべての関連するコメントが同様に削除されます。
この新しい関連に伴って、コメントを追加するシンプルなヘルパメソッドを 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("[email protected]", "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());
}
グリーンになりましたか?
Fixture を使ったより複雑なテスト
より複雑なテストを書き始める場合、しばしばテストに使うデータセットが必要になります。Fixtures は、モデルを YAML ファイルに記述して、テストを実行する前にはいつでもロードできるようにします。
/yabe/test/data.yml ファイルを編集して User を定義してみましょう:
User(bob):
email: [email protected]
password: secret
fullname: Bob
...
ああ、 data.yml ファイルは少々大きいので、ここからダウンロード することができます。
このデータをロードして、それについていくつかのアサーションを実行するテストを作成してみましょう:
@Test
public void fullTest() {
Fixtures.load("data.yml");
// Count things
assertEquals(2, User.count());
assertEquals(3, Post.count());
assertEquals(3, Comment.count());
// Try to connect as users
assertNotNull(User.connect("[email protected]", "secret"));
assertNotNull(User.connect("[email protected]", "secret"));
assertNull(User.connect("[email protected]", "badpassword"));
assertNull(User.connect("[email protected]", "secret"));
// Find all of Bob's posts
List<Post> bobPosts = Post.find("author.email", "[email protected]").fetch();
assertEquals(2, bobPosts.size());
// Find all comments related to Bob's posts
List<Comment> bobComments = Comment.find("post.author.email", "[email protected]").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());
}
作業内容の保存
ここまででブログエンジンの大きな部分をやり終えました。これらを作成し、すべてテストしており、web アプリケーションそれ自身の開発を始めることができます。
しかし、開発を続ける前に作業内容を Bazaar を使って保存しましょう。コマンドラインを開いて bzr st とタイプし、最後のコミットから行われた変更内容を確認してください:
$ bzr st
見ての通り、いくつかのファイルはバージョン管理されていません。 test-result フォルダはバージョン管理する必要がないので、これは無視しましょう。
$ bzr ignore test-result
その他のファイルは bzr add を使って追加します。
$ bzr add
これでプロジェクトをコミットすることができます。
$ bzr commit -m "The model layer is ready"
次の章 に進みましょう。