目次


Google App Engine for Java

Google App Engine for Java: 第 3 回 永続化とリレーションシップ

Java ベースの永続化と Google App Engine データ・ストア

Comments

コンテンツシリーズ

このコンテンツは全#シリーズのパート#です: Google App Engine for Java

このシリーズの続きに乞うご期待。

このコンテンツはシリーズの一部分です:Google App Engine for Java

このシリーズの続きに乞うご期待。

App Engine for Java の狙いは、スケーラブルな Web アプリケーションに対応した永続化層を作成するのに伴う心配をなくすことですが、この目標はどれだけ達成されているでしょうか。この記事では、App Engine for Java を紹介する連載の締めくくりとして、JDO (Java Data Object) と JPA (Java Persistence API) をベースとする App Engine for Java 永続化フレームワークの概要を説明します。App Engine の Java ベースの永続化は初めのうちは有望だったものの、これから説明し、デモで示すように、現状では深刻な欠点を抱えています。App Engine for Java の永続化の仕組みとその問題、そして Google のクラウド・プラットフォームで作業するときに Java 開発者が選択できる永続化の方法について学んでください。

記事を読んでサンプル・コードを実行する上で常に念頭に置いておかなければならない点は、App Engine for Java は現在、プレビュー・リリースであるということです。今のところ、Java ベースの永続化は開発者の理想通りのものでも、必要を満たすものでもありませんが、今後この事態は変わっていくはずです。また、変わらなくてはなりません。この記事を執筆するなかで私が学んだことは、App Engine for Java を今すぐスケーラブルなデータ量の多い Java アプリケーション開発に利用するという選択肢は、臆病な開発者や保守的な開発者には向いていません。App Engine for Java を使用するのは、いわば底の深いプールに飛び込むようなものです。視界のなかにプールの監視員はいません。プロジェクトが沈むか、順調に運ぶかは開発者の腕にかかっています。

この記事で使用するサンプル・アプリケーションは、第 2 回で開発した連絡先管理アプリケーションをベースにしていることに注意してください。そのため、ここに記載する演習を進めるには、前回のアプリケーションが作成済みであり、実行可能でなければなりません。

基本の仕組み、そしてメモリー・リークを起こしやすい抽象化

オリジナルの Google App Engine と同じく、App Engine for Java でも、スケーラブルなアプリケーション開発の 3 本柱となる分散、複製、ロード・バランシングについては Google の内部インフラストラクチャーに委ねています。開発者は Google のインフラストラクチャーで作業することから、この魔法のほとんどは舞台裏で行われ、開発者が内部インフラストラクチャーにアクセスするには App Engine for Java の標準 API を使用することになります。データ・ストア・インターフェースは JDO と JPA をベースとし、この JDO と JPA がベースとするのはオープンソースの DataNucleus プロジェクトです。App Engine for Java にはそのデータ・ストアを直接操作するための下位レベルのアダプター API が用意されています。この API は、Google の BigTable 実装をベースとします (BigTable についての詳細は第 1 回を参照)。

その一方、App Engine for Java のデータ永続化は純粋な Google App Engine での永続化ほど単純ではありません。JDO および JPA インターフェースにはメモリー・リークを起こしやすい抽象化がいくつかあります。その原因は、BigTable がリレーショナル・データベースでないという事実にあります。例えば App Engine for Java では、結合を行うクエリーは実行することができません。JPA と JDO でリレーションシップを設定することはできるものの、この 2 つを使用できるのはリレーションシップを永続化するという目的でのみです。さらにオブジェクトを永続化する場合でも、同じエンティティー・グループに含まれるオブジェクトを同じアトミック・トランザクションで永続化することしかできません。慣例では、所有関係があるリレーションシップの場合、オブジェクトは親と同じエンティティー・グループに含まれます。逆に、所有関係がないリレーションシップの場合は、オブジェクトはそれぞれ別のエンティティー・グループに含まれることになります。

データ正規化を再考する

App Engine のスケーラブルなデータ・ストアを扱うにあたっては、正規化されたデータのメリットに対する既成概念について考え直す必要があります。もちろん、実際の開発経験が長ければ、これまで一度や二度はパフォーマンスのために正規化を犠牲にしたことはあるでしょう。App Engine データ・ストアを扱う場合の正規化で何が違うかと言えば、初期段階に、頻繁に非正規化しなければならないことです。非正規化はもはや禁句ではなく、設計ツールとして App Engine for Java アプリケーションのさまざまな側面で適用することになります。

