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
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()); } }