前ポストの続きで、RDB の Nested Set Model と POJO との対応を考えてみる。
Nested Set モデルに従って電化製品の種別を表す、下のようなテーブル `category`を、クラス Category にマッピングするとして、、、
+-------------+----------------------+------+------+
| category_id | name | lft | rgt |
+-------------+----------------------+------+------+
| 1 | ELECTRONICS | 1 | 10 |
| 2 | TELEVISIONS | 2 | 9 |
| 3 | TUBE | 3 | 4 |
| 4 | LCD | 5 | 6 |
| 5 | PLASMA | 7 | 8 |
+-------------+----------------------+------+------+
こんな風に書いて、、、
public static void main(String[] args) {
・・・ 略
EntityManager em = emf.createEntityManager();
Category node = em.find(Category.class, 4);
someBusinessLogic(node);
・・・ 略
}
static void someBusinessLogic(Category node) {
for (Category a: node.ancestors()) {
System.out.printf("%s %s%n", a.getId(), a.getName());
}
}
、、、以下のような出力を得たい。
1 ELECTRONICS
2 TELEVISIONS
someBusinessLogic() では EntityManager を触っていない事に留意しつつ、どんな風に書けるか考えてみる。(話を簡単にするため、Nested Set への操作は祖先ノードを検索する ancestors() メソッドのみとする。)
■ 実装例
◆エンティティクラス
まず、RDBのテーブルにマッピングされる Category エンティティはこんな風にした。
@Entity(name="category")
public class Category {
static aspect UseNestedSet extends NestedSetIdiom<Category>{}
@Id()
@Column(name="category_id")
private int id;
private String name;
@Transient
NestedSet<Category> nestedSet;
・・・id と name のゲッタセッタ
public List<Category> ancestors() {
return nestedSet.ancestors();
}
}
ここで、フィールド nestedSet は Category を要素として持つ Nested Set への ファサード・オブジェクトで、以下のインターフェイスを持つ。
public interface NestedSet<T> {
List<T> ancestors();
}
interface 定義の中では、Nested Set というデータ集合への操作を契約として表現するのみで、JPA など下位レイヤのAPIにはタッチしない。
さて、実装クラスについては、次のことが問題になる。
- いつ、どのようにして NestedSet インスタンスと Category を関連付けるか。
- 実際の DB へのアクセスは、NestedSet 実装クラスが受け持つ事になるが、そこで用いる EntityMangaer をどう関連付けるか。
これらの問題を解決するために、AspectJ を使ってみた。上記 Category クラス定義冒頭の内部アスペクトはそのためのもので、抽象アスペクト NestedSetIdiom を具象化している。
◆アスペクトコード
抽象アスペクト NestedSetIdiom は以下のようなもので、NestedSetUtil.injectNestedSet() を使って、NestedSetImpl、EntityManager、および T の間の関連付けを行う(この例では T=Category )。
public abstract aspect NestedSetIdiom<T>
extends EntityCreationWormhole<T> {
protected void postEntityCreated(EntityManager em, T entity) {
NestedSetUtil.injectNestedSet(em, entity);
}
}
class NestedSetUtil {
public static <T> void injectNestedSet(EntityManager em, T entity) {
try {
for (Field f: entity.getClass().getDeclaredFields()) {
if (NestedSet.class.equals(f.getType())) {
f.set(entity, new NestedSetImpl<T>(em, entity));
return;
}
}
} catch (Exception e) ・・・略
}
・・・略: その他メソッド
}
postEntityCreated() は、
- 基底抽象アスペクト EntityCreationWormhole で定義されたタイミングで呼ばれ、
- Wormhole を通じて渡されてきた EntityManager で NestedSet 実装クラスを初期化し、
- これを T entity のフィールドとして挿し込む。
アスペクト EntityCreationWormhole の定義は以下のようになる。
public abstract aspect EntityCreationWormhole<T> {
pointcut caller(EntityManager em):
call(public * EntityManager.*(..)) && target(em);
pointcut callee(): execution(T+.new(..));
pointcut wormhole(EntityManager em) : cflow(caller(em)) && callee();
after(EntityManager em, T entity): wormhole(em) && this(entity) {
postEntityCreated(em, entity);
}
protected abstract void postEntityCreated(EntityManager em, T entity);
}
これで上記の問題1と2が解決できた。あとは NestedSetImpl の ancestors()実装で、EntityManager を使った先祖 Category 検索コードを書くだけ。
◆ NestedSet 実装クラス
これは適当にどんな書き方でも良いが、例えば以下のように書ける。
class NestedSetImpl<T> implements NestedSet<T> {
public static final String SELECT_ANCESTORS =
"select p.* from %1$s as p, %1$s as c " +
" where c.%2$s = ?1 " +
" and p.lft < c.lft and c.rgt < p.rgt";
private final T entity;
private final EntityManager entityManager;
NestedSetImpl(EntityManager em, T entity) {
this.entityManager = em;
this.entity = entity;
}
public List<T> ancestors() {
String query = String.format(
SELECT_ANCESTORS,
NestedSetUtil.tableNameOf(entity),
NestedSetUtil.idColNameOf(entity));
return (List)entityManager.createNativeQuery(query, entity.getClass())
.setParameter(1, NestedSetUtil.idOf(entity))
.getResultList();
}
}
ここで使っている NestedSetUtil の tableNameOf(), idColNameOf(), getIdOf() はベタなリフレクションコードなので割愛するが、以下のような仕様になる。
- tableNameOf(T entity): T についた@Entity のname属性を返す。
- idColNameOf(T entity): T のフィールドのうち、型 int で @Id アノテーションがついたものを見つけて、その @Column の name属性を返す。
- idOf(T entity): T のフィールドのうち、型int で@Id アノテーションがついたものを見つけて、その値を返す。
とりあえず上記のようなコーディングで、JPA コードを排除した Java ビジネスロジックコードから、Nested Set Model 準拠 テーブルにある情報にアクセスできるようになる。
Nested Set への他の検索操作(例えば子孫要素の一括取得など)も NestedSet で宣言してNestedSetImplで実装する形で追加できる。
■ 制約と課題
上述の解は、Proof-of-concept 目的のサンプルコードで、いくつか制約がある。また AspectJ や Nested Set Model に関連する本質的な課題もある。
以下、単純にコードを書き足すだけでどうにでもできそうな制約。
- NativeQuery を作る際、@Entityや@Columnで表名・列名が明示されていることに依存しているが、本来はクラス名とフィールド名からデフォルト名を得ることもできるようにすべき。
- ネイティブ SQL Where句中の、自分の位置を決めるため等式 c.%2$s = ?1で、単一の int 列を id として決め打ちしているが、JPA でIDとして許容されるものは複合キーでもその他の型でも、なるべく対応できたほうがいい。
以下は、ちょっと研究が必要な課題。
以下は、難しいのがわかっている事柄。
0 件のコメント:
コメントを投稿