Orfeon Blog

主に備忘録

slim3でProjectionQueryを使えるようにしてみた

AppEngine SDK1.6.6からAppEngineでProjectionQuery https://developers.google.com/appengine/docs/java/datastore/queries#Query_Projection が使えるようになったので試してみました。

自分の場合、AppEngineのDatastoreまわりは完全にslim3に依存していることもあり、せっかくなのでいつもお世話になっているslim3のコードの勉強も兼ねて、slim3でProjectionQueryが使えるように仕込んでみることにしました。

ProjectionQueryとは何か

ProjectionQueryをひとことで言うと、keysOnlyのコスト(通常の1/7の金銭コスト)でEntityのプロパティを取得できるQueryです。

AppEngineのデータストアではリレーショナルDBのようにテーブルの特定のパラメータだけ取得することができず、Entityをまるごと取得する必要があります。そのため特定のプロパティだけ集計したい場合でもEntity取得と同じコストが掛かってしまいます。それをProjectionQueryを使うとリレーショナルDBと同じように特定のプロパティだけ低コストで取得できるようになります。

ProjectionQueryの制限

といってもProjectionQueryには制限があります。おもな制限は以下。

  • indexを張っているプロパティのみ取得可能
  • equal filterで指定しているプロパティは取得できない(影響無し?)
  • Set,Listなどのコレクションは取得できない(これは惜しい…)

indexを張っているプロパティのみ取得可能というのは、AppEngineのIndexの仕組みによるものです。そもそもAppEngineで使われているKeyValue型のデータストアは単体ではkeyを指定してValueオブジェクトを取得するもので、Queryでデータを取得するにはDatastoreとは別にindexを持つ必要があります。AppEngineではQueryを実行時に、まずindexにscanを掛けてValueオブジェクトを取得用のkeyを取得し、次にkeyによりDatastoreに対してオブジェクトを取得するという、2段階のステップを踏みます。取得したいプロパティがindexに含まれている場合は、プロパティをindexから取得し2段階目を経由せずそのまま返すのがProjectionQueryの仕組みではないかと思われます。

Slim3での使い方

今回はSlim3に手を入れて以下のような通常のクエリと同じような形でProjectionQueryを実行できるようにしてみました。
(ビルドしたjarファイルもあるので、手軽に確認してみたい人はお試しください)

BookMeta e = BookMeta.get();
List<Book> books = Datastore.query(e)
    .filter(e.userId.equal("xxxx"))
    .select(e.price, e.boughtMonth)
    .asProjectionList();
    
for(Book book : books){
    System.out.println(book.getPrice()); // 520: 値段が入っている
    System.out.println(book.getRating()); // null: 指定していない値はnull
}

この例はユーザxxxxの蔵書情報を取得しています。3行目では通常のQueryと同様にユーザIDを指定しています。4行目のselect句では取得したいプロパティを指定し、5行目のasProjectionListでProjectionQueryとして実行し、Projectionの結果を受け取っています。

ProjectionQueryの結果は通常のQueryの結果とは異なり、Query指定時にselectで指定したプロパティの値しか含まれていません。(そのためProjectionQueryで取得した結果をputしないよう注意が必要です)

ProjectionQueryはどういう場面で使えるか

このProjectionQueryはデータを集計するシーンで効果を発揮するように思います。

例えば蔵書管理アプリの場合、あるユーザの月間・年間の購入費をカウントするには通常のQueryだとBookオブジェクトを取ってこないといけませんでしたが、ProjectionQueryだと購入額プロパティをindex対象とし、Projection取得することで、keysOnlyのコストで購入費をまとめて手軽に取得できるようになります。購入費以外に購入月などもプロパティとして取得すると、月ごとの集計なども同時に行えます。

逆にProjectionQueryでは取得対象となるプロパティをindex化する必要があるため、読み込みに比べて頻繁に書き換えがあるようなプロパティには向かないです。

slim3の書き換え内容

slim3のソースで手を入れたのは、以下3ファイル、追加2ファイルです。

※試作レベルなので動作に責任は持てません。
間違いなどあればご指摘頂けるとうれしいです。

AbstractQuery.java

// 以下プロパティ、メソッドを追加
    protected List<PropertyProjection> projections = new ArrayList<PropertyProjection>();
    protected PreparedQuery prepareProjectionQuery() {
        applyFilter();
        applyProjection();
        return txSet ? ds.prepare(tx, query) : ds.prepare(query);
    }
    public List<Entity> asProjectionEntityList() {
        PreparedQuery pq = prepareProjectionQuery();
        return pq.asList(fetchOptions);
    }
    protected void applyProjection() {
        for (PropertyProjection projection : projections) {
            query.addProjection(projection);
        }
    }
    

ModelQuery.java

// 以下メソッドを追加
    public ModelQuery<M> select(CoreAttributeMeta... attribute)
            throws NullPointerException {
        List<ProjectionCriterion> criteriaList = new ArrayList<ProjectionCriterion>();
        for(CoreAttributeMeta a : attribute){
            criteriaList.add(new DefaultProjectionCriterion(a));
        }
        ProjectionCriterion[] criteria = new ProjectionCriterion[criteriaList.size()];
        for(int i=0; i<criteriaList.size(); i++){
            criteria[i] = criteriaList.get(i);
        }
        projections.addAll(DatastoreUtil.toProjections(modelMeta, criteria));
        return this;
    }
    
    public List<M> asProjectionList() {
        applyPolyModelFilter();
        List<Entity> entityList = asProjectionEntityList();
        List<M> ret = new ArrayList<M>(entityList.size());
        for (Entity e : entityList) {
            ModelMeta<M> mm = DatastoreUtil.getModelMeta(modelMeta, e);
            M model = mm.entityToModel(e);
            mm.postGet(model);
            ret.add(model);
        }
        ret = DatastoreUtil.filterInMemory(ret, inMemoryFilterCriteria);
        return DatastoreUtil.sortInMemory(ret, inMemorySortCriteria);
    }

DatastoreUtil.java

// 以下メソッドを追加
    protected static List<PropertyProjection> toProjections(ModelMeta<?> modelMeta,
        ProjectionCriterion... criteria) {
        List<PropertyProjection> list = new ArrayList<PropertyProjection>(criteria.length);
        for (ProjectionCriterion c : criteria) {
            if (c == null || c.getProjection() == null) {
                throw new NullPointerException(
                    "The element of the criteria parameter must not be null.");
            }
            list.add(c.getProjection());
        }
        return list;
    }

ProjectionCriterion.java

public interface ProjectionCriterion {
    public PropertyProjection getProjection();
}

DefaultProjectionCriterion.java

public class DefaultProjectionCriterion extends AbstractCriterion implements ProjectionCriterion {
    protected PropertyProjection projection;
    public DefaultProjectionCriterion(CoreAttributeMeta<?, ?> attributeMeta) throws NullPointerException {
        super(attributeMeta);
        projection =
            new PropertyProjection(
                attributeMeta.getName(),
                attributeMeta.attributeClass);
    }
    @Override
    public PropertyProjection getProjection(){
        return this.projection;
    }
}