ドメインオブジェクトモデル
モデルは Play アプリケーションの中心的な存在です。モデルは、アプリケーションが操作する情報のドメインに特化した表現です。
マーチンファウラーはこれを次のように定義しています:
ビジネスのコンセプト、ビジネスの状況に関する情報、およびビジネスルールを表現する責務を負います。これを保存する技術的な詳細はインフラストラクチャに委譲されるものの、ビジネスの状況を反映する状態はここで制御され、使用されます。この層は業務ソフトウェアの心臓部です。
一般的な Java のアンチパターンは、モデルをシンプルな Java Beans のセットに保ち、モデルオブジェクトを操作する “service” 層にアプリケーションロジックを戻すことです。
マーチンファウラーはこのアンチパターンを ドメインモデル貧血症 と名付けました:
ドメインモデル貧血症の基本的な症状は、一見、それが本物のドメインモデルに見えるという点です。オブジェクトがいくつかあり、それらはドメイン空間にある名詞から名前をつけられています。それから、オブジェクト同士がしっかりとしたリレーションで結びついており、本物のドメインモデルと同じような構造を持っているのです。ただし、オブジェクトの振る舞いを見れば違いが分かります。それらのオブジェクトにはわずかな振る舞いしかない、ということに気づくと思います。ドメインのロジックをドメインオブジェクトの中に入れないという設計ルールに従っているのでしょう。その代わり、すべてのドメインロジックを含むサービスオブジェクト群が存在しているのです。こういったサービスはドメインモデルの上位に居座り、データのためだけにドメインモデルを使うのです。
このアンチパターンが根本的に怖いのは、オブジェクト指向設計の基本概念 (データと処理を一緒にする) の真逆だということです。ドメインモデル貧血症とは、単なる手続き型設計のことなのです。まさに、私 (そして Eric) のようなオブジェクト信望者が、Smalltalk の初期からずーーーーーっと戦ってきたもの、そのものなのです。さらに困ったことに、貧血オブジェクト (Anemic Object) が本物のオブジェクトだと思っているひとがたくさんいます。つまり、オブジェクト指向設計が何たるかをまったく分かっていないということなんです。
プロパティのシミュレーション
Play のサンプルアプリケーションを見てみると、クラスが public 変数を宣言しているのをしばしば目にするでしょう。どのような経験があろうと、Java 開発者であれば public 変数を目にした途端に警告サイレンがじゃんじゃん鳴り出すかもしれません。Java においては (他のオブジェクト指向言語と同様に) すべてのフィールドを private にして、アクセサとミューテータを提供するのを最も良い習慣としています。これは、オブジェクト指向デザインにおいて重要な概念であるカプセル化を促進するためのものです。
Java には、本当の意味でプロパティを定義する組み込みのシステムはありません。Java Beans と名付けられた規約を使用します: Java オブジェクトのプロパティは getXxx/setXxx メソッドのペアと共に定義するという規約です。プロパティが読み取り専用の場合、getter しかありません。
システムはうまく動きますが、書くのは非常に退屈です。いずれのプロパティについても、private で宣言して 2 つのメソッドを書かなければなりません。ほとんどの場合、getter と setter の実装はいつも同じです:
private String name;
public String getName() {
return name;
}
public void setName(String value) {
name = value;
}
Play フレームワークの Model 部分は、自動的にこのパターンを生成し、コードを簡潔に保ちます。事実上、すべての public 変数はインスタンスのプロパティになります。規約では、クラスの public で static ではなく 、 final でない フィールドはすべてプロパティと見なされます。
例えば、このようにクラスを定義する場合:
public class Product {
public String name;
public Integer price;
}
ロードされたクラスは以下のようになります:
public class Product {
public String name;
public Integer price;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getPrice() {
return price;
}
public void setPrice(Integer name) {
this.price = price;
}
}
プロパティにアクセスしたい場合は、以下のように書くだけです:
product.name = "My product";
product.price = 58;
ロード時には以下のように変換されます:
product.setName("My product");
product.setPrice(58);
警告!
自動生成に頼る場合、プロパティにアクセスするために getter および setter メソッドを直接使用することはできません。これらのメソッドは実行時に生成されます。このため、それらを参照するコードを書くと、コンパイラはメソッドを見つられず、エラーを発生させます。
もちろん、自分で getter および setter メソッドを定義することができます。メソッドが存在している場合、Play は既存のアクセサを使用します。
これらから、Product クラスの price プロパティを保護するには、以下のように書くことができます:
public class Product {
public String name;
public Integer price;
public void setPrice(Integer price) {
if (price < 0) {
throw new IllegalArgumentException("Price can’t be negative!");
}
this.price = price;
}
}
このプロパティに負の値を設定しようとすると、例外が投げられます:
product.price = -10: // Oops! IllegalArgumentException
Play は getter または setter が存在すれば、常にそれを使用します。次のコードを見てください:
@Entity
public class Data extends Model {
@Required
public String value;
public Integer anotherValue;
public Integer getAnotherValue() {
if(anotherValue == null) {
return 0;
}
return anotherValue;
}
public void setAnotherValue(Integer value) {
if(value == null) {
this.anotherValue = null;
} else {
this.anotherValue = value * 2;
}
}
public String toString() {
return value + " - " + anotherValue;
}
}
別のクラスから、以下のようなアサーションを試すことができます:
Data data = new Data();
data.anotherValue = null;
assert data.anotherValue == 0;
data.anotherValue = 4
assert data.anotherValue == 8;
これはちゃんと動きます。拡張されたクラスは Java Beans の規約に従うので、JavaBean を期待するようなライブラリからオブジェクトを使用するような場合でも、それは完璧に動作します!
モデルを永続化するデータベースの設定
ほとんどの場合、モデルオブジェクトのデータを永続化して保存する必要があるでしょう。データをデータベースに保存するのは、もっとも自然なやり方です。
開発中、インメモリ、またはアプリケーション中のサブディレクトリにデータを保存する組み込みデータベースを素早くセットアップすることができます。
インメモリの HSQLDB データベースインスタンスを始めるには、単に conf/application.conf ファイルに次の一行を追加してください:
db=mem
データベースをファイルシステムに保存したい場合は、以下を使用してください:
db=fs
既存の MySQL5 server に接続する場合は、以下を使用してください:
db=mysql:user:pwd@database_name
Play の配布物は $PLAY_HOME/framework/lib/ ディレクトリに HSQLDB と MySQL の JDBC ドライバを含みます。例えば PostgreSQL または Oracle データベースを使用する場合は、 $PLAY_HOME/framework/lib/ または アプリケーションの lib/ ディレクトリに JDBC ドライバを追加します。
JDBC に対応したいかなるデータベースにも接続することができます。ライブラリを追加して、application.conf に JDBC プロパティを定義してください:
db.url=jdbc:mysql://localhost/test
db.driver=com.mysql.jdbc.Driver
db.user=root
db.pass=
設定オプションで JPA の方言を指定することもできます:
jpa.dialect=<dialect>
ソースコードにおいて、 play.db.DB から java.sql.Connection を取得して、通常の方法で使用することができます。
Connection conn = DB.getConnection();
conn.createStatement().execute("select * from products");
Hibernate によるモデルの永続化
Java オブジェクトを自動的にデータベースに永続化するために (JPA を通して) Hibernate を使用することができます。
どんな Java オブジェクトでも、 @javax.persistence.Entity アノテーションを追加して JPA エンティティとして定義した場合、Play は自動的に JPA EntityManager を起動します。
@Entity
public class Product {
public String name;
public Integer price;
}
警告!
JPA のものではなく、Hibernate の @Entity アノテーションを使用するのはよくある誤りです。Play は JPA API を通して Hibernate を使用することを思い出してください。
play.db.jpa.JPA オブジェクトから EntityManager を取得することができます:
EntityManager em = JPA.em();
em.persist(product);
em.createQuery("from Product where price > 50").getResultList();
Play は、JPA の扱いを手助けする気の利いたサポートクラスを提供します。 play.db.jpa.Model を継承するだけです。
@Entity
public class Product extends Model {
public String name;
public Integer price;
}
その後、Product インスタンスのシンプルなメソッドを使用して Product オブジェクトを操作してください:
Product.find("price > ?", 50).fetch();
Product product = Product.findById(2L);
product.save();
product.delete();
ステートレスモデル
Play は、‘何も共有しない’ アーキテクチャで機能するように設計されています。この考えは、アプリケーションを完全にステートレスに保ちます。これをすることによって、アプリケーションは同時に必要とされる複数のサーバノード上で動作するようになります。
モデルをステートレスに保つために避けるべき一般的な落とし穴はどのようなものでしょうか? いかなるオブジェクトも、複数リクエストのために Java ヒープに保存してはならないということです。
複数リクエストをまたいでデータを保持するには、いくつかの選択肢があります:
1. データが小さくて、十分に簡単である場合は、フラッシュかセッションスコープに保存してください。ただし、これらのスコープに保存できるデータはそれぞれ 4KB に制限され、かつ String 型のデータしか認められません。
2. (データベースのような) 永続的なストレージに、データを恒久的に保存してください。例えば “ウィザード” を使ってオブジェクトを作成するような場合、データは複数のリクエストに渡ります:
- 最初のリクエストでオブジェクトを初期化し、データベースに保存します。
- 新規に作成したオブジェクトの ID をフラッシュスコープに保存します。
- 連続するリクエストの間、オブジェクトの id を使ってデータベースからオブジェクトを取り出し、更新し、再度保存します。
3. (キャッシュのような) 一時的なストレージに、データを一時的に保存してください。例えば “ウィザード” を使ってオブジェクトを作成するような場合、データは複数のリクエストに渡ります:
- 最初のリクエストでオブジェクトを初期化し、キャッシュに保存します。
- 新規に作成したオブジェクトのキーをフラッシュスコープに保存します。
- 連続するリクエストの間、キャッシュから (正しいキーを持った) オブジェクトを取り出し、更新し、再度保存します。
- 一連の処理の最後のリクエストの終わりに、(例えばデータベースに) オブジェクトを恒久的に保存します。
キャッシュは信頼できるストレージではありませんが、キャッシュにオブジェクトを入れられるなら、それは検索できて然るべきです。要件によって、キャッシュは Java Servlet セッションを置き換えるとても良い選択肢となり得ます。
考察を続けます
今度は、 JPA 永続化 を使ってモデルを永続化する方法について確認しましょう。