HoneyAnt

少し時間があったので, HoneyAnt という Eclipse Plugin を公開しました。 これは Eclipse 上でインクリメンタルに Ant を実行するというプラグインで, 特定のアノテーションが付いたファイルを編集すると, 自動的に Ant が実行されるという機能を提供します。

HoneyAnt の使い方

HoneyAnt を clone すると honeyant-feature-updatesite というプロジェクトがあるので, これを Eclipse でインストールしてください。再起動すると, Java プロジェクトのプロパティに「HoneyAnt」という項が増えているはずです。サンプルとして honeyant-example というプロジェクトも用意しているので, ひとまずこれをベースに説明します。

honeyant-example のプロパティを見ると, HoneyAnt が有効になっており, [ Builders ] に HoneyAnt Runner というビルダが追加されていることが分かると思います。これがインクリメンタルビルドを実行するビルダです。次に Person.java というファイルを開いてください。ただのバリューオブジェクトですが, @HoneyAnt というアノテーションを付与しています(※アノテーションは自由に指定出来ます)。ではここに String sex というプロパティを追加して保存してみましょう。

1
2
3
4
5
6
7
8
9
10
11
Buildfile: xxx\honeyant-example\honeyant.xml

honeyant:
     [echo] HoneyAnt incremental build: com.usopla.honeyant.example.source.Person (xxx\honeyant-example\src\main\java\com\usopla\honeyant\example\source\Person.java)
     [dump] dump to xxx\honeyant-example\dest\dump.txt

refresh:
BUILD SUCCESSFUL

BUILD SUCCESSFUL
Total time: 0 seconds

すると自動的に Ant が実行され, このようなログが表示されます。dump.txt を見てみましょう。

1
2
3
4
5
6
7
path=xxx\honeyant-example\src\main\java\com\usopla\honeyant\example\source\Person.java
class : com.usopla.honeyant.example.source.Person
package : com.usopla.honeyant.example.source
fields ->
  name (java.lang.String)
  age (int)
  sex (java.lang.String)

このように編集後のクラス定義を元にファイルが自動生成されています。ここで実行しているのは DumpTask というサンプルの Ant タスクで, クラス情報をリフレクションして適当に出力しています。

何故つくったか?

2 年程前に仕事で新しいパッケージプロダクトを作ることになり, そのときにつくったものです(もちろん同じものではなく, 全体的にリファインして HoneyAnt として公開しています)。当時 Hibernate を使っていたのですが, 折角 O/R マッパーを使っているのにタイプセーフにクエリが書けないことに不満を感じていました。Hibernate には Criteria API というものが存在し, これを使うとある程度オブジェクト指向的にクエリを書くことが出来るのですが, これは(属性名の文字列指定や型情報の欠落を伴う)ランタイムバインドを行うもので, 安全性・保守性・生産性の面から好ましくありませんでした。

1
2
3
4
5
6
7
8
9
10
// Hibernate の Criteria API
// プロパティ名を文字列で指定。引数も Object 型。
Criteria criteria = session.createCriteria(Cat.class);
criteria.add(Restrictions.like("name", "Fritz%"));
criteria.add(
  Restrictions.or(
    Restrictions.eq("age", 0), // 文字列でもバインド出来てしまう
    Restrictions.isNull("age") // プロパティ名を間違えたらランタイムエラーになる
  )
);

「どうにかタイプセーフに出来ないものか」と悩んでいた当時, Slim3 のタイプセーフなクエリに感銘を受けて「コレだ!」と思ったのでした。リフレクションを使うのではなく, 予めエンティティのメタ情報を定義しておき, これを用いてクライテリアを表現すればパフォーマンス面でも型安全性の面でも良いのだと気付いたのでした。また 「折角メタ情報を作るのであれば, エンティティのメタ定義からエンティティ, DDL, hbm, クライテリア用のメタクラスも全て自動生成すれば夢が広がるなぁ」などと思いつき, これを実現する方法を考えてみることにしました。

自動生成の実現にあたって, 最初は Slim3 と同じように APT (Annotation Processing Tool) を用いた自動生成を行おうとしたのですが, APT は意外と自由度が低くファイルの自動生成にはあまり向いていないなと感じました。当初エンティティのメタ情報を Java もしくは Groovy クラスとして定義し, そのクラス情報をリフレクション経由で取得してファイルを自動生成しようとしていたのですが, APT を使うと修正しているファイルのクラス情報を完全に取得することが出来ません(でした……今は違うのかもしれません)。「特定のファイルを編集したときに, もっと自由に自動生成したいなぁ」と思い, Eclipse の Ant を自動的に実行すれば良いかな、と思いついたのでひとまず Eclipse の Plugin をプロトタイプしてみることにしました。Plugin の作り方なんてさっぱり知らなかったのですが, 本やコードを読みながら 2〜3 日ほどかけて実装してみると, 思いのほか上手くいったので APT は止めて Eclipse Plugin にすることにしました。結果的にできあがったのが HoneyAnt でした。

実際にどのように使ったか?

さて, 当時の PJ ではエンティティは全てメタ定義(Java クラス・ランタイムでアプリケーションに含まれない)を元に HoneyAnt で自動生成しました。例えば Redmine のようなプロジェクト管理アプリケーションの「プロジェクト」をサンプルにすると, メタ定義はこんな感じです(適当な例にしています)。テーブル名やカラム名をデフォルトで推測したり, Hibernate の主要な機能は一通り使えるようにしたりしました。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
@HoneyAnt("ユーザー")
public class UserSource extends ClassSource {

    @Property("ID")
    NumberProperty<Long> id = sequenceIdProperty();

    @Property("名前")
    StringProperty name = stringProperty(20).notNull();