App Engine for Java のメモリー・リークしやすい永続化が持つ主な問題点は、RDBMS を対象に作成したアプリケーションを App Engine for Java に移植しようとするときに現れてきます。App Engine for Java のデータ・ストアはリレーショナル・データベースに代わるものとして簡単に置き換えられるわけではないため、App Engine for Java で実行する内容を、RDBMS 用のアプリケーションを移植して置き換えるのは容易なことではありません。だからと言って既存のスキーマを引き継いで App Engine for Java のデータ・ストアに移植するというシナリオは、さらに現実性に欠けています。レガシー Java エンタープライズ・アプリケーションを App Engine に移植することにした場合は、慎重に事を進め、分析によって裏付けを取るようにしてください。Google App Engine は、それ専用に設計されたアプリケーションのためのプラットフォームです。Google App Engine 専用のアプリケーションを正規化されていない従来のエンタープライズ・アプリケーションに移植することを可能にするのは、Google App Engine for Java の JDO および JPA に対するサポートです。

リレーションシップの問題

App Engine for Java が現行のプレビュー・リリースで抱えるもう 1 つの問題点は、そのリレーションシップの扱いにあります。現在、リレーションシップを作成するには、JDO に対して App Engine for Java 固有の拡張機能を適用しなければなりません。キーが BigTable の成果物を基に生成されることを考えると (つまり、「主キー」が持つ親オブジェクト・キーが、そのすべての子にエンコードされるということ)、データは非リレーショナル・データベースで管理しなければならないことになります。また、データの永続化にも制約があり、標準ではない AppEngine for Java の Key クラスを使用する場合、複雑な事態になります。第 1 に、モデルを RDBMS に移植するときに標準ではない Key をどのように使用するかという問題があります。そして第 2 の問題として、GWT エンジンでは Key クラスを変換できないため、このクラスを使用するモデル・オブジェクトは GWT アプリケーションの一部として使用することができません。

もちろん、この記事を執筆している時点で Google App Engine for Java はまさにプレビュー・モードなので、まだ完成しているわけではありません。このことは、JDO でのリレーションシップに関する資料を調べると極めて明白になります。情報は少なく、記載されているサンプルも不完全だからです。

App Engine for Java 開発キットには、一連のサンプル・プログラムが同梱されています。サンプルの多くは JDO を使用しており、JPA を使用しているサンプルはありません。これらのサンプルのうち (jdoexamples という名前のサンプルも含め)、単純なリレーションシップの例でさえも含まれているものはありません。いずれのサンプルにしても、たった 1 つのオブジェクトを使用してデータをデータ・ストアに保管しています。Google App Engine for Java ディスカッション・グループには、単純なリレーションシップを機能させる方法についての質問が殺到していますが、その答えはほとんどありません。一部の開発者は明らかにリレーションシップを機能させられるようになっていますが、それでも大変な作業と面倒な問題が伴うことは確かです。

App Engine for Java でのリレーションシップでポイントとなるのは、JDO や JPA によるサポートはほとんどないため、開発者がリレーションシップを管理せざるを得ないということです。しかし、Google の BigTable はスケーラブルなアプリケーションを作成する技術として実証されているので、この技術を足掛かりにすることはできます。BigTable をベースにすることで、完成したとは言えない API ファサードを処理する必要はなくなります。代わりに、下位レベルの API を処理することになります。

AppEngine for Java での JDO

従来の Java アプリケーションを App Engine for Java に移植しても意味がないことかもしれず、しかもリレーションシップの問題もありますが、それでもなお、このプラットフォームを使用するのが妥当な永続化シナリオはあります。そこで、この連載は App Engine for Java の永続化の機能を経験できる実際のサンプルで締めくくろうと思います。出発点となるのは、第 2 回で作成した連絡先管理アプリケーションです。今回は、App Engine for Java データ・ストア機能を使って Contact オブジェクトを永続化するためのサポートを追加する手順を説明します。

前回の記事では、Contact オブジェクトに対して CRUD 操作を行う単純な GWT GUI を作成しました。その際にまず定義したのは、リスト 1 に記載する単純なインターフェースです。

リスト 1. 単純な ContactDAO インターフェース
package gaej.example.contact.server;

import java.util.List;

import gaej.example.contact.client.Contact;

