2009年11月12日木曜日

Slim3 Datastoreに乗り換える(3)

前回Slim3 Datastore の導入方法をエントリにしてみました。今回はそれを利用してエンティティのクラスを作る方法を紹介してみようと思います。と、前回に引っ張ってみたのですが、Slim3 Datastore ではあまりに簡単にエンティティを定義できるのでちょっと趣向を変えようと思います。

今回は、エンティティの定義方法を簡単に紹介した後、普段私が気をつけている点について簡単に紹介してみようと思います。また、経済性を考えて、アプリケーションには次のような制約をつけておきます。

  1. エンティティオブジェクトを永続化層以外でも利用する
  2. エンティティオブジェクトの生成はnewで行う

このあたりはやや宗教的な話も含みますので、参考程度に。

なお、このエントリで言及する注意点などは、少し堅めにApp Engineのデータストアを利用したいひと向けです。非常に小さなアプリケーションを作成する場合や、少数精鋭でライフサイクルの短いアプリケーションを作成する場合などには、少し冗長になると思います。 また、解説の中のパッケージ構成はSlim3推奨のものと異なります。Slim3本体を利用する場合には注意してください。

エンティティクラスの作成

まずはデータストア上のエンティティと1対1に対応するエンティティクラスを作成します。今回サンプルとして取り扱うクラスはブログのエントリ情報を表現するエンティティで、普通に書いた場合は下記のような感じになります。

package com.example.entity;

public class BlogEntry {

    private String permalink;

    private String title;

    public String getPermalink() {
        return this.permalink;
    }

    public void setPermalink(String permalink) {
        this.permalink = permalink;
    }

    public String getTitle() {
        return this.title;
    }

    public void setTitle(String title) {
        this.title = title;
    }
}

このとき、それぞれのプロパティはBean styleのアクセサ(getter/setter)を定義しておいてください。

@Model アノテーションの追加

先ほどのクラスを Slim3 Datastore のエンティティとして利用する場合、クラスに @Model アノテーションをつけてやります。

package com.example.entity;

import org.slim3.datastore.Model;

@Model
public class BlogEntry {
    // ...
}

この状態でセーブすると、「[SILM3GEN1015] You should define @Attribute(primaryKey = true) to one field.」というエラーが表示されます(表示されない場合はSlim3のジェネレータが正しく動いていませんので、注釈プロセッサのファクトリの設定あたりをみなおしてみてください)。このエラーはエンティティにプライマリキーの定義がないために発生しているエラーです。この状態ではエンティティとして利用できませんので、次はプライマリキーを設定してやります。

プライマリキーの追加

それぞれのエンティティにはプライマリキーが必要です。Slim3 Datastore で利用可能なプライマリキーは、App Engine が提供するKey型のプロパティのみです。先ほどのクラスにプライマリキーのフィールドとアクセサ、およびプライマリキーであることを宣言するアノテーションを書いてやります。

package com.example.entity;

import org.slim3.datastore.Attribute;
import org.slim3.datastore.Model;

import com.google.appengine.api.datastore.Key;

@Model
public class BlogEntry {

    @Attribute(primaryKey = true)
    private Key internalKey;

    private String permalink;

    private String title;

    public Key getInternalKey() {
        return this.internalKey;
    }

    public void setInternalKey(Key internalKey) {
        this.internalKey = internalKey;
    }
    // ...
}

Slim3 Datastore のエンティティクラスに必要な最低限の定義は以上です。今回はブログエントリのPermalinkをプライマリキーの名前に利用しようと思いますので、BlogEntry.permalinkフィールドを削除して、BlogEntry.internalKey.getName()で代用する手もあります。ただし、ソートをする場合や前方一致検索をしたい場合などでは、同フィールドは残しておきましょう。

使ってみる

先ほど作成したクラスを実際に使ってみます。Slim3 Datastore を利用してデータストアの操作を行うには、Datastoreという名前のクラスのクラスメソッドを利用します。

まずは、データストアにエンティティを追加するコード片です。前述の通り、BlogEntryエンティティのプライマリキーはPermalinkにすることになっているため、入口のところでPermalinkからKeyオブジェクトを生成しています。

public void put(String permalink, String title) {
    // Permalinkから BlogEntryのキーを作成する
    Key key = Datastore.createKey(BlogEntry.class, permalink);

    // インスタンスを作って値を設定していく
    BlogEntry blogEntry = new BlogEntry();
    blogEntry.setInternalKey(key);
    blogEntry.setPermalink(permalink);
    blogEntry.setTitle(title);

    // Datastore.put() で書きこむ
    Datastore.put(blogEntry);
}

次に、PermalinkをもとにデータストアからBlogEntryエンティティを取得するコード片です。App EngineのデータストアはKey-Valueストア (KVS) のような機能を持っているので、キーさえ分かれば簡単にエンティティを取り出すことができます。

