§はじめてのモデル
ここから、タスク管理システムのモデルを書いていきます。
§Ebean 概論
モデル層は、Play アプリケーション (そして、実際のところはよくデザインされたすべてのアプリケーション) において中心的な位置を占めます。モデルは、アプリケーションが操作する情報のドメインに特化した表現です。ここではタスク管理システムを作りたいので、モデル層は User
, Project
そして Task
といったクラスを含むことになるでしょう。
モデルオブジェクトはアプリケーションを再起動する間も存続する必要があるので、これを永続化データストアに保存しなければなりません。一般的にはリレーショナルデータベースを使うことを選択します。しかしながら Java はオブジェクト指向言語なので、インピーダンスミスマッチの減少を手助けする オブジェクト-リレーショナルマッピングツール を使用します。
Play にはリレーショナルデータベースのサポートが組み込まれていますが、Play framework を NoSQL データベースと共に使用することを妨げるものは何もありません。実際、 NoSQL を使用することは Play framework においてモデルを実装するとても一般的な方法です。しかしながら、このチュートリアルではリレーショナルデータベースを使います。
Ebean は Java の ORM ライブラリで、とてもシンプルなインタフェースでオブジェクトとデータベースのマッピングを行えるような実装を目指しています。Ebean はクラスをテーブルにマッピングするために JPA アノテーションを使用しますが、もし JPA を以前に使ったことがあるならば、Ebean はセッションレスであるという点で JPA とは異なることが分かるでしょう。このことは、JPA を使っているとちょっとした時に起こり得る、セッションのフラッシュや、失効または切断されたオブジェクトに関わるエラーのような多くの驚きを排除し、データベースとのやり取りをとてもシンプルにします。
§User クラス
User
クラスを作成するところから ZenTasks のコーディングを始めましょう。新しく app/models/User.java
ファイルを作成し、User
クラスの最初の実装を定義します:
package models;
import javax.persistence.*;
import play.db.ebean.*;
import com.avaje.ebean.*;
@Entity
public class User extends Model {
@Id
public String email;
public String name;
public String password;
public User(String email, String name, String password) {
this.email = email;
this.name = name;
this.password = password;
}
public static Finder<String,User> find = new Finder<String,User>(
String.class, User.class
);
}
@Entity
アノテーションは、このクラスが管理された Ebean エンティティであることを印付けし、 Model
スーパークラスは後述する便利な JPA ヘルパを自動的に提供します。このクラスのすべてのフィールドは、自動的にデータベースに永続化されます。
モデルオブジェクトは
play.db.ebean.Model
クラスを継承しなければならないわけではありません。素の Ebean を使うこともできます。しかし、このクラスは Ebean 周りの多くの部分を簡易にするので、多くのケースにおいて、これを継承するのは良い選択です。
もし以前に JPA を使ったことがあるなら、すべての JPA エンティティは @Id
プロパティを提供しなければならないことを知っているでしょう。ここでは、email
を id フィールドに選択しています。
後で紹介する find
フィールドは、プログラムでクエリを組み上げるために使用されます。
どのような経験があろうと、Java 開発者であれば public 変数を目にした途端に警告ベルがじゃんじゃん鳴り出すかもしれません。Java においては (他のオブジェクト指向言語と同様に) すべてのフィールドを private にして、アクセサとミューテータを提供するのを最も良い習慣としています。これは、オブジェクト指向デザインにおいて重要な概念であるカプセル化を促進するためのものです。実際のところ、Play はこれに対応しており、getter と setter を自動生成してカプセル化を保護します; これがどのようにして動作するのかについては、このチュートリアルの後半で紹介します。
これでアプリケーションのホームページをリフレッシュすることができます。今回は、これまでとは違うものを目にするでしょう:
Play は新しいモデルが追加されたことを自動的に検知し、そのための evolution を生成しました。evolution は、アプリケーションにおけるデータベーススキーマをある状態から次の状態へ移行する SQL スクリプトです。今回の例では、データベースは空の状態であり、Play はテーブルを作成するスクリプトを生成します。これから開発の初期段階の間は、Play にこれらのスクリプトを生成し続けてもらうことになるでしょう。開発プロジェクトの後半には、これらのスクリプトを Play に代わって自分で書いていくことになります。そのどの場合でもこのメッセージが表示されるので、安全に apply ボタンを押すことができます。
Play を再起動するするたびに evolution を適用する心配に気を揉みたくない場合は、
play
コマンド実行時に-DapplyEvolutions.default=true
引数を追加することで、このプロンプトを無効にすることができます。
§はじめてのテストの作成
新規に作成した User クラスをテストする良い方法は、JUnit テストケースを書くことです。これによりアプリケーションをくり返し仕上げ、すべてがばっちりであることを確信できるようになります。
test/models/ModelsTest.java
という新しいファイルを作成してください。テストを作成し、実行する準備を整えるため、インメモリデータベースを使用するようアプリケーションを設定することから始めます:
package models;
import models.*;
import org.junit.*;
import static org.junit.Assert.*;
import play.test.WithApplication;
import static play.test.Helpers.*;
public class ModelsTest extends WithApplication {
@Before
public void setUp() {
start(fakeApplication(inMemoryDatabase()));
}
}
WithApplication
クラスを継承しました。これは必須ではありませんが、このクラスは、簡単にフェイクアプリケーションを起動し、さらにテストを実行するたびにアプリケーションを自動的にクリアする、start()
メソッドを提供します。
また、@Before
で注釈したメソッドも実装しました。このアノテーションは、このメソッドがそれぞれのテストの前に実行されることを意味します。今回の例の場合、新しい FakeApplication
を起動して、アプリケーションが新しいインメモリデータベースを使用するよう設定しています。インメモリデータベースを使用するので、それぞれのテストの前に新しいまっさらのデータベースが作成されますから、テスト毎にデータベースをクリアすることに気を揉む必要はありません。
いよいよ、行を追加すること、そしてそれを再び検索できることを確認するだけの最初のテストを書いていきます:
@Test
public void createAndRetrieveUser() {
new User("[email protected]", "Bob", "secret").save();
User bob = User.find.where().eq("email", "[email protected]").findUnique();
assertNotNull(bob);
assertEquals("Bob", bob.name);
}
email
が Bob のメールアドレスと一致する唯一のインスタンスを見つけるために、User.find
ファインダを使ってプログラムでクエリを組み立てているのが分かります。
このテストケースを実行するには、Play コンソールで Ctrl+D
を押して実行中のアプリケーションを終了したことを確認してから、test
を実行します。テストは成功するはずです。
ユーザを問い合わせるこの find
オブジェクトは、コードのどこででも使うことができますが、アプリケーション中にコードを散らかすのは良いプラクティスではありません。必要なのはユーザを認証するクエリです。User.java
に authenticate()
メソッドを追加します:
public static User authenticate(String email, String password) {
return find.where().eq("email", email)
.eq("password", password).findUnique();
}
これでテストケースは以下のようになります:
@Test
public void tryAuthenticateUser() {
new User("[email protected]", "Bob", "secret").save();
assertNotNull(User.authenticate("[email protected]", "secret"));
assertNull(User.authenticate("[email protected]", "badpassword"));
assertNull(User.authenticate("[email protected]", "secret"));
}
変更を行うたびに Play テストランナーですべてのテストを実行し、なにも壊れていないことを確認することができます。
上記の認証コードはパスワードを平文で保存しています。これはとても悪いプラクティスで、パスワードは保存される前にハッシュ化するべきですし、問い合わせの前にもハッシュ化するべきです。しかし、これはこのチュートリアルの範囲外です。
§Project クラス
Project
クラスは、複数のタスクがその一部となり得るプロジェクトを表現します。ひとつのプロジェクトには、そのプロジェクト内のタスクにアサインすることのできるメンバーのリストも存在します。さっそく実装してみましょう:
package models;
import java.util.*;
import javax.persistence.*;
import play.db.ebean.*;
@Entity
public class Project extends Model {
@Id
public Long id;
public String name;
public String folder;
@ManyToMany(cascade = CascadeType.REMOVE)
public List<User> members = new ArrayList<User>();
public Project(String name, String folder, User owner) {
this.name = name;
this.folder = folder;
this.members.add(owner);
}
public static Model.Finder<Long,Project> find = new Model.Finder(Long.class, Project.class);
public static Project create(String name, String folder, String owner) {
Project project = new Project(name, folder, User.find.ref(owner));
project.save();
project.saveManyToManyAssociations("members");
return project;
}
public static List<Project> findInvolving(String user) {
return find.where()
.eq("members.email", user)
.findList();
}
}
あるプロジェクトには、ひとつの名前、格納されるひとつのフォルダ、そして複数のメンバーが存在します。今回もまた、クラスに @Entity
アノテーションがあること、Model
を継承していること、id
フィールドに @Id
アノテーションがあること、クエリを実行する find
があることが分かります。User
クラスを @ManyToMany
として定義することで、関連も定義しました。これは、それぞれのユーザは複数のプロジェクトのメンバーになれること、そしてそれぞれのプロジェクトは複数のメンバーを保持できることを意味します。
create()
メソッドも実装しました。members
の多対多の関連は明示的に保存する必要があることに注意してください。id
プロパティを実質的に設定していないことにも注目してください。これは、データベースに id を生成させているからです。
最後に、特定のユーザを含むすべてのプロジェクトを探すもうひとつのクエリメソッドを実装しました。ドット表記を使って、members
リスト中の User
の email
プロパティを参照していることが分かるでしょう。
ここで、Project
クラスとそのクエリをテストする新しいテストを ModelsTest
クラスに書きましょう:
@Test
public void findProjectsInvolving() {
new User("[email protected]", "Bob", "secret").save();
new User("[email protected]", "Jane", "secret").save();
Project.create("Play 2", "play", "[email protected]");
Project.create("Play 1", "play", "[email protected]");
List<Project> results = Project.findInvolving("[email protected]");
assertEquals(1, results.size());
assertEquals("Play 2", results.get(0).name);
}
コンパイルエラーが発生しないよう
java.util.List
のインポートを 忘れないでください 。
§最後の Task
モデル設計に必要な、最後の、そしてもっとも重要なこと、それは task です。
package models;
import java.util.*;
import javax.persistence.*;
import play.db.ebean.*;
@Entity
public class Task extends Model {
@Id
public Long id;
public String title;
public boolean done = false;
public Date dueDate;
@ManyToOne
public User assignedTo;
public String folder;
@ManyToOne
public Project project;
public static Model.Finder<Long,Task> find = new Model.Finder(Long.class, Task.class);
public static List<Task> findTodoInvolving(String user) {
return find.fetch("project").where()
.eq("done", false)
.eq("project.members.email", user)
.findList();
}
public static Task create(Task task, Long project, String folder) {
task.project = Project.find.ref(project);
task.folder = folder;
task.save();
return task;
}
}
それぞれのタスクには、生成された id とタイトル、そのタスクが完了されたか否かを示すフラグ、そのタスクが完了されるべき日付、もしアサインされていればユーザー、そしてフォルダとプロジェクトが存在します。assignedTo
と project
の関連は @ManyToOne
を使ってマッピングされています。これは、ユーザはそれぞれアサインされた複数のタスクを持ち、プロジェクトはそれぞれ複数のタスクを持つ一方で、あるタスクは一人のユーザとひとつのプロジェクトを持つことを意味します。
シンプルな - 今回はすべての todo タスクを見つけ出すクエリもあります。すなわち、まだ完了されておらず、特定のユーザーがアサインされたものであり、これに加えて create メソッドも用意されています。
このクラスのテストも同じように書いてみましょう:
@Test
public void findTodoTasksInvolving() {
User bob = new User("[email protected]", "Bob", "secret");
bob.save();
Project project = Project.create("Play 2", "play", "[email protected]");
Task t1 = new Task();
t1.title = "Write tutorial";
t1.assignedTo = bob;
t1.done = true;
t1.save();
Task t2 = new Task();
t2.title = "Release next version";
t2.project = project;
t2.save();
List<Task> results = Task.findTodoInvolving("[email protected]");
assertEquals(1, results.size());
assertEquals("Release next version", results.get(0).title);
}
§Fixture を使ったより複雑なテスト
より複雑なテストを書き始める場合、しばしばテストに使うデータセットが必要になります。Java クラスを作成して保存することはとても面倒になりがちです。このため、Play では Java オブジェクトを定義するためにデータを簡単に宣言することのできる YAML ファイルが簡単に使えるようになっています。データを宣言するときは、定義しているデータのモデルクラスを指定するために、!!
型演算子 を使うことを忘れないでください。
conf/test-data.yml
ファイルを編集して User を定義してみましょう:
- !!models.User
email: [email protected]
name: Bob
password: secret
...
このオブジェクトが、リストであるルートオブジェクトの一部として定義されていることに注意してください。これで更に多くのオブジェクトを定義することができますが、データセットは少々大きいので、ここ から完全なデータセットをダウンロードすることができます。
それでは、このデータをロードしていくつかのアサーションを実行するテストケースを作成します。
@Test
public void fullTest() {
Ebean.save((List) Yaml.load("test-data.yml"));
// Count things
assertEquals(3, User.find.findRowCount());
assertEquals(7, Project.find.findRowCount());
assertEquals(5, Task.find.findRowCount());
// Try to authenticate as users
assertNotNull(User.authenticate("[email protected]", "secret"));
assertNotNull(User.authenticate("[email protected]", "secret"));
assertNull(User.authenticate("[email protected]", "badpassword"));
assertNull(User.authenticate("[email protected]", "secret"));
// Find all Bob's projects
List<Project> bobsProjects = Project.findInvolving("[email protected]");
assertEquals(5, bobsProjects.size());
// Find all Bob's todo tasks
List<Task> bobsTasks = Task.findTodoInvolving("[email protected]");
assertEquals(4, bobsTasks.size());
}
テストデータが全てのテストから利用できるように、
@Before
メソッドでテストデータをロードする方がより便利だと気づくかもしれません。
§作業内容の保存
ここまででタスク管理システムの大きな部分をやり終えました。これらを作成し、すべてテストしており、web アプリケーションそれ自身の開発を始めることができます。
しかし、開発を続ける前に作業内容を git を使って保存しましょう。コマンドラインを開いて git status
とタイプして、最後のコミットから変更された内容を確認しました:
$ git status
ご覧の通り、いくつかの新しいファイルがバージョン管理されていません。すべてのファイルを追加してプロジェクトをコミットしてください。
$ git add .
$ git commit -m "The model layer is ready"
次章 に進みましょう