public interface ContactDAO {
	void addContact(Contact contact);
	void removeContact(Contact contact);
	void updateContact(Contact contact);
	List<Contact> listContacts();
}

次に作成したのは、メモリー内のコレクションに含まれるデータと対話するモック・バージョンです (リスト 2 を参照)。

リスト 2. DAO のモック ContactDAOMock
package gaej.example.contact.server;

import gaej.example.contact.client.Contact;

import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

public class ContactDAOMock implements ContactDAO {

	Map<String, Contact> map = new LinkedHashMap<String, Contact>();
	
	{
		map.put("rhightower@mammatus.com", new Contact("Rick Hightower", 
                                 "rhightower@mammatus.com", "520-555-1212"));
		map.put("scott@mammatus.com", new Contact("Scott Fauerbach", 
                                 "scott@mammatus.com", "520-555-1213"));
		map.put("bob@mammatus.com", new Contact("Bob Dean", 
                                 "bob@mammatus.com", "520-555-1214"));

	}
	
	public void addContact(Contact contact) {
		String email = contact.getEmail();
		map.put(email, contact);
	}

	public List<Contact> listContacts() {
		return Collections.unmodifiableList(new ArrayList<Contact>(map.values()));
	}

	public void removeContact(Contact contact) {
		map.remove(contact.getEmail());
	}

	public void updateContact(Contact contact) {		
		map.put(contact.getEmail(), contact);
	}

}

これから、このモック実装を Google App Engine データ・ストアを扱うアプリケーションのバージョンで置き換えるとどうなるかを見ていきます。このサンプルでは、JDO を使って Contact クラスを永続化します。Google Eclipse Plugin を使って作成したアプリケーションには、JDO を使用するために必要なライブラリーがすべて揃っています。このアプリケーションには jdoconfig.xml ファイルも含まれているので、Contact クラスにアノテーションを付けさえすれば、JDO を使い始められるようになります。

リスト 3 に、JDO API を使用して、オブジェクトを永続化したり、削除したり、オブジェクトに対して更新を行ったり、クエリーを実行したりするように拡張した ContactDAO インターフェースを記載します。

リスト 3. JDO を使用した ContactDAO
package gaej.example.contact.server;

import gaej.example.contact.client.Contact;

import java.util.List;

import javax.jdo.JDOHelper;
import javax.jdo.PersistenceManager;
import javax.jdo.PersistenceManagerFactory;

public class ContactJdoDAO implements ContactDAO {
	private static final PersistenceManagerFactory pmfInstance = JDOHelper
			.getPersistenceManagerFactory("transactions-optional");

	public static PersistenceManagerFactory getPersistenceManagerFactory() {
		return pmfInstance;
	}

	public void addContact(Contact contact) {
		PersistenceManager pm = getPersistenceManagerFactory()
				.getPersistenceManager();
		try {
			pm.makePersistent(contact);
		} finally {
			pm.close();
		}
	}

	@SuppressWarnings("unchecked")
	public List<Contact> listContacts() {
		PersistenceManager pm = getPersistenceManagerFactory()
				.getPersistenceManager();
		String query = "select from " + Contact.class.getName();
		return (List<Contact>) pm.newQuery(query).execute();
	}

	public void removeContact(Contact contact) {
		PersistenceManager pm = getPersistenceManagerFactory()
				.getPersistenceManager();
		try {
			pm.currentTransaction().begin();

			// We don't have a reference to the selected Product.
			// So we have to look it up first,
			contact = pm.getObjectById(Contact.class, contact.getId());
			pm.deletePersistent(contact);

			pm.currentTransaction().commit();
		} catch (Exception ex) {
			pm.currentTransaction().rollback();
			throw new RuntimeException(ex);
		} finally {
			pm.close();
		}
	}

	public void updateContact(Contact contact) {
		PersistenceManager pm = getPersistenceManagerFactory()
				.getPersistenceManager();
		String name = contact.getName();
		String phone = contact.getPhone();
		String email = contact.getEmail();

		try {
			pm.currentTransaction().begin();
			// We don't have a reference to the selected Product.
			// So we have to look it up first,
			contact = pm.getObjectById(Contact.class, contact.getId());
			contact.setName(name);
			contact.setPhone(phone);
			contact.setEmail(email);
			pm.makePersistent(contact);
			pm.currentTransaction().commit();
		} catch (Exception ex) {
			pm.currentTransaction().rollback();
			throw new RuntimeException(ex);
		} finally {
			pm.close();
		}
	}

}