public BlogEntry get(String permalink) {
    // Permalinkから BlogEntryのキーを作成する
    Key key = Datastore.createKey(BlogEntry.class, permalink);

    // 自動生成されたメタデータをインスタンス化する
    BlogEntryMeta meta = new BlogEntryMeta();

    // Datastore.get() で読みだす
    try {
        BlogEntry blogEntry = Datastore.get(meta, key);
        return blogEntry;
    }
    catch (EntityNotFoundRuntimeException e) {
        // 存在しない場合は専用の例外が飛ぶ
        return null;
    }
}

このほかに、Slim3 Datastoreはタイプセーフなクエリを記述できるなど、様々な機能を提供しています。 私見ですが、App Engineのクエリの難しさはクエリの記述自体にはなく、主に問い合わせ先のエンティティの構造に関係しています。 クエリの書き方の紹介は、その辺りのネタと合わせて後日書こうと思います。

以上が Slim3 Datastore のエンティティ入門です。以降は思想やらtipsやらをつらつらと書いていきます。

エンティティクラスに集約させる機能

Slim3 Datastoreを利用する上で地味に問題になるのが、Keyオブジェクトの取り扱いです。前項の例ではブログエントリのPermalinkをプライマリキーとして取り扱って、それをもとにKeyオブジェクトを構築していました。

Keyというのはかなりいろいろな情報を持つオブジェクトで、アプリケーションからは次のような項目が設定できます。

エンティティの種類(カインド)
先ほどの例ではBlogEntryとなる。
キー名
先ほどの例ではPermalinkの文字列を利用していた。(この代わりに自動採番される数値を使うこともできる)
親キー
エンティティグループを構成する場合に必要になる。

App Engineのデータストアを利用する場合、このように複雑な情報を持てるKeyオブジェクトは、エンティティの情報と一貫して管理される必要があります。ここでは、エンティティクラスにこのキーの情報を集約させるために、ふだん私が心がけているプラクティスをいくつか紹介します。

キーファクトリをエンティティクラスに作成する

先ほどのBlogEntryの例では、Slim3が提供するDatastore.createKey()というメソッドを利用して外部からKeyオブジェクトを生成していました。このメソッドは様々なエンティティのキーを生成できる汎用的なメソッドで、個々のエンティティのキーをプライマリキーの設計に従って生成するためのものではありません。

そのため、Datastore.createKey()を利用するプログラムがアプリケーション内に散らばってしまうと、エンティティごとのプライマリキー設計に従わないものが紛れ込む可能性があります。

そこで、Keyオブジェクトを生成するメソッドを、エンティティクラスの中に用意してやります。

@Model
public class BlogEntry {

    @Attribute(primaryKey = true)
    private Key internalKey;

    private String permalink;

    private String title;

    /**
     * 指定のPermalinkを持つエントリに対するキーを生成して返す。
     * @param permalink 対象エントリのPermalink
     * @return 対応するキー
     * @throws IllegalArgumentException 引数に{@code null}が含まれる場合
     */
    public static Key createKey(String permalink) {
        if (permalink == null) {
            throw new IllegalArgumentException("permalink is null");
        }
        return Datastore.createKey(BlogEntry.class, permalink);
    }
    // ...
}

上記はDatastore.createKey()を呼び出すだけの簡単なメソッドですが、重要な点が3つあります。

  1. 自然にドキュメンテーションできる

    上記のファクトリメソッドを見れば、Permalinkをもとにキーを生成することが明らかに分かります。文字列以外の情報をもとに構築する場合や、KVSのテクニックの一つに「2つ以上の値を合成してキーを構成する」というものがありますが、利用者側は各プロパティの値や型、区切り文字などを意識せずにキーを取得できます。

  2. 不正なキーを排除できる

    Datastore.createKey()を呼び出す前に、キーの構成情報を検査できます。今回はnull検査だけやりましたが、必要に応じて不正な文字列を排除したりすることもできます。

  3. キーの構成方法を変更できる

    Datastore.createKey()を直接利用しないことで、キーの構成方法を変えた際の変更が容易になります。ただし、キーの構成方法を変えること自体が非常に大変という問題はあります。

ひと手間必要になりますが、上記のような利点がありますので作成しても損はないかと思います。ただし、Datastore.createKey()を直接呼び出すプログラムがクライアントコードに紛れ込んでいるとひどいことになりそうなので、まだ検討の余地はありそうです。

なお、先ほどの例からBlogEntry.permalinkフィールドを削除する場合や、Key only queryなどを利用する場合には、先ほどの逆の操作、つまりキーから値を取り出すためのメソッドを用意するとよいと思います。重要なことは、「キーの意味をエンティティクラスの中で全て規定する」という点です。

コンストラクタにキーオブジェクトを構築するための情報を渡す

