本腰入れてActiveObjects(その3) ActiveObjectsを使ってEntityを作成する。それと@NotNullとか@Defaultとか、それと@Generator

今回は「ActiveObjectsを使ってEntityを作成するには」と言う事で前回先に取り上げておけば、と思ったEntityManager#create(RawEntity,DBParam...)辺りをば。
と言っても、こいつはJavaDoc見ればワカンジャンっていうくらいのものですね。DBに実際にINSERT文を発行してEntityをつくります。EntityManager#create(RawEntity,Map)っていうのもありますが、これは中でMapからDBParamsを作ってEntityManager#create(RawEntity,DBParam...)を呼び出すだけです。わかりやすい。
で、実際にDBにレコードをつくろうとかなると、デフォルト値やらNotNull制約やらつけたくなりますよね。そんなときはアノテーションが@Defaultとか、@NotNullとか。その辺の制約をつけたい場合はカラムに対応するアクセッサにこの辺のアノテーションをつけてください。
デフォルト値としてUUIDをつけたいとか、初期値の設定になんらかの処理が必要な場合に使うのが@Generatorです。@Generatorは引数にValueGeneratorインターフェースを実装して使うらしい。こんな感じ。

/** Person.class */
public interface Person extends Entity{
... 中略
	@NotNull
	@Generator(UUIDValueGenerator.class)
	public void setUUID(String uuid); // Stringなのは、DBにUUID型に対応する型が定義されているとは限らないから
	public String getUUID();
}
/** UUIDValueGenerator.class */
public class UUIDValueGenerator implements ValueGenerator<String> {
	public String generateValue(EntityManager manager) {
		return UUID.randomUUID().toString(); // managerを使うのはDBから取得した値を使う場合とか?いろいろできちゃいそうだけど、やると首締めるね。きっと。
	}
}

で、EntityManager#get(Class>,K key...)について。これ、二つめ以降の引数で指定できるkeyは、RawEntityのGenericsの型、つまりPKを指定します。Entityを継承している場合は、整数(intかinteger)を指定しますが、DBに存在しない値を指定したらどうなるでしょう?ちゃんとJavaDocに書いてます。

No checks are performed to ensure that the key actually exists in the database for the specified object.Thus, this method is solely a Java memory state modifying method. There is no database access involved. The upshot of this is that the method is very very fast. The flip side of course is that one could conceivably maintain entities which reference non-existant database rows.

私的訳

渡されたPKが本当にDBに存在するかはチェックしません。したがって、このメソッドはJavaのメモリ上の状態だけ更新します。そしてDBへのアクセスは実行されません。なので、このメソッドを用いて取得したオブジェクトがDBに存在しない場合、このオブジェクトの処理はとてもとても早く実行されます。この事を裏返せば存在しないデータベースの行を参照しているともいえるでしょう。

言っている事はややこしいんですが、要するにDBを用意しなくてもEntityの振る舞いをテストできるということなんです。どういうことか。EntityManager(Class>,K key...)で取得したオブジェクトに値を設定し、その後取得すると、@Implementationで定義した振る舞いも実行されます。(ただ取得だけするとDBから取得しようとするのでアウト)また、テスト用のモックを作成しないでも値を設定して使えるということで、DBのセットアップをする必要がないんです。あんまりそんなフレームワークを知らなかったので、これ変態だと思ったんす。どうすかね。
次回はオブジェクト同士の関連の記述方法辺りをば。

本腰入れてActiveObjects(その2) @Preloardと@Implementationそしてなぜだか@Ignore

メリクリですね。メリクリ。あんまりそんな気分でもないのだけれども。
前回PreloadアノテーションとImplementationアノテーションについて解説するぜ!って言ってみました。が、たぶんActiveObjects入門とか、「使おうぜ」という本筋からすると、PreloadアノテーションやらImplementationアノテーションはだいぶチガウ位置にいますね。はい。そこは否定しませんが、そんなことは知りません。きっとサンタさんの贈り物です。そのうちおいおい「使おうぜ」編については書くでしょう。
はい。Preloadアノテーションは某女史が、「EntityManger#find()を使って取得したEntityオブジェクト」のGetterを呼ぶ度にSelect文が飛ぶとは何事デスの?と言っていました。何ということでしょう、その通り。EntityManager#find()系のメソッドはEntityの取得はできますが、何も指定していないとキャッシュ領域を使用しないようにIDカラムのみ取得します。Getterを呼ぶ度にSQLを発行しては、その値をメモリにキャッシュしてくれるのです><

普通DBから取得するのは1行まるまるだよね。(これは言い過ぎか。)

SELECT * FROM なんとか

だよねって事で利用するのがPreloadアノテーション。このアノテーションは型に対して修飾する(って表現あってんのかな?)アノテーションで、その1のPersonの例でいうと、

@Preload
public interface Person extends Entity{
  ... 中略 ...
}

とかやるわけですよ。@PreloadのJavaDocによると、valueとして文字列配列が持てて、列名を書いてね、デフォルトは"*"だからね、って書いてあるので、もし列名を指定したければ指定するとよろし。