メソッドそれぞれの説明

ここで、リスト 3 のそれぞれのメソッドで何が行われるかを検討します。メソッド名は新しいかもしれませんが、そのアクションはほとんどおなじみのものであることがわかるはずです。

リスト 3 ではまず、PersistenceManager にアクセスするために静的 PersistenceManagerFactory を作成しています。JPA を扱った経験がある人にとっては、PersistenceManager は JPA の EntityManager のようなものです。Hibernate を扱った経験がある人にとっては、PersistenceManager は Hibernate の Session に似ています。基本的に、PersistenceManager は JDO 永続化システムに対する主要インターフェースであり、データベースとのセッションを表現します。そのため、getPersistenceManagerFactory() メソッドによって、静的に初期化された PersistenceManagerFactory が返されます (リスト 4 を参照)。

リスト 4. PersistenceManagerFactory を返す getPersistenceManagerFactory()
private static final PersistenceManagerFactory pmfInstance = JDOHelper
		.getPersistenceManagerFactory("transactions-optional");

public static PersistenceManagerFactory getPersistenceManagerFactory() {
	return pmfInstance;
}

addContact メソッドは新しい連絡先をデータ・ストアに追加します。連絡先を追加するために、このメソッドは PersistenceManager のインスタンスを作成した後、PersistenceManagermakePersistence() メソッドを呼び出します。makePersistence() メソッドは一時的な Contact オブジェクト (ユーザーがこのオブジェクトを GWT GUI に入力することになります) を取得して、それを永続オブジェクトにします。この一連の処理をリスト 5 に記載します。

リスト 5. addContact()
public void addContact(Contact contact) {
	PersistenceManager pm = getPersistenceManagerFactory()
			.getPersistenceManager();
	try {
		pm.makePersistent(contact);
	} finally {
		pm.close();
	}
}

リスト 5 では PersistenceManagerfinally ブロックに取り込まれていることに注目してください。これによって、PersistenceManager に関連付けられたリソースが確実にクリーンアップされることになります。

リスト 6 に記載する listContacts() メソッドは query オブジェクトを作成し、PersistenceManagernewQuery(query).execute() メソッドを呼び出すことで、データ・ストアから Contact のリストが返されます。

リスト 6. listContacts()
@SuppressWarnings("unchecked")
public List<Contact> listContacts() {
	PersistenceManager pm = getPersistenceManagerFactory()
			.getPersistenceManager();
	String query = "select from " + Contact.class.getName();
	return (List<Contact>) pm.newQuery(query).execute();
}

removeContact() は連絡先をデータ・ストアから削除する前に、まず ID でその連絡先を検索します (リスト 7 を参照)。連絡先を直接削除するのではなく、連絡先を検索しなければならない理由は、GWT GUI から提供された Contact は、JDO についての情報を何も持っていないためです。そのため PersistenceManager キャッシュに関連付けられた Contact を取得してからでないと、削除することができません。

リスト 7. removeContact()
public void removeContact(Contact contact) {
	PersistenceManager pm = getPersistenceManagerFactory()
			.getPersistenceManager();
	try {
		pm.currentTransaction().begin();

		// We don't have a reference to the selected Product.
		// So we have to look it up first,
		contact = pm.getObjectById(Contact.class, contact.getId());
		pm.deletePersistent(contact);

		pm.currentTransaction().commit();
	} catch (Exception ex) {
		pm.currentTransaction().rollback();
		throw new RuntimeException(ex);
	} finally {
		pm.close();
	}
}

リスト 8 に記載する updateContact() メソッドは、removeContact() メソッドと同じように、まず Contact を検索しますが、updateContact() メソッドの場合は、続いて Contact からプロパティーをコピーします。これらのプロパティーは、永続化マネージャーによって検出された Contact に引数として渡されます。PersistenceManager は 検出されたオブジェクトに対して変更の有無をチェックします。オブジェクトが変更されている場合には、トランザクションのコミット時に、PersistenceManager が変更をデータベースにフラッシュします。