エンティティオブジェクトのライフサイクル中に、プライマリキーの値が変更されることはあまりありません。ということで、オブジェクトの生成時にプライマリキーを渡して、以後変更しないようにする、という運用がよいかもしれません。

@Model
public class BlogEntry {

    @Attribute(primaryKey = true)
    private Key internalKey;

    private String permalink;

    private String title;

    /**
     * フレームワークが利用するコンストラクタ。
     * クライアントコードからは利用しないこと。
     */
    public BlogEntry() {
        // 特に何もしない
    }

    /**
     * インスタンスを生成する。
     * @param permalink エントリのPermalink
     * @param title エントリのタイトル
     * @throws IllegalArgumentException 引数に{@code null}が含まれる場合
     */
    public BlogEntry(String permalink, String title) {
        if (permalink == null) {
            throw new IllegalArgumentException("permalink is null");
        }
        if (title == null) {
            throw new IllegalArgumentException("title is null");
        }
        this.permalink = permalink;
        this.title = title;
        // キーオブジェクト自体を引数に取らないようにして、
        // 先ほど作ったキーファクトリを利用して構築する
        this.internalKey = createKey(permalink);
    }
    // ...
}

ここでのポイントは2点です。

  1. Slim3 Datastore用に引数なしのコンストラクタを用意する

    Slim3 Datastoreのエンティティには引数なしのコンストラクタが公開されている必要があります。クライアントコードから利用されないように、適当にコメントを書いておきます。

  2. Keyオブジェクトを直接渡さない

    クライアントコードからKeyオブジェクトを意識することがないように、コンストラクタの引数にKeyオブジェクトを渡さないようにします。キーオブジェクトを渡されてしまうと、エンティティとキーの整合性が崩れる場合がありますので、あくまで「キーを構築するための情報」という範囲にとどめておくことが重要です。ただし、複雑なトランザクション処理を行うなどの一部のケースでは、キーを直接渡したほうがよい場面もあります。そのため、私自身も原理主義的に守っているわけではありません。

もっとしっかり整合性を保つ場合には、エンティティオブジェクトに対するファクトリを用意してあげるのがよいと思います。ただし、エンティティ構造の変更に関するメンテナンスコストが多少上がり、さらに使い勝手が多少落ちるという弊害が考えられますので、このあたりはトレードオフです。

キーを直接利用する際に注意を促す

ここまでに、Keyオブジェクトをエンティティクラスに隠蔽する手段をいくつか紹介しました。ただ、Slim3 Datastoreを使う以上、エンティティクラスからKeyオブジェクトそのものを隠蔽することはできず、何らかの形でアクセサを提供しなければなりません。

最後のプラクティスとして、このKeyオブジェクトをクライアントコードから利用しようとした際に、何となく気持ち悪い気分を開発者に与える方法について紹介します。

@Model
public class BlogEntry {

    @Attribute(primaryKey = true)
    private Key internalKey;

    // ...

    /**
     * フレームワークが利用するメソッド。
     * クライアントコードからは利用しないこと。
     * @return the key
     */
    public Key getInternalKey() {
        return this.internalKey;
    }

    /**
     * フレームワークが利用するメソッド。
     * クライアントコードからは利用しないこと。
     * @param internalKey to set
     */
    public void setInternalKey(Key internalKey) {
        this.internalKey = internalKey;
    }
    
    // ...
}

上記は、元のプログラムに対して少しだけコメントを付け加えたものです。ここでのポイントは2点。

  1. プライマリキーのフィールド名を、何となく使うのが憚られる名前にする

    keyとかidとかにすると、普通のプロパティの一部に見えてしまいます。internalKeyなどにしておくと、フレームワーク内だけで利用されるように取ってくれるかもしれません。ちなみにこの対策は、一部の相手に全く効果がありません。いっそ、org.hamcrest.Matcherのように、ひどい名前のメンバ名にして、ソースコード中に出現すると残念な気分になるようにしてもいいかもしれません。

  2. プライマリキーのアクセサにドキュメンテーションを追加する

    使ってはいけないということを明記しておきます。deprecatedの指定をするという手もありますが、Slim3 Datastore や、一部の永続化ロジックからは利用されるため、あまり適切ではありません。この対策も、一部の相手に全く効果がありません。

上記は、「クライアントコードで誤って使わないように」という対策の意味と、今後のメンテナンス性やポータビリティの観点の2重の意味があります。 普段からKeyオブジェクトを意識しないようにクライアントコードを作成することで、永続化に関する領域と、ユースケースに関する領域の作業をちゃんと切り分けて作成できるようになるという利点があります。

長くなりすぎた+時間切れになったので、ここで一度終わりにします。 次回はDomain Driven DesignのRepositoryパターンを使って、エンティティの操作に関する責務を集約するやり方について紹介しようと思います。 今回のKeyオブジェクト周りの話は、Repositoryパターンのための布石なので、片手落ちなエントリになってしまいました。

0 件のコメント:

コメントを投稿