開発者:java
JPAの試用
Samudra Gupta著
ケーススタディ:Java Persistence API (JPA)の使用と配置
2006年11月公開
2006年夏にリリースされたEJB 3.0仕様は、大幅に簡素化されたものの極めて強力なEJBフレームワークを実現します。これは、このフレームワークが従来のEJB 2.xデプロイメント・ディスクリプタよりもアノテーションを優先にしていることを示します。J2SE 5.0で導入されたアノテーションは、クラス、フィールド、メソッド、パラメータ、ローカル変数、コンストラクタ、列挙、パッケージなどに使用できる修飾子です。アノテーションは、主にEJB 3.0で導入された多数の新機能、具体的には、Plain Old Java ObjectベースのEJBクラス、EJBマネージャ・クラスの依存性注入、他のビジネス・メソッドの呼び出しに割り込むことができるインターセプタまたはメソッドの導入、大幅に改善されたJava Persistence API (JPA)などで使用されています。
JPAの概念を説明するために、実例を確認します。最近、オフィスでは、税登録システムを導入する必要が生じました。どのシステムでも言えますが、このシステムも独自の複雑で困難な部分が見られました。このシステムの困難な部分は、データ・アクセスとオブジェクト・リレーショナル・マッピング(ORM)にあるので、システムを実装しながら新しいJPAを試してみることにしました。
システムの開発中、次のような問題に直面しました。
- アプリケーションで使用するエンティティ間にリレーションシップが存在する。
- リレーショナル・データ全体にわたって複雑な検索をサポートする必要がある。
- データの整合性を維持する必要がある。
- データを永続化する前に検証する必要がある。
- バルク操作が要求される。
データ・モデル
まず、簡略化されたリレーショナル・データ・モデルから見ていきます。簡略化されているとはいえ、JPAのニュアンスを説明するには十分です。ビジネス的側面から見ると、主となる申請者が税登録申請書を提出します。この申請者には、ゼロか、あるいは1人以上の関係者がいます。申請者と関係者は、登録住所と取引住所の2つを指定する必要があります。主となる申請者は、過去に科せられた罰金についても申告および記述する必要があります。
エンティティの定義 - 次のエンティティを各表にマッピングして定義しました。
エンティティ |
マッピングされる表 |
Registration |
REGISTRATION |
Party |
PARTY |
Address |
ADDRESS |
Penalty |
PENALTY |
CaseOfficer |
CASE_OFFICER |
表 1.
エンティティ-表マッピング
データベース表と列をマッピングするエンティティを特定するのは簡単です。以下に、Registrationの簡略化された例を示します(このエンティティのその他のマッピングと設定については後で説明します)。
@Entity
@Table(name="REGISTRATION")
public class Registration implements Serializable{
@Id
private int id;
@Column(name="REFERENCE_NUMBER")
private String referenceNumber;
..........
}
JPAエンティティを使用する主な利点は、通常のJavaクラスと同様に簡単にコーディングできることです。複雑なライフサイクル・メソッドは必要ありません。アノテーションを使用して、永続化機能をエンティティに割り当てることができます。Data Transfer Objects (DTO)という余分なレイヤーも必要なくなりました。エンティティを再利用して、レイヤー間を移動することもできます。データを簡単に持ち運べるようになったのです。
ポリモフィズムのサポート - データ・モデルを見直してみると、PARTY表にApplicant(申請者)とPartner(関係者)の両レコードを格納していることが分かります。これらのレコードは共通の属性を持っていると同時に、それぞれに固有の属性も持っています。
そこで、このモデルを継承階層でモデル化してみることにします。EJB 2.xでは、Party Entity Beanを1つだけ使用して、コード内にロジックを実装することでparty_typeの値に応じてApplicantオブジェクトまたはPartnerオブジェクトを作成していました。JPAでは、エンティティ・レベルで継承階層を指定できます。
そこで、1つの抽象エンティティであるPartyと、2つの具象エンティティのPartnerおよびApplicantを使用して、継承階層をモデル化することにしました。
@Entity
@Table(name="PARTY_DATA")
@Inheritance(strategy= InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name="PARTY_TYPE")
public abstract class Party implements Serializable{
@Id
protected int id;
@Column(name="REG_ID")
protected int regID;
protected String name;
.........
}
これで、2つの具象クラスのPartnerおよびApplicantが、抽象クラスのPartyから継承するようになります。
@Entity
@DiscriminatorValue("0")
public class Applicant extends Party{
@Column(name="TAX_REF_NO")
private String taxRefNumber;
@Column(name="INCORP_DATE")
private String incorporationDate;
........
}
永続性プロバイダは、party_type列の値が0の場合にはApplicantエンティティのインスタンスを、同列の値が1の場合には、Partnerエンティティのインスタンスを返します。
リレーションシップの構築 - アプリケーション・データ・モデル内のPARTY表には、REGISTRATION表に対する外部キー列(reg_id)があります。この構造では、Partyエンティティがエンティティの所有側、すなわちリレーションシップのソースになります。Partyエンティティで結合列を指定しているからです。REGISTRATION表はリレーションシップのターゲットになります。
ManyToOneのリレーションシップでは、リレーションシップが双方向になることが多くなります。すなわち、2つのエンティティ間にOneToManyのリレーションシップが存在するということです。次の表に、リレーションシップ定義を示します。
>
リレーションシップ |
所有側 |
多様性/マッピング |
Registration->CaseOfficer |
CaseOfficer |
OneToOne |
Registration->Party |
Party |
ManyToOne |
Party->Address |
Address |
ManyToOne |
Party->Penalty |
Penalty |
ManyToOne |
リレーションシップの反対側 |
Registration->CaseOfficer |
|
OneToOne |
Registration->Party |
|
OneToMany |
Party->Address |
|
OneToMany |
Party->Penalty |
|
OneToMany |
表 2.
リレーションシップ
public class Registration implements Serializable{
....
@OneToMany(mappedBy = "registration")
private Collection<Party> parties;
....
}
public abstract class Party implements Serializable{
....
@ManyToOne
@JoinColumn(name="REG_ID")
private Registration registration;
....
注:mappedBy要素は、結合列がリレーションシップの他方の端に指定されていることを示します。
次に、JPA仕様の定義と永続性プロバイダの実装に従って、リレーションシップの動作を検討する必要があります。関連データのフェッチ方法には、EAGERとLAZYのどちらを使用すればよいでしょうか。JPAに定義されているリレーションシップのデフォルトのFETCHタイプを調べ、表2に新しい列を追加して、表3にその結果を示しました。
リレーションシップ |
所有側 |
多様性/マッピング |
デフォルトのFETCHタイプ |
Registration->CaseOfficer |
CaseOfficer |
OneToOne |
EAGER |
Party->Registration |
Party |
ManyToOne |
EAGER |
Address->Party |
Address |
ManyToOne |
EAGER |
Penalty->Party |
Penalty |
ManyToOne |
EAGER |
|
|
|
|
リレーションシップの反対側 |
Registration->Party |
|
OneToMany |
LAZY |
Party->Address |
|
OneToMany |
LAZY |
Party->Penalty |
|
OneToMany |
LAZY |
表 3.デフォルトのFETCHタイプの設定
ビジネス要件を見ると、Registrationの詳細を取得した際、その登録内容に関係するPartyの詳細も常に表示する必要があるようです。しかし、FETCHタイプがLAZYに設定されているため、データを取得する際にデータベースを繰り返し呼び出す必要があります。これは、Registration→PartyリレーションシップのFETCHタイプをEAGERに変更すると、パフォーマンスが向上することを示唆しています。FETCHタイプをEAGERに設定すると、永続性プロバイダは関連データを単一のSQLの一部として返すようになります。
同様に、画面にPartyの詳細を表示する際にも、対応する住所を表示する必要があります。したがって、Party-Addressリレーションシップでも、FETCHタイプをEAGERに変更することは理に適っています。
一方、Party→PenaltyリレーションシップのFETCHタイプはLAZYに設定しても問題ありません。ユーザーが要求しないかぎり罰金の詳細を表示する必要がないからです。このリレーションシップのFETCHタイプをEAGERに設定すると、m個のPARTYに対してそれぞれn個のPenaltyが対応するため、m*n個のPenaltyエンティティをロードすることになり、不要に大きなオブジェクト・グラフが生成され、パフォーマンスの低下に繋がります。
public class Registration implements Serializable{
@OneToMany(mappedBy = "registration", fetch = FetchType.EAGER)
private Collection<Party> parties;
.....
}
public abstract class Party implements Serializable{
@OneToMany (mappedBy = "party", fetch = FetchType.EAGER)
private Collection<Address> addresses;
@OneToMany (mappedBy = "party", fetch=FetchType.LAZY)
private Collection<Penalty> penalties;
.....
}
レイジー・リレーションシップへのアクセス - レイジーなロード方法を検討する場合は、永続性コンテキストの有効範囲を考慮します。EXTENDED永続性コンテキストとTRANSACTIONスコープの永続性コンテキストのどちらかを選択できます。EXTENDED永続性コンテキストはトランザクションの境界を越えて存続するため、ステートフルSession Beanのように動作します。
このアプリケーションは対話型ではないため、トランザクションの境界を越えて永続性コンテキストを保持する必要はありません。したがって、TRANSACTIONスコープの永続性コンテキストを選択することにしました。しかし、これはレイジー・ローディングで問題となります。エンティティを取得してトランザクションが終了すると、そのエンティティは分離された状態になります。このアプリケーションでは、レイジーにロードされたリレーションシップ・データをロードしようとすると、未定義の動作になってしまいます。
多くの場合、ケースワーカーが登録データを取り出す場合、罰金履歴まで表示する必要はありません。しかし、管理者が操作している場合は、罰金レコードを追加表示する必要があります。ほとんどのケースで罰金履歴を表示する必要はないことを考えると、リレーションシップのFETCHタイプをEAGERに変更することは理に反しています。そこで、マネージャがシステムを使用していることを検出し、そのときのみリレーションシップのレイジー・ロードを起動する方法を検討します。この方法であれば、エンティティが分離されており、後で参照可能になる場合でも、リレーションシップ・データを利用できます。次の例で、この考え方を説明します。
Registration registration = em.find(Registration.class, regID);
Collection<Party> parties = registration.getParties();
for (Iterator<Party> iterator = parties.iterator(); iterator.hasNext();) {
Party party = iterator.next();
party.getPenalties().size();
}
return registration;
上の例では、Partyエンティティの罰金コレクションのsize()メソッドを呼び出しているだけです。このメソッド呼び出しによって、レイジー・ロードが起動され、Registrationエンティティが分離されている状態でも、すべてのコレクションが読み込まれ、利用可能になります(あるいは、JP-QLの特殊な機能FETCH JOINを使用する方法もあります。これについては後述します)。
リレーションシップと永続性
次に、永続データのコンテキストでのリレーションシップの動作方法を検討します。本質において、リレーションシップ・データを変更する場合、オブジェクト・レベルでその変更を行い、変更されたデータを永続性プロバイダに保持しておきたいのです。JPAでは、カスケード・タイプを使用して永続性動作を制御します。
JPAには、次の4つのカスケード・タイプが定義されています。
- PERSIST:所有側エンティティが永続化された場合、その関連データもすべて永続化されます。
- MERGE:分離状態のエンティティがアクティブな永続性コンテキストに戻された場合、その関連データもすべて永続性コンテキストに戻されます。
- REMOVE:所有側エンティティが削除された場合、その関連データもすべて削除されます。
- ALL:上記すべてが適用されます。
エンティティの作成 - すべてのケースで、新しい親エンティティを作成した場合は、そのすべての関連子エンティティも自動的に永続化される方法を選択しました。これによりコーディングが容易になります。リレーションシップ・データを正しく設定すれば、各エンティティに対して別々にpersist()操作を呼び出す必要がないからです。
したがって、PERSISTカスケード・タイプが最も適切な選択ということになります。そこで、PERSISTカスケード・タイプを使用するように、すべてのリレーションシップ定義をリファクタリングしました。
エンティティの更新 - あるトランザクション内でデータを取得し、そのトランザクション外でエンティティを変更して、その変更を永続化するという操作は、一般的に行われます。たとえば、このアプリケーションでは、ユーザーは既存の登録内容を取得し、主となる申請者の住所を変更することができます。既存のRegistrationエンティティを取得し、その結果、すべての関連データを同じトランザクション内で取得すると、そのトランザクションはそこで終了し、データはプレゼンテーション・レイヤーに渡されます。この時点で、Registrationおよび他のすべての関連エンティティ・インスタンスは永続性コンテキストから分離された状態になります。
JPAでは、分離されたエンティティに対する変更を永続化するために、EntityManagerのmerge()操作を使用します。また、変更内容をリレーションシップ・データに反映するために、リレーションシップ・マッピングの構成で定義したカスケード・タイプに加えて、すべてのリレーションシップ定義にMERGEカスケード・タイプを含める必要があります。
こうした背景を踏まえて、すべてのリレーションシップ定義のカスケード・タイプが正しく指定されていることを確認しました。
エンティティの削除 - 次に、特定のエンティティを削除したときの処理を決定する必要があります。たとえば、Registrationエンティティを削除した場合は、そのRegistrationエンティティに関するすべてのPartyを削除しても問題ありません。しかし、その逆はそうではありません。ここで要領よく作業するために、リレーションシップに対するremove()操作をカスケードすることで、削除してはならないエンティティまで削除してしまうのを防ぎます。次のセクションで確認できますが、こうした操作は参照整合性制約のために失敗する可能性があります。
以上により、PartyとAddress、PartyとPenaltyなど、OnetoMany関係に従う明らかな親子リレーションシップでは、REMOVEカスケード・タイプをリレーションシップの親側(ONE)だけに指定したほうが安全であるという結論に達しました。そこで、それに応じてリレーションシップ定義をリファクタリングしました。
public abstract class Party implements Serializable{
@OneToMany (mappedBy = "party", fetch = FetchType.EAGER, cascade =
{CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE})
private Collection<Address> addresses;
@OneToMany (mappedBy = "party", fetch=FetchType.LAZY, cascade =
{CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE})
private Collection<Penalty> penalties;
.....
}
リレーションシップの管理
JPA仕様によると、リレーションシップの管理はプログラマの責任で行う必要があります。永続性プロバイダは、リレーションシップ・データの状態について何も想定しないため、リレーションシップを管理しようとしません。
この事実を踏まえて、リレーションシップを管理し、潜在的な問題領域を絞り込むための戦略を見直しました。その結果、次のことが分かりました。
- 親と子の間にリレーションシップを設定しようとした場合に、親が(別のユーザーに削除されるなどして)データベース内に存在していないと、データ整合性の問題を招く。
- 事前に子レコードを削除せずに親レコードを削除しようとすると、参照の整合性が崩れる。
そこで、次のようなコーディング上の指針を設定しました。
- トランザクション内で、あるエンティティとその関連エンティティを取得し、そのトランザクションの外でリレーションシップを変更してから、新しいトランザクション内でその変更内容を永続化する場合は、その親エンティティを再フェッチするのが最善の方法である。
- 子レコードを削除せずに親レコードを削除する場合は、親レコードを削除する前にすべての子レコードの外部キーフィールドをNULLに設定する必要がある。
CaseWorkerとRegistrationの間のOneToOneリレーションシップについて検討します。特定のRegistrationレコードを削除してもCaseWorkerレコードを削除することはありません。したがって、Registrationレコードを削除する前にreg_id外部キーをnullに設定する必要があります。
@Stateless
public class RegManager {
.....
public void deleteReg(int regId){
Registration reg = em.find(Registration.class, regId);
CaseOfficer officer =reg.getCaseOfficer();
officer.setRegistration(null);
em.remove(reg);
}
}
データ整合性
あるユーザーが登録レコードを表示しているとき、別のユーザーが同じ申請を変更する場合があります。このとき、前者のユーザーが申請に対して追加の変更を行うと、(後者のユーザーが)変更したデータを、知らずに古いデータで上書きしてしまう危険性があります。
この問題に対処するため、"楽観的ロック"を使用することにしました。JPAでは、エンティティにバージョニング列を定義できます。これを使用して楽観的ロックを実装できます。
public class Registration implements Serializable{
@Version
private int version;
.....
}
永続性プロバイダはバージョニング列のインメモリ値とデータベースのインメモリ値を照合し、2つの値が異なっていると、例外を発生させます。
検証
主となる申請者は少なくとも1つの住所を指定し、その住所には少なくとも第1行と郵便番号が含まれている必要があると書いた場合、このビジネス・ルールは、PartyエンティティとAddressエンティティの両方に適用されます。しかし、住所の各行は100文字以下でなければならないと書けば、この検証はAddressエンティティだけに適用されます。
このアプリケーションでは、複数オブジェクトにまたがるビジネス・ルールの検証をSession Beanレイヤーに実装します。ほとんどのワークフローとプロセス指向のロジックはSession Beanレイヤーにコーディングされるからです。一方、特定のエンティティ固有の検証については、エンティティ内で行うことにしました。JPAを使用すると、任意のメソッドをエンティティのライフ・サイクル・イベントに関連付けることができます。
次の例では、各Address行が100文字以下であることを検証し、(@PrePersistアノテーションを使用して)Addressエンティティを永続化する前にこの検証メソッドを呼び出しています。検証に不合格になると、このメソッドは呼び出し元にビジネス例外(ランタイム例外クラスから拡張)をスローします。呼び出し元はその例外情報を使用してユーザーにメッセージを送ります。
public class Address implements Serializable{
.....
@PrePersist
public void validate()
if(addressLine1!=null && addressLine1.length()>1000){
throw new ValidationException("Address Line 1 is longer than 1000 chars.");
}
}
検索
この税登録アプリケーションでは、特定の登録、その関係者、その他の詳細を検索する検索機能を提供しました。効率的な検索機能を提供するには、多くの問題を解決する必要があります。たとえば、効率的な問合せを書いたり、長い結果リストをブラウズするためのページ区切りを実装したりといったことです。JPAでは、データ・アクセスを実装するために、Java Persistence Query Language (JP-QL)をエンティティに対して使用するよう規定しています。JP-QLはEJB 2.xのEJB QLを大幅に改良したものです。JP-QLを使用して、効率的なデータ・アクセスの仕組みを実現することができました。
問合せ
JPAでは、問合せを動的に作成するか、静的に定義するかを選択できます。これらの静的な問合せまたは名前付き問合せでは、パラメータを指定できます。パラメータ値は実行時に割り当てられます。問合せの有効範囲は明確に定義されているので、名前付き問合せにパラメータを指定して使用することにしました。名前付き問合せを使用すると、永続性プロバイダが変換済みのSQL問合せをキャッシュに格納しておき、後で利用できるので、効率性が向上します。
このアプリケーションでは、次のような簡単なユースケースを用意しました。ユーザーが申請参照番号を入力して登録の詳細を取得するケースです。Registrationエンティティに対して指定した名前付き問合せは次のとおりです。
@Entity
@Table(name="REGISTRATION")
@NamedQuery(name="findByRegNumber", query = "SELECT r FROM REGISTRATION r WHERE r.appRefNumber=?1")
public class Registration implements Serializable{
.....
}
たとえば、アプリケーション内のある検索要件は特に注意を要するものでした。それは、すべての関係者をその罰金総額といっしょに検索するレポート問合せです。アプリケーションでは、罰金履歴のない関係者の作成も許可しているため、単純なJOIN操作では罰金履歴のない関係者がリストされません。この問題を解決するために、JP-QLのOUTER JOIN機能を使用しました。また、罰金の累計を算出するためにGROUP BY句を使用しました。Partyエンティティに対して指定した名前付き問合せは次のとおりです。
@Entity
@Table(name="PARTY_DATA")
@Inheritance(strategy= InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name="PARTY_TYPE")
@NamedQueries({@NamedQuery(name="
generateReport",
query=" SELECT NEW com.ssg.article.ReportDTO(p.name, SUM(pen.amount))
FROM Party p LEFT JOIN p.penalties pen GROUP BY p.name""),
@NamedQuery(name="bulkInactive",
query="UPDATE PARTY P SET p.status=0 where p.registrationID=?1")})
public abstract class Party {
.....
}
上の例の"generateReport"という名前付き問合せでは、問合せの内部で新しいReportDTOオブジェクトをインスタンス化している点に注意してください。これは、JPAの強力な機能です。
バルク操作も可能
このアプリケーションでは、担当係が登録を非アクティブにして検索を行うことができます。その場合、そのRegistrationに対するすべての関連Partyも非アクティブにする必要があります。これは、通常、PARTY表のStatus列を0に設定することで行います。この処理では、パフォーマンスを向上させるため、Partyごとに個別にSQL文を実行する代わりにバッチ更新を使用したいところです。
JPAにはバッチ処理を行うための方法が用意されています。
@NamedQuery(name="bulkInactive", query="UPDATE PARTY p SET p.status=0 where p.registrationID=?1")
public abstract class Party implements Serializable{
.....
}
注:バルク操作ではSQLを直接データベースに対して発行します。したがって、永続性コンテキストには変更が反映されません。単一のトランザクション範囲を越えて存続する拡張永続性コンテキストを使用している場合は、キャッシュ内エンティティに古いデータが残っている可能性があります。
取得の高速化
もう1つの困難な要件は、選択的なデータ表示です。たとえば、責任者が登録を検索する場合、登録済み関係者のすべての罰金を表示する必要があります。しかし、一部の登録については、登録済み関係者のすべての罰金をケースワーカーにも表示する必要があります。この情報は、普通ならば、通常のケースワーカーには表示されないところです。
PartyとPenalty間のリレーションシップはOneToManyです。前述したとおり、このリレーションシップのデフォルトのフェッチ・タイプはLAZYです。しかし、上記のような検索の選択的表示要件を満たすには、複数回のSQLコールを避けるために、Penaltyの詳細を1つのSQL文でフェッチするのが理に適っています。
そこで役に立つのがJP-QLのFETCH Join機能です。FETCH Joinは、LAZYフェッチ・タイプを一時的に上書きしたいときに使用します。しかし、FETCH Joinを頻繁に使用する場合は、フェッチ・タイプをEAGERにするリファクタリングを検討したほうが賢明です。
@NamedQueries({@NamedQuery(name="generateReport",
query=" SELECT NEW com.ssg.article.ReportDTO(p.name, SUM(pen.amount))
FROM Party p LEFT JOIN p.penalties pen GROUP BY p.name""),
@NamedQuery(name="bulkInactive",
query="UPDATE PARTY P SET p.status=0 where p.registrationID=?1"),
@NamedQuery(name="getItEarly", query="SELECT p FROM Party p JOIN FETCH p.penalties")})
public abstract class Party {
.....
}
結論
JPAによって永続性のコーディングが大幅に簡素化されました。JPAは機能が豊富で極めて効率的です。その豊富な問合せインタフェースと大幅に改善された問合せ言語によって、複雑なリレーショナルを使用するケースにも容易に対応できます。また、継承サポートによって、論理ドメイン・モデルを永続性レベルでも維持できるので、同じエンティティを複数のレイヤーにまたがって再利用できます。こうした利点を考慮すると、JPAは最適な選択と言えます。
Samudra Gupta氏は、イギリス在住のフリーランスのJava/J2EEコンサルタントです。公的機関、小売業、国家安全保障システムなどで、9年以上に渡るJava/J2EEアプリケーションの開発経験があります。著書に『
Pro Apache Log4j』 (Apress, 2005年)があり、『Javaboutique』、『Javaworld』、および『
Java Developers Journal』などの各誌にも記事を寄稿しています。趣味はブリッジとボーリングです。
|