2009年12月23日水曜日

Nested Set Model と Java のマッピング例

前ポストの続きで、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にはタッチしない。 さて、実装クラスについては、次のことが問題になる。
  1. いつ、どのようにして NestedSet インスタンスと Category を関連付けるか。
  2. 実際の 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として許容されるものは複合キーでもその他の型でも、なるべく対応できたほうがいい。
以下は、ちょっと研究が必要な課題。
  • ancestor で集められた 各祖先 Category オブジェクトには、NestedSet オブジェクトが関連付けられていない。これは上例のサンプルが、EntityManager のメソッド実行から連なる Categoryインスタンス生成を joinpoint としている一方、祖先 Cateogry の生成は Query.getResultList() 呼び出しから始まるものでコールスタックに EntityManager のメソッドが無いため。 これは EntityCreationWormhole アスペクトの callee ポイントカットに Query のメソッド実行を書き足せば、何とかなりそう。
  • Category のクラス定義で、抽象アスペクトを具象化しているが、できれば エンティティ・クラスは Java にしたい。本当は、こんな風に
    public interface NestedSet<T> {
       static aspect UseNestedSet extends NestedSetIdiom{}
       List<T> ancestors();
    }
    書きたかったが、文法的にだめらしい。
  • Adjacency List Model(自テーブルを再帰参照するやつ)との共存。
以下は、難しいのがわかっている事柄。
  • 追加・削除。

0 件のコメント:

コメントを投稿