argius note

プログラミング関連

Cayenne(3.0.2)で任意のSELECT文+SQLを外部ファイルにする

※使用しているCayenneのバージョンは3.0.2ですが、バージョン3以上なら使えると思います。
Cayenneでは、CayenneModelerで"Query"というマッピングを作ることができ、SQLTemplateとして利用できます。この機能は、Adapter(DBMSのインターフェイスみたいなもの)ごとに別のSQLを記述できたりするので便利です。
ただ、このデータは当然ながらXMLファイルの中に書かれます。


今回は、*.sqlファイルにSQLを書いておき、これを使ってSelect文を実行する汎用的な方法を考えてみました。


Cayenneの設定は、このエントリ(Wicket+CayenneでWebアプリ)のものを流用します。


Cayenneバージョン3から導入された機能に、DataRowというものがあります。これは、結果レコードをマップのようなデータ構造で返すというものです。実際、DataRowクラスはHashMapのサブクラスとなっています。
これとSQLTemplateを組み合わせると、任意のSQL文字列で検索を実行できます。

final String sql = "SELECT * FROM TASKLIST LIMIT 3";
ObjectContext ctx = DataContext.createDataContext();
SQLTemplate q = new SQLTemplate(TaskItem.class, sql);
q.setFetchingDataRows(true);
@SuppressWarnings("unchecked")
List<DataRow> rows = ctx.performQuery(q);
for (DataRow row : rows) {
    System.out.println(row.get("title"));
}

SQLTemplate#setFetchingDataRows(true)を設定することで、performQueryの結果がListとして返されます。
値を取得する際のキーは、DBMSによって大文字小文字が変わってきます。この例ではPostgreSQLを使っています。


DataRowのままだと使いにくいと思ったら、ObjEntityに変換してみましょう。
ObjEntityに変換すると言っても、この方式はObjEntityのスーパークラス(ここではPersistentBase)を作っておき、そこにDataRowを渡してしまうだけです。
当然、DataRowのままより処理コストがかかりますので、パフォーマンスを優先したい場合は出力の(編集処理などの)直前までDataRowを持っていったほうが良いと思います。

  • PersistentBase.java
import org.apache.cayenne.*;

public abstract class PersistentBase extends CayenneDataObject {

    private transient DataRow dataRow;

    public void setDataRow(DataRow dataRow) {
        this.dataRow = dataRow;
    }

    @Override
    public final Object readProperty(String propertyName) {
        if (dataRow != null) {
            return dataRow.get(propertyName.toLowerCase());
        }
        return super.readProperty(propertyName);
    }

}

これをObjEntity(TaskItemクラス)のスーパークラスにすれば、TaskItemのgetterが呼ばれた時にreadPropertyが呼ばれ、結果としてDataRowから値が取得できます。dataRow.get(propertyName.toLowerCase())のところは、DBMSによって変わるかもしれませんので注意してください。
もうひとつ、SELECTの結果をListとして返すためのヘルパーメソッドを定義します。この中でPersistentBaseを使います。

public static <T extends PersistentBase> List<T> select(Class<T> c,
                                                        String sql) {
    ObjectContext ctx = DataContext.createDataContext();
    SQLTemplate q = new SQLTemplate(c, sql);
    q.setFetchingDataRows(true);
    @SuppressWarnings("unchecked")
    List<DataRow> rows = ctx.performQuery(q);
    List<T> a = new ArrayList<T>(rows.size());
    for (DataRow row : rows) {
        try {
            T o = c.newInstance();
            o.setDataRow(row);
            a.add(o);
        } catch (InstantiationException ex) {
            throw new RuntimeException(ex);
        } catch (IllegalAccessException ex) {
            throw new RuntimeException(ex);
        }
    }
    return a;
}

これを使って最初のSELECTを書き直してみます。

final String sql = "SELECT * FROM TASKLIST LIMIT 3";
List<TaskItem> rows = select(TaskItem.class, sql);
for (TaskItem row : rows) {
    System.out.println(row.getTitle());
}


最後に、SELECT文のSQLを"Q0001.sql"に保存して使えるようにします。
"Q0001.sql"をTaskItemクラスと同じパッケージに置きます。これをClass#getResourceAsStreamなどでSQL文字列として読み込んで、ヘルパーメソッドselectを使えば実現できます。
実際はgetResourceAsStreamを使うのは面倒なので、Java7ならFiles.readAllLines、WicketならResourceUtil.readStringを使うと楽です。

import org.apache.wicket.resource.*;
import org.apache.wicket.util.resource.*;

    public static <T extends PersistentBase> List<T> performSelectSqlFile(Class<T> c, String id) {
        final String sqlFileName = id + ".sql";
        final String sql = ResourceUtil.readString(new PackageResourceStream(c, sqlFileName));
        return select(c, sql); // 前述のヘルパーメソッド
    }

    public static void main(String[] args) {
        List<TaskItem> rows = performSelectSqlFile(TaskItem.class, "Q0001");
        for (TaskItem row : rows) {
            System.out.println(row.getTitle());
        }
    }