お気に入り | 日本語 | ログイン

JDO を利用したデータストアの使用

スケーラブルな Web アプリケーションでデータを保存するときは注意が必要です。ユーザーはある時点で不特定多数の Web サーバーのいずれかと通信していたとしても、次のリクエストは、前のリクエストを処理したのとは別の Web サーバーに送られる可能性があります。すべての Web サーバーは、(おそらく世界中の地理的に異なる場所にある)多数のマシンに拡散したデータを送受信する必要があるのです。

Google App Engine なら、そのようなことを気にする必要はありません。App Engine のインフラは、シンプルな API を使って、データの配布、複製、負荷分散を一手に引き受けます。ユーザーは強力なクエリ エンジンとトランザクションのメリットを享受できます。

App Engine データストアは、標準 API と低レベル API の両方を介して提供されるサービスの 1 つです。標準 API を使用すると、アプリケーションを他のホスティング環境やデータベース技術に移植する必要が生じた場合に、簡単に移植可能です。つまり、標準 API によって、アプリケーションを App Engine サービスから「切り離す」ことができるということです。App Engine サービスには、サービスの機能を直接公開する低レベル API も用意されています。低レベル API を使用すると、新しいアダプタ インターフェースを実装できます。また、アプリケーション内で直接 API を使用することも可能です。

App Engine でデータストアの API 標準としてサポートされているのは、JDO(Java Data Objects)と JPA(Java Persistence API)の 2 つです。これらのインターフェースは、複数の Java 永続性標準のオープン ソース実装である DataNucleus Access Platform で、App Engine データストア用のアダプタを使って提供されます。

このゲストブックでは、JDO インターフェースを使用して、ユーザーが投稿したメッセージを取得およびポストします。

DataNucleus Access Platform のセットアップ

Access Platform では、JDO 実装のバックエンドとして App Engine データストアを使用することを指定する設定ファイルが必要になります。最終的な WAR ファイルでは、このファイルが jdoconfig.xml という名前で war/WEB-INF/classes/META-INF/ ディレクトリに格納されます。

Eclipse を使用している場合、このファイルはすでに src/META-INF/jdoconfig.xml として作成されています。プロジェクトをビルドすると、このファイルが自動的に war/WEB-INF/classes/META-INF/ にコピーされます。

Eclipse を使用していない場合は、直接 war/WEB-INF/classes/META-INF/ ディレクトリを作成するか、ビルド プロセス中にディレクトリを作成し、設定ファイルを別の場所からコピーします。Apache Ant の使用で説明する Ant ビルド スクリプトでは、このファイルが src/META-INF/ からコピーされます。

jdoconfig.xml ファイルのコンテンツは次のようになっています:

<?xml version="1.0" encoding="utf-8"?>
<jdoconfig xmlns="http://java.sun.com/xml/ns/jdo/jdoconfig"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:noNamespaceSchemaLocation="http://java.sun.com/xml/ns/jdo/jdoconfig">

    <persistence-manager-factory name="transactions-optional">
        <property name="javax.jdo.PersistenceManagerFactoryClass"
            value="org.datanucleus.store.appengine.jdo.DatastoreJDOPersistenceManagerFactory"/>
        <property name="javax.jdo.option.ConnectionURL" value="appengine"/>
        <property name="javax.jdo.option.NontransactionalRead" value="true"/>
        <property name="javax.jdo.option.NontransactionalWrite" value="true"/>
        <property name="javax.jdo.option.RetainValues" value="true"/>
        <property name="datanucleus.appengine.autoCreateDatastoreTxns" value="true"/>
    </persistence-manager-factory>
</jdoconfig>

JDO クラスの拡張

JDO クラスの作成時には、インスタンスをデータストアに格納する方法や、データストアから取得したときの再生成方法を、Java アノテーションを使用して記述します。Access Platform では、コンパイル後の処理ステップでデータ クラスを実装に接続します。DataNucleus では、この処理を「拡張」と呼んでいます。