    @Property("メールアドレス")
    StringProperty email = stringProperty(50).notNull();

    @Property("管理者")
    BooleanProperty admin = primitiveBooleanProperty();

    @Property("ステータス")
    UserTypeProperty<UserStatus> status = userTypeProperty(UserStatus.class).notNull();

}


@HoneyAnt("プロジェクト")
public class ProjectSource extends ClassSource {

    @Property("ID")
    NumberProperty<Long> id = sequenceIdProperty();

    @Property("序数")
    NumberProperty<Integer> sequenceNo = primitiveIntProperty(10);

    @Property("名称")
    StringProperty name = stringProperty(50).notNull();

    @Property("ステータス")
    UserTypeProperty<ProjectStatus> status = userTypeProperty(ProjectStatus.class).notNull();

    @Property("メンバー")
    @Index(name = "IXA_PROJECT_01")
    ManyToManyProperty<User>> users = manyToMany(UserSource.class, "PROJECT_USER_RELATION").lazy(true);

}

このファイルからおおよそ以下が HoneyAnt により自動生成されます。

  1. エンティティ・エンティティの基底クラス(Generation Gap パターン)
  2. エンティティメタクラス・エンティティメタ基底クラス(Generation Gap パターン)
  3. hbm
  4. DDL
  5. テーブル定義書等

これだけでも十分便利なのですが, このときに一番実現したかったものは「タイプセーフなクエリ」です。これを実現する際の肝になっているのが 2. の「エンティティメタクラス」です。このメタクラスを使用してタイプセーフクライテリアを実現しました。タイプセーフクライテリアの実現に当たっては, まずタイプセーフなクライテリア API を作り, 次に HoneyAnt でこの API を利用したエンティティメタクラスを自動生成するようにしました。エンティティメタクラスを利用したタイプセーフなクライテリアは, 以下の場面で使用しました。

  • DAO(Hibernate の Criteria API に変換してタイプセーフなクエリを実現)
  • 継続クエリ(OQL に変換してサーバサイドに登録し, メッセージフィルタとして使用)

クライテリア API を独立させることによって, 様々な場面で活用出来る拡張性を持たせています。パーサさえ書けば様々な要素にバインド出来るので, MongoDB 用のアダプタなんかも作ろうかなと思っていました(このときは使用しなかったので作りませんでしたが)。

では実際に使う場合の簡単な例を紹介します。DAO として使用する場合はこんな感じです(クエリの意味は適当ですスミマセン)。ここではひとつずつ検索条件をその場で作っていますが、Rails の Named Scope のようにメタクラスに条件を定義することも出来ます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
ProjectMeta meta = ProjectMeta.getInstance();
Dao<Project> dao = daoFactory.createDao(meta);

// ex. update
dao.save(project);
dao.update(project);
dao.delete(project);

// ex. entity-query(エンティティ単位)
{
    EntityQuery<Project> query = dao.createQuery();
    query.orderBy(meta.id, Order.ASC);
    Criteria<Project> criteria = meta.createCriteria().add(meta.sequenceNo.lt(5)).add(meta.status.eq(ProjectStatus.ACTIVE));
    List<Project> projects = query.list(criteria);
}

// ex. projection-query(射影)
{
    ProjectionQuery<Project> query = dao.createProjectionQuery();
    Criteria<Project> criteria = meta.createCriteria().add(meta.users.admin.isTrue()).add(meta.users.status.eq(UserStatus.ACTIVE));
    List<Long> userIds = query.listValues(criteria, meta.id); // もちろん複数プロパティの射影も可能
}

// ex. aggregation-query(集約)
{
    AggregationQuery<Project> query = dao.createAggregationQuery();
    AggregationFunctionProperty<Project, Long> maxUserIdProperty = Aggregations.max(meta.users.id);
    Criteria<Project> criteria = meta.createCriteria().add(meta.id.eq(1L)).add(meta.status.eq(UserStatus.ACTIVE));
    Long maxUserId = query.uniqueValue(criteria, maxUserIdProperty);
}

見ての通り一通りタイプセーフにクエリが出来るようになっています。当然ですがクライテリアには強い型制約を持たせています。上記の例ではプロパティ同士の比較はしていませんが, 例えばプロパティ同士の比較クライテリオン ge (greater than or equals to) はこんな感じのシグネチャです。

1
2
// E はエンティティの型・C は比較される値の型
<E, C extends Comparable<? super C>> PropertyCompareCriterion<E, C, ComparableProperty<? super E, C>> ge(ComparableProperty<? super E, C> condition)

かなり強い型制約を持たせていることが分かるかと思います。

継続クエリとして使用するときも同じクライテリア API を用いることが出来ます。

1
2
3
4
ProjectMeta meta = ProjectMeta.getInstance();
Criteria<Project> criteria = meta.createCriteria().add(meta.status.eq(ProjectStatus.ACTIVE));
CriteriaFilter filter = CriteriaFilterFactory.create(criteria);
boolean b = filter.apply(project);

同じ API を使用しているので, 検索条件をひとつ作れば DB アクセスにも継続フィルタを使用した非同期更新キャッシュなんかにも使うことが出来ます。

というわけで

色々な用途に使えると思うので, 良ければご自由にお使い下さい。

Java で DSL を作るのは難しいです。DSL を定義するのであれば Groovy や Ruby を使った方がよほど楽でしょう。ですので Java で DSL を無理矢理つくるよりは、タイプセーフなメタ情報を自動生成するのが個人的にはオススメです。今なら Xtext を使うのもひとつ有効な手段なのかもしれませんが、使い慣れた Java で Ant タスクとして自動生成処理を定義したい場合には HoneyAnt を使うのも良いかなと思います。

Comments