リスト 8. updateContact()
public void updateContact(Contact contact) {
	PersistenceManager pm = getPersistenceManagerFactory()
			.getPersistenceManager();
	String name = contact.getName();
	String phone = contact.getPhone();
	String email = contact.getEmail();

	try {
		pm.currentTransaction().begin();
		// We don't have a reference to the selected Product.
		// So we have to look it up first,
		contact = pm.getObjectById(Contact.class, contact.getId());
		contact.setName(name);
		contact.setPhone(phone);
		contact.setEmail(email);
		pm.makePersistent(contact);
		pm.currentTransaction().commit();
	} catch (Exception ex) {
		pm.currentTransaction().rollback();
		throw new RuntimeException(ex);
	} finally {
		pm.close();
	}
}

オブジェクトの永続化用アノテーション

Contact を永続化可能にするには、@PersistenceCapable アノテーションによって永続化対応オブジェクトとして識別する必要があります。その上で、Contact の永続化可能なフィールドのすべてにアノテーションを付ける必要があります (リスト 9 を参照)。

リスト 9. 永続化可能な Contact
package gaej.example.contact.client;

import java.io.Serializable;

import javax.jdo.annotations.IdGeneratorStrategy;
import javax.jdo.annotations.IdentityType;
import javax.jdo.annotations.PersistenceCapable;
import javax.jdo.annotations.Persistent;
import javax.jdo.annotations.PrimaryKey;

@PersistenceCapable(identityType = IdentityType.APPLICATION)
public class Contact implements Serializable {

	private static final long serialVersionUID = 1L;
	@PrimaryKey
	@Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
	private Long id;
	@Persistent
	private String name;
	@Persistent
	private String email;
	@Persistent
	private String phone;

	public Contact() {

	}

	public Contact(String name, String email, String phone) {
		super();
		this.name = name;
		this.email = email;
		this.phone = phone;
	}

	public Long getId() {
		return id;
	}

	public void setId(Long id) {
		this.id = id;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public String getEmail() {
		return email;
	}

	public void setEmail(String email) {
		this.email = email;
	}

	public String getPhone() {
		return phone;
	}

	public void setPhone(String phone) {
		this.phone = phone;
	}

}

これで、オブジェクト指向プログラミングの驚くべき効果とインターフェースによる設計原則のおかげで、ただ単に、元の ContactDAOMock を新しい ContactJdoDAO で置き換えることができます。後は何も変更しなくても、GWT GUI が JDO と連動します。

最終的に、この置換によって変わるのは、このサービスで DAO がインスタンス化される方法です (リスト 10 を参照)。

リスト 10. RemoteServiceServlet
public class ContactServiceImpl extends RemoteServiceServlet implements ContactService {
	private static final long serialVersionUID = 1L;
	//private ContactDAO contactDAO = new ContactDAOMock();
	private ContactDAO contactDAO = new ContactJdoDAO();
...

まとめ

この 3 回からなる連載記事では、現状での Google App Engine for Java による永続化のサポートを紹介しました。永続化は、スケーラブルなアプリケーションを実現するには欠かせないものの 1 つです。全体的な結果は満足のいくものではありませんが、頭に入れておくべき重要な点は、このプラットフォームは現在進化しつつあるということです。App Engine for Java のプレビュー・リリースを対象に作成されるアプリケーションは、JDO や JPA を使うにしても App Engine の永続化インフラストラクチャーに結合されます。また、App Engine for Java がそのプレビュー・バージョンで提供する永続化フレームワークに関する資料はわずかしかなく、App Engine for Java に同梱されたサンプルでは、単純なリレーションシップでさえも機能させることはほとんど不可能です。

JDO 実装と JPA 実装が万全に仕上げられているとしても、今のところ、App Engine for Java アプリケーションを作成して RDBMS ベースのエンタープライズ・アプリケーションに容易に移植できるとは考えられません。少なくとも、移植バージョンを機能させるためには相当なコーディングが必要になってきます。

永続化に対する私の期待は、時間とともに永続化が成熟していくことです。今すぐに App Engine for Java を扱わなければならないとしたら、Java API は使わずに、下位レベルのデータ・ストア API に直接書き込むという方法を採る必要があると思います。App Engine for Java プラットフォームを扱うことは可能ですが、JPA や JDO を扱い慣れている場合には、この記事の最初で説明したメモリー・リークしやすい抽象化、そしてまだ上手く動作しない機能や、現在十分に文書化されていない機能が原因で、習得に時間がかかることになるはずです。


ダウンロード可能なリソース


関連トピック


コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Java technology, Open source
ArticleID=431428
ArticleTitle=Google App Engine for Java: Google App Engine for Java: 第 3 回 永続化とリレーションシップ
publish-date=08252009