はい次!Implementationアノテーション。こいつについては実は過去書いてたから飛ばします><ActiveObjectsの変態加減を感じてください。
ActiveObjectsの変態的な考え方にまたまた驚愕 - Fly me to the Juno!
あわせて読んでね、@Ignore編
なるほど。@Ignoreか。 - Fly me to the Juno!

はい、読んでいただけましたか?Implementationアノテーションはインターフェースに対して実装を付加することのできるアノテーション、IgnoreアノテーションはDBとは関係ないメソッドだよ!っていうことをActiveObjectsに教えてあげるアノテーションです。実装の方にはフィールドを定義できるので、この二つを組み合わせると永続化したくないけどメモリ上には持っておくとかできます。また、Entityの親のRawEntityで定義されているinit()は、Implementationアノテーションを使って実装をくっつけることで初めて使われるので覚えておくといいかもしれません。

次回はほんとは先にやるべきだったEntityManager#create(RawEntity,DBParams[])辺りをば。EntityManager#get(Classclass,K key)の不思議っぷりも。こんなんよくやるわー。

本腰入れてActiveObjects (その1) EntityManager#migrate()

java-ja忘年会に急遽行ってみたらid:yuripopのサンタコスにヤラレタ。なんだかリア・ディゾンぽかったんだけど、id:yuripopってそういう方でしたっけ?ともかく、普通にかわいくてビビった。id:t_yano氏が終始ドキドキしていたのも頷ける。
そういえばActiveObjectsのEntityManager#migrate()がすごいっていう事を語れとid:yuripopから言われていた気がするので、今日はその辺を頑張ってみることにした。ちなみにActiveObjectsはJAMCircleの中でも利用しています。かれこれ半年くらいお付き合いしてきた成果があるのでつらつらと書いていきますかね。
さて、ActiveObjectsはインターフェースベースのO/Rマッパーです。RailsのActiveRecodeに影響されて作られました。と言っても、他のO/Rマッパーは頭の片隅にも残ってないので、O/Rマッパーって普通インターフェースベースでしょ?ってツッコミを頂くかもしれませんね。でも基本的にDBを使わないようにしてたので、よく知りません。3年くらい前にHibernateを使った事ありますけど、設定ファイルにいろんな事を書かなきゃいけなくて、めんどかったです。ActiveObjectsは基本的に設定ファイルにガシガシテーブルとオブジェクトのリレーションを書いていくなんて事はありません。
インターフェースにアノテーションで色々書いていく事でオブジェクト同士を関連付けたり、DBに格納するタイプを指定したりします。DBのテーブル名やカラム名は規約を使って自動的にマッピングされるので、ほとんど意識しません。自動的にマッピングするためのDBスキーマを作ってくれるのが、EntityManager#migrate(RawEntity...)です。
RawEntityはDBの1行に対するインターフェースで、それを継承して永続化したいオブジェクトのインターフェースを作成していきます。Genericsで指定しているTはPKの型を指定します。既存のものでEntityという、PKのカラム名をID、型をIntegerとしているインターフェースがあるので、普通はそれを継承すればいいでしょう。こんな感じで作ります。

	public interface Person extends Entity{
		public String getName();
		public void setName(String name);
		public boolean isClever();
		public void setClever(boolean clever);
	}

こうすると、Personという名前のテーブルと、文字列型のnameカラム、論理値型のcleverカラムが作成されるわけです。ここで出てきた型は、DBごとに適した型で作成されます。その辺はまた今度。
話をEntityManger#migrate()に戻します。このメソッドを呼び出すと、指定したインターフェースのテーブルが無いか検索し、無ければ新規作成ということでCREATE TABLE文を、あれば、既存のテーブルと比較し、たらない項目があればALTER TABLE文で追加、多すぎる項目があればDROP TABLE文→CREATE TABLE文を実行します。なのでオブジェクトのプロパティを追加していくだけであれば基本的にデータ移行は発生しません。っていうのがEntityManager#migrate()のすごいところ。さっきの例で言うと、整数型のageカラムを追加しよう、ってなったときは

	public interface Person extends Entity{
		public String getName();
		public void setName(String name);
		public boolean isClever();
		public void setClever(boolean clever);
		// 追加
		public int getAge();
		// 追加
		public void setAge(int age);
	}

ってした後で、

// DBごとの接続先Provider
DatabaseProvider provider = new なんとかProvider(uri,username,password);
EntityManager manager = new EntityManager(provider);
manager.migrate(Person.class);

とかしてあげると、それだけでPersonテーブルが更新されます。
今のActiveObjectsではサポートされていませんが、将来的には新規に追加したカラムの移行直後の値をアノテーションで指定できるようにするらしい。

12/24追記
ちなみにEnumもDBに自動的にマッピングしてくれます。例えば先ほどのPersonの例でいうと、

public enum Gender implements Serializable{
  MALE,FEMALE
}
public interface Person extends Entity{
  ... 中略 ...
  public Gender getGender();
  public void setGender(Gender gender);
}

とかすると、DBに整数型のGENDERカラムができて、MALEだったら0、FEMALEだったら1を格納してくれます。もちろん取り出す時は意識しないよ。この例だとenumに他のタイプを追加しづらいです(ゲイとか?)けど、例えばJAMCircleの中だと色をenumとして定義しているので、新しい色をどんどん後ろに追加していく事ができますね。

次回はPreloadアノテーションと、Implementationアノテーションあたりについて語ってみます。

DBにマッピングするオブジェクトの継承関係

ActiveObjectsのクセがだいぶ分かってきた。
Table Inheritance with ActiveObjects - Code Commitという記事を読んで、「へぇ。継承関係を持つオブジェクトも永続化できるのか。」と思って試してみると、しばらく動かず、うんうんうなっていた。
原因は親クラスの方にテーブルに変換されてしまうようなプリミティブじゃないフィールドをもたせていたことが原因。0.8.2ではどうもそういうプリミティブでないクラスの関連を持たせるとうまくテーブルの作成順のソートができず、インデックスがはれないなどが原因で例外が投げられます。

なるほど。@Ignoreか。

なるほど。setterやgetterメソッドに@IgnoreをつけるとDBのスキーマに変換しない対象になるのか。それで代わりに実装の方でデータを保持すれば良いと。気をつけないといけないのは、DBからデータを取り直しても実装の方のフィールドの値が初期化されるわけではないと言う点。同じレコードを取得しようとした場合はキャッシュされています。前述したとおり、他のプロセスから値が書き換えられる場合は@Transientをつけるルールなので、これは別に悪いことではないです。が、ちょっとハマっていました。

import net.java.ao.Entity;
import net.java.ao.Implementation;
import net.java.ao.schema.Ignore;

@Implementation(NonPersistableFieldImpl.class)
public interface NonPersistableField extends Entity{

	@Ignore
	public void setDeleted(boolean deleted);

	@Ignore
	public boolean getDeleted();
	
	public void setName(String name);

	public String getName();

}
public class NonPersistableFieldImpl {

	private boolean deleted = false;
	
	public NonPersistableFieldImpl(NonPersistableField delegated){
		
	}
	
	public void setDeleted(boolean deleted){
		this.deleted = deleted;
	}

	public boolean getDeleted(){
		return deleted;
	}
	
}
	@Test
	public void notPersistant() throws Exception {
		NonPersistableField model = manager.create(NonPersistableField.class);
		model.setName("fool");
		model.setDeleted(true);
		model.save();
		model = manager.get(NonPersistableField.class, 1);
		assertTrue(model.getDeleted());
	}

ActiveObjectsアノテーションについてはまとめていきますかね。

Transientと言ったら

シリアライズしてもそのパラメータはシリアライズの対象外という概念が頭にあるんです。XStreamとかでオブジェクトをXMLに変換する時にtransientをつけておくとそのフィールドを外して永続化してくれます。しかしActiveObjectsの@Transientはちょと違う。DBのキャッシュ対象から外すという意味らしい。例えば他のプロセスからDBの値を書き換えられるような場合や、意図的にキャッシュしないときに@TransientをつけてくれとJavaDoc神がおっしゃっている。
永続化したくないプロパティがあったときの回避方は調査中。

ActiveObjectsの変態的な考え方にまたまた驚愕

正式リリース前のせいか、まともなドキュメントは見当たらないActiveObjects。でも面白い機能が多いせいか、いろんなところで取り上げられているみたいですね。その中でも参考になったのがJavalobbyの記事。結構後ろの「Implementing the Active Record Pattern」という節に書かれているコードが変態的。
ActiveObjectsはDynamicProxyを使ってインターフェースからオブジェクトを作成します。インターフェースに書いたゲッター・セッターがDBのカラムにマッピングされるのです。そうなってくると、セッターになにかロジックを書きたくても書けません。Hibernateなんかだとユーザーが用意したPOJOに対して、CGLIBを使ってバイトコードをいじって何とかしてますよね。でもActiveObjectsはそんな事しない。どうやってるかというとこんな感じ。

@Implementation(CompanyImpl.class)
public interface Company extends Entity {
	// ...
}

public class CompanyImpl {
	private Company company;
	
	public CompanyImpl(Company company) {
		this.company = company;
	}
	
	public void setName(String name) {
		company.setName(name);
		
		name = name.trim();
		if (name.length() > 4) {
			company.setTickerSymbol(name.substring(0, 4).toUpperCase());
		} else {
			company.setTickerSymbol(name.toUpperCase());
		}
	}
}

アノーテーションを使って、インターフェースの方に実装をくっつけてるんです。なんてコード…。初めて見た…。

プログラマが書くコードは実装に対してではなく、インターフェースに対してです。この例でいえばCompanyですね。中の動きを見てみるとCompany#setName(String)を呼ぶとCompanyImplに同一シグネチャのメソッドが無いか検索して見つかればそのシグネチャのメソッドを実行するという…。そんなところに流れている雰囲気もActiveRecodeに近いものがあるじゃないですか。面白いっすね。
ところで、ActiveObjectsでMapオブジェクトをDBにマッピングするにはどうすればいいんすかね。MapのEntryクラスを別途クラスにだして…。みたいにちょいと面倒なことをしないといけないんですかね。