Eclipse と Google Plugin を使用している場合は、プラグインによるビルド プロセスの中で、JDO クラスの拡張ステップが自動的に実行されます。

Apache Ant の使用で説明する Ant ビルド スクリプトには、必要な拡張ステップが含まれています。

JDO クラスの拡張に関する詳しい情報は、JDO の使用をご覧ください。

POJO と JDO アノテーション

JDO では、DataNucleus Access Platform をはじめとする JDO 準拠のアダプタを使って、Java オブジェクト(Plain Old Java Objects または POJO と呼ぶこともある)を任意のデータストアに格納できます。App Engine SDK には App Engine データストア用の Access Platform プラグインが含まれています。つまり、定義したクラスのインスタンスを App Engine データストアに格納しておき、JDO API を使用してオブジェクトとして取得できるということです。クラスのインスタンスの格納と再構築の方法は、Java アノテーションを使用して指定します。

では、ゲストブックに投稿された個別のメッセージを表す Greeting クラスを作成しましょう。

新しいクラス Greeting を、guestbook パッケージ内に作成します(Eclipse を使用していない場合は、Greeting.java というファイルを src/guestbook/ ディレクトリに作成します)。ソース ファイルのコンテンツを次のようにします:

package guestbook;

import java.util.Date;
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;
import com.google.appengine.api.users.User;

@PersistenceCapable(identityType = IdentityType.APPLICATION)
public class Greeting {
    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    private Long id;

    @Persistent
    private User author;

    @Persistent
    private String content;

    @Persistent
    private Date date;

    public Greeting(User author, String content, Date date) {
        this.author = author;
        this.content = content;
        this.date = date;
    }

    public Long getId() {
        return id;
    }

    public User getAuthor() {
        return author;
    }

    public String getContent() {
        return content;
    }

    public Date getDate() {
        return date;
    }

    public void setAuthor(User author) {
        this.author = author;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public void setDate(Date date) {
        this.date = date;
    }
}

このシンプルなクラスでは、グリーティング メッセージのプロパティとして authorcontent、および date を定義しています。これら 3 つのプライベート フィールドには、@Persistent アノテーションが記述されています。これらのアノテーションの目的は、各フィールドをオブジェクトのプロパティとして App Engine データストアに格納することを DataNucleus に伝えるためのものです。

次に、それぞれのプロパティのゲッターとセッターを定義しています。これらはアプリケーションだけで使用するもので、JDO では使用しません。

このクラスでは、id および Long というフィールドも定義し、@Persistent@PrimaryKey の 2 つのアノテーションを付加しています。App Engine データストアにはエンティティ キーという概念が採用されており、オブジェクトにおいてさまざまな形式でキーを表現できます。ここでのキー フィールドは long 型の整数です。オブジェクトを保存時には、一意な数値 ID が自動的に設定されます。

JDO アノテーションに関する詳しい情報は、データ クラスの定義をご覧ください。

PersistenceManagerFactory

データストアを使用するリクエストのたびに、PersistenceManager クラスの新しいインスタンスが生成されます。生成には、PersistenceManagerFactory クラスのインスタンスが使用されます。

PersistenceManagerFactory インスタンスは初期化に時間がかかりますが、アプリケーションに必要なインスタンスは幸い 1 つのみです。インスタンスを静的変数に格納すれば、複数のリクエストや複数のクラスから使用できます。簡単なのは、静的インスタンスのシングルトン ラッパー クラスを作成する方法です。

PMF という新しいクラスを guestbook パッケージ内に作成し(PMF.java というファイル名で src/guestbook/ ディレクトリに格納)、コンテンツを次のようにします:

package guestbook;

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

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

    private PMF() {}

    public static PersistenceManagerFactory get() {
        return pmfInstance;
    }
}

オブジェクトの作成と保存

DataNucleus と Greeting クラスが揃ったら、フォーム処理ロジックに基づいてグリーティング メッセージをデータ ストアに格納できます。

src/guestbook/SignGuestbookServlet.java を次のように編集します:

package guestbook;

import java.io.IOException;
import java.util.Date;
import java.util.logging.Logger;
import javax.jdo.PersistenceManager;
import javax.servlet.http.*;
import com.google.appengine.api.users.User;
import com.google.appengine.api.users.UserService;
import com.google.appengine.api.users.UserServiceFactory;

import guestbook.Greeting;
import guestbook.PMF;

public class SignGuestbookServlet extends HttpServlet {
    private static final Logger log = Logger.getLogger(SignGuestbookServlet.class.getName());

    public void doPost(HttpServletRequest req, HttpServletResponse resp)
                throws IOException {
        UserService userService = UserServiceFactory.getUserService();
        User user = userService.getCurrentUser();

        String content = req.getParameter("content");
        Date date = new Date();
        Greeting greeting = new Greeting(user, content, date);

        PersistenceManager pm = PMF.get().getPersistenceManager();
        try {
            pm.makePersistent(greeting);
        } finally {
            pm.close();
        }

        resp.sendRedirect("/intl/ja/guestbook.jsp");
    }
}

このコードを実行すると、コンストラクタが呼び出されて新しい Greeting インスタンスが生成されます。インスタンスをデータストアに保存するため、PersistenceManagerFactory を使用して PersistenceManager を作成し、そのインスタンスを PersistenceManagermakePersistent() メソッドに渡しています。アノテーションやバイトコードの拡張時には、ここからインスタンスを取得します。makePersistent() が返されると、新しいオブジェクトがデータストアに格納されます。

JDOQL を使用したクエリ

JDO 標準には、JDOQL という永続オブジェクト用のクエリ メカニズムが定義されています。JDOQL を使用すると、App Engine データストア内のエンティティに対するクエリを実行し、結果を JDO 拡張オブジェクトとして取得できます。

ここでは、あまり複雑にならないよう、guestbook.jsp 内に直接クエリ コードを記述することにします。大きなアプリケーションであれば、クエリ ロジックを別のクラスに委任する方法もあります。

war/guestbook.jsp にコード行を追加して次のようにします:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.util.List" %>
<%@ page import="javax.jdo.PersistenceManager" %>
<%@ page import="com.google.appengine.api.users.User" %>
<%@ page import="com.google.appengine.api.users.UserService" %>
<%@ page import="com.google.appengine.api.users.UserServiceFactory" %>
<%@ page import="guestbook.Greeting" %>
<%@ page import="guestbook.PMF" %>

<html>
  <body>

<%
    UserService userService = UserServiceFactory.getUserService();
    User user = userService.getCurrentUser();
    if (user != null) {
%>
<p>Hello, <%= user.getNickname() %>! (You can
<a href="<%= userService.createLogoutURL(request.getRequestURI()) %>">sign out</a>.)</p>
<%
    } else {
%>
<p>Hello!
<a href="<%= userService.createLoginURL(request.getRequestURI()) %>">Sign in</a>
to include your name with greetings you post.</p>
<%
    }
%>

<%
    PersistenceManager pm = PMF.get().getPersistenceManager();
    String query = "select from " + Greeting.class.getName();
    List<Greeting> greetings = (List<Greeting>) pm.newQuery(query).execute();
    if (greetings.isEmpty()) {
%>
<p>The guestbook has no messages.</p>
<%
    } else {
        for (Greeting g : greetings) {
            if (g.getAuthor() == null) {
%>
<p>An anonymous person wrote:</p>
<%
            } else {
%>
<p><b><%= g.getAuthor().getNickname() %></b> wrote:</p>
<%
            }
%>
<blockquote><%= g.getContent() %></blockquote>
<%
        }
    }
    pm.close();
%>

    <form action="/sign" method="post">
      <div><textarea name="content" rows="3" cols="60"></textarea></div>
      <div><input type="submit" value="Post Greeting" /></div>
    </form>

  </body>
</html>

クエリを準備するには、PersistenceManager インスタンスの newQuery() メソッドを呼び出します。文字列としては、クエリのテキストを渡します。メソッドを実行すると、クエリ オブジェクトが返されます。クエリ オブジェクトの execute() メソッドを呼び出すとクエリが実行され、適切な型の結果オブジェクトの List<> リストが返されます。クエリ文字列には、クエリの対象となるクラスの完全な名前を、パッケージ名も含めて指定する必要があります。

プロジェクトをビルドし直してサーバーを再起動します。http://localhost:8080/ にアクセスします。グリーティング メッセージを入力して送信します。グリーティング メッセージがフォームの上に表示されます。別のグリーティング メッセージを入力して送信します。両方のグリーティング メッセージが表示されます。リンクを使ってログイン/ログアウトして、ログインしている状態とログインしていない状態でそれぞれメッセージを送信します。

ヒント: 実際のアプリケーションを開発する際は、ユーザーから送信されたコンテンツ(たとえば今回のグリーティング メッセージ)を表示する際に、HTML 文字をエスケープすることをおすすめします。JSTL(JavaServer Pages 標準タグ ライブラリ)には、文字エスケープ用のルーチンが用意されています。JSTL(およびその他の JSP 関連ランタイム JAR)は App Engine に含まれていますので、個別のアプリケーションに含める必要はありません。http://java.sun.com/jsp/jstl/functions のタグ ライブラリ内の escapeXml 関数を参照してください。詳しくは、Sun の J2EE 1.4 チュートリアルをご覧ください。

JDOQL の概要

現時点でのゲストブックには、システムにこれまで投稿されたすべてのメッセージが表示されます。また、メッセージが作成された順番で、上から下に表示されます。ゲストブックに投稿されるメッセージが増えてきたら、最近のメッセージだけを表示し、最新のメッセージを一番上に表示できると便利です。このような場合は、データストア クエリを少し調整します。

JDO インターフェースでは、SQL によく似たデータ オブジェクト用クエリ言語、JDOQL を使用してクエリを記述します。ここまでに作成した JSP ページでは、JDOQL クエリ文字列は次のように定義されています:

    String query = "select from " + Greeting.class.getName();

JDOQL クエリ文字列だけを抜き出すと次のようになります:

select from guestbook.Greeting

このクエリを実行すると、その時点でデータストアに保存されている Greeting クラスのすべてのインスタンスが返されます。

クエリでは、オブジェクトのプロパティについて条件を設定し、それを満たしていないオブジェクトを結果から除外できます。たとえば、Greeting オブジェクトの内 author プロパティが alfred@example.com のものだけを取得したい場合は、クエリを次のように記述できます:

select from guestbook.Greeting where author == 'alfred@example.com'

クエリでは、プロパティの値に基づいて、各結果が返される順序を指定できます。すべての Greeting オブジェクトを、投稿された順番とは逆の順序で(つまり新しいメッセージから先に)取得したい場合は、クエリを次のように記述できます:

select from guestbook.Greeting order by date desc

クエリでは、特定の範囲の結果のみが返されるようにすることもできます。最新のグリーティング メッセージ 5 件のみを取得したい場合は、order byrange を組み合わせて次のようにします:

select from guestbook.Greeting order by date desc range 0,5

これを、ゲストブック アプリケーションに応用してみましょう。guestbook.jsp で、query の定義を次のように変更します。

    String query = "select from " + Greeting.class.getName() + " order by date desc range 0,5";

ゲストブックのグリーティング メッセージが、6 件以上になるまで投稿します。最新の 5 件のみが、新しいものから順番に上から下へ表示されます。

クエリと JDOQL に関する詳しい情報は、クエリとインデックスをご覧ください。

次のステップ

各 Web アプリケーションは、テンプレートまたは何らかのシステムにより、コードで動的に生成した HTML を返します。ほとんどの Web アプリケーションは、画像や CSS スタイルシート、JavaScript ファイルなどの静的コンテンツに対応する必要があります。効率性のため、App Engine では、静的ファイルを、アプリケーション ソースやデータ ファイルとは分けて扱います。次は、このアプリケーションの CSS スタイルシートを、静的ファイルとして作成してみましょう。

静的ファイルの使用に進みます。