インピーダンス・ミスマッチ
いわゆるObjectとRelationalのいやーんなやつです.
この「インピーダンス・ミスマッチ」,言葉はよく耳にするし,実際にビシバシと感じるわけですが,何がどうミスマッチなのかをちゃんと自分の言葉でまとめたことがないので,たまには書いてみるてすと(弱気).
このミスマッチは,次の3つを含んでいると思います*1.
- Javaなどのプログラミング言語(手続き的)とSQL(宣言的)のミスマッチ
- Record at a timeとSet at a timeのミスマッチ
- オブジェクトモデルとリレーショナルモデルのミスマッチ
と書いたものの,実は始めの二つは最後のミスマッチから導出されたものと考えられるので,根源はオブジェクトモデルとリレーショナルモデルのミスマッチなのかなぁ,と思う次第です.そしてこのミスマッチ,考えても考えても相容れなさそうというか,相性が悪いというか,どうやっても仲良くできない気がしちゃって,なのでO/Rマッピングなんていうのもちゃんちゃらおかしいね*2.
オブジェクトモデルとリレーショナルモデル,どちらかがどちらかのスーパーセットであるなら,もっとうまく一緒に使えると思うんですよね.例えばオブジェクトモデルとERモデル.ERモデルのちゃんとした定義を見たことはありませんが,なんとなくオブジェクトモデルはERモデルのスーパーセットでしょって感じがします.なので,プロセスとしてのDOAなんかはともかく,成果物としてのERモデルは,表記法さえ変えればオブジェクトモデルと言い張っても誰も気づかなさそう.そう,見た目だけの問題って感じです.
ところがリレーショナルモデルは違います.これは,オブジェクトモデルのサブセットとして扱うわけにはいかないものを持っています.
リレーショナルモデルの主な構成要素は,ドメイン(カラム),タップル(レコード),リレーション(テーブルまたはビュー,それに問合せ結果)です.
このうち,ドメインとオブジェクト(正確にはクラスですか)の相性はバッチリです.ドメインは簡単に言えば型です(細かいことは気にしない).それも値型です.数値型とか文字列型とか日付型とか.これらをオブジェクト(クラス)として見ることはごく自然です.ですから,この部分では大きな問題になるようなミスマッチはないと思います.Javaのデータ型とSQLのデータ型が微妙に違うとかみたいな程度でしょう.小さい小さい.
問題はタップルおよびリレーションです.タップルはリレーションの要素であるとして,ここではリレーションとクラスを対比します.この二つは,ドメインと(値型の)クラスのように親和性の高いもの同士ではなく,次のような相違点があります.
- リレーションは静的な状態のみを持つが,クラスはそれに加えて動的な振る舞いを持つ.
- クラスは決まりきった形(静的な構造)を持つが,リレーションは自在に生み出すことができる(動的).
このように,どちらも相手にないものを持っているのですね.
この相違点を避ければ,いわゆるO/Rマッピングも結構簡単になります.
- (継承の表現は別として)一つのクラスを一つのテーブルにして,テーブル単位にアクセスする(自在なリレーションを排除).
- そのクラスには振る舞いとしてのメソッドを持たせない(動的な振る舞いの排除).
その結果がバリューオブジェクトとかデータトランスファーオブジェクトとか呼ばれるようなものですよね.でもこれ,オブジェクトモデル(オブジェクト指向,かな)の能力を使っているとは言いがたいですし,リレーショナルモデル(RDBMS,かな)の能力を使っているとも言いがたいです.必要なカラムだけを選択したり,他のテーブルと結合したりすることで最適なDBアクセスができるのに.
ここで行き詰まっちゃうんですよね.無念だ.
自由にリレーションを生み出す能力と,動的な振る舞いを持つ能力.
この両者を共存させるにはどうすればいいのか? そう,この問題で頭がいたいのは,両者のナイスな能力をともに使いたいから.RDBMSを単なるオブジェクトのストレージとして使うのではなくて,RDBMS本来の能力(リレーショナルモデルのいいところ)も一緒に使いたいのです.なので,オブジェクトを単純にテーブルにマップするだけのO/Rマッピングツールって興味が持てないのですよね.いやその,Hibernateとかはその程度のものじゃないのかもしれませんけどね.勉強不足なのでよく知りません.心より恥じる.
それでですね,ちょっと邪道なのかもしれないけれど,もしかしてAspectがこの解決にならないかなぁって考えています.自在に作り出されるリレーションすなわち結果セットをCore Concernとみなして,それに適用できる動的な振る舞いすなわちメソッドをCrosscutting Concernとみなしてみる.うん,その方が素敵ッ! な感じがちょっとだけするんですよね.いやその,深く考えたわけじゃないんでバカなことを言っている可能性濃厚なんですが.
ちょっと退屈な作業をしているので,ついつい逃避してしまいました.心より恥じる.
考えが深まったら(撤回したくなったら?)また書きます...
Spring Framework 入門記 AOPその14 Metadata-Driven Autoproxying
前回に引き続きautoproxyです.今回は「5.9.2. Using metadata-driven autoproxying」に進みます.
Metadataですよ,メタデータ.C#の,あれ.J2SE1.5(Tiger)で導入される,あれ.あれを先取りしてAOPです.カックイー!
ということで,Core Concernのソースにメタデータを記述してWeavingするAspectを指定するっていうのが今回の学習内容です.個人的には,Crosscutting Concernの適用をCore Concernのソースで指定するのには抵抗を感じないではありません.よく,@remote
みたいなメタデータの例を見かけますが,こんなのをCore Concernで指定するのはちょっと違うと思うわけです.でも,それはたぶん@remote
なんていうのでは抽象度が低すぎるからで,もっと抽象度の高い表現ならいいのかもしれません.具体的にどんなのって言われても困るわけですが,例えば@service
とかって感じ? で,そのサービスメソッドをリモートにするかしないかは,後からどうにでもすればいいわけで.
IoCにAOPだけでもどう設計すればいいかが追いついていない感じの現状ですが,メタデータもまた同じような課題を増やしてくれそうですね.
能書きはこれくらいにして,Springでメタデータをどう扱うのか見ていきましょう.
Springでは今のところ, Apache Jakarta Commons の Attributes を使ってメタデータを扱うようです.ただし,直接 Commons Attributes のAPIを呼び出さなくてもいいように,抽象レイヤが用意されているみたい.
ということで,まずは Commons Attributes に寄り道します.不覚にも触ったことがないもので.ドキドキです.
Commons Attributes では,メタデータの記述は
/** * @@MyAttribute ("constructor arg", property="value") */ public class Foo { ・・・
みたいに記述するようです.'@'
が二つなんですねぇ.
このようにメタデータを記述したソースを,Attribute Compiler というものにかけると,上の例だとFoo$__attributeRepository
というクラスのソースファイルが生成されます.'$'
なんてついていますが,別に内部クラスではありません.そういう名前の普通のクラスです.この生成されたクラスは,元のクラス(Foo
)の中に記述されたメタデータに関する情報を返してくれるようなクラスで,次のようなメソッドを持っています.
Set getClassAttributes()
Map getFieldAttributes()
Map getConstructorAttributes()
Map getMethodAttributes()
なるほどなるほど.それで,こいつらが返してくれるSet
やMap
の要素はどんなものかというと,上の例の場合はMyAttribute
というクラスのインスタンスになります.そういう風に書いたので.MyAttribute
クラスには,上の書き方だと1つのString
を引数として受け取るコンストラクタと,property
という名前でString
型のプロパティのsetterつまりsetProperty(String)
が必要です.これはあくまで上のように書いた場合の話で,実際には引数の数や型は自由自在みたいです.というのも,メタデータでの記述がほとんどそのまま生成されるソースファイルにコピペされるだけなのです.ナイス.なのか?
とまぁ,そんなわけでメタデータのリポジトリとなるクラスのソースが生成されますから,それをコンパイルして使えばいいみたい.使い方は,普通にインスタンスをnew
して前述のメソッドを呼ぶだけ.簡単そうですね.これでひとまず Commons Attributes を終了,Springに戻ります.
ということで,Springに戻ってきました.Springでは,メタデータを扱うためにAttributes
というinterface
を用意しています.こいつは,次のメソッドを持っています.
Collection getAttributes(Class)
Collection getAttributes(Field)
Collection getAttributes(Field, Class)
Collection getAttributes(Method)
Collection getAttributes(Method, Class)
なんとなくイメージは分かりますね.コレクションの要素が何かというのは明記されていない感じなのですが,普通に考えれば先ほどのMyAttribute
みたいなのに決まってます.間違いない.二つ目の引数でClass
を取るものは,その型のメタデータ情報だけを取得するためのものです.
Attributes
はinterface
なのですが,それをimplements
したclass
としてCommonsAttributes
が用意されています.もちろん,Commons Attributes を使った実装です.こいつは普通にBeanとしてインスタンス化して,メソッドを呼び出せばそれでいいみたい.
それで,このAttributes
をどこで使うかというと,もうおなじみの Advisor(Pointcut) で使うようです.具体的には,ClassFilter#matches(Class)
とかMethodMatcher#matches(Method, Class)
の中でAttributes
を呼び出してあげればよさげです.
さぁ,これで材料が揃いました.メタデータは定義できるし,それを取得するべき場所もその方法もわかりました.後は実践あるのみ!
で,恥ずかしながらまたしてもDebugInterceptor
を使います.しょうがないんですよ,日記としては標準出力に跡を残してくれるAspectを使うのが一番なんですから.心より恥じる.
とういわけで,メタデータは @@DebugAttribute
に決定です.となると,必然的にDebugAttribute
クラスを用意しなくてはなりません.でも,トランザクションのAspectみたいにREQUIRED
とか指定する必要はないので,超単純です.
package study; public class DebugAttribute { }
日記に載せるほどでもないですね.いいんです,これも大切な思い出なんです.
で,次にAspectを適用するクラス.今回はメタデータ付.
package study; public class Foo { /** * @@DebugAttribute() */ public void one() { System.out.println("one"); } public void two() { System.out.println("two"); } /** * @@DebugAttribute() */ public void three() { System.out.println("three"); } }
3つの無駄なメソッドのうち二つにはメタデータでデバッグ対象であることを記述しています.
これをAttribute Compilerで処理します.Antのタスクが提供されているので,次のようなbuild.xml
を用意すればOK.
<?xml version="1.0"?> <project name="attribute-compiler" basedir="." default="build"> <target name="build"> <property name="spring.root" value="C:\Program Files\spring\spring-framework-1.0"/> <path id="attribute-compiler-classpath"> <fileset dir="${spring.root}/lib/jakarta-commons"> <include name="commons-attributes-compiler-SNAPSHOT.jar"/> <include name="commons-collections.jar"/> </fileset> <fileset dir="${spring.root}/lib/xdoclet"> <include name="xjavadoc-1.0.jar"/> </fileset> </path> <taskdef resource="org/apache/commons/attributes/anttasks.properties"> <classpath refid="attribute-compiler-classpath"/> </taskdef> <attribute-compiler destdir="${basedir}/generated"> <fileset dir="${basedir}/sources" includes="**/*.java"/> </attribute-compiler> </target> </project>
これをAntで実行すると,study.Foo$__attributeRepository
が生成されます.
次にStaticPointcutなAdvisorを作成します.
package study; import java.lang.reflect.Method; import org.springframework.aop.support.StaticMethodMatcherPointcutAdvisor; import org.springframework.metadata.Attributes; public class DebugAttributesAdvisor extends StaticMethodMatcherPointcutAdvisor { private Attributes attributes; public boolean matches(Method method, Class targetClass) { return !attributes.getAttributes(method, DebugAttribute.class).isEmpty(); } public Attributes getAttributes() { return attributes; } public void setAttributes(Attributes attributes) { this.attributes = attributes; } }
このクラスは,プロパティとしてAttributes
を持っており,matches(Method, Class)
ではそれを使って該当のメソッドにDebugAttribute
が存在するかチェックしています.Attributes#getAttributes()
はメタデータがない場合は空のコレクションを返します.null
が返ってこないっていいっすねー.
さ,次は定義ファイルです.
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd"> <beans> <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"/> <bean class="org.springframework.metadata.commons.CommonsAttributes"/> <bean class="org.springframework.aop.interceptor.DebugInterceptor"/> <bean class="study.DebugAttributesAdvisor" autowire="byType"/> <bean id="foo" class="study.Foo"/> </beans>
えへへ,今回はautowire
を指定して,シンプルな定義ファイルに挑戦してみました.
さて,あとは実行用のクラスです.前回同様,コンテナからfoo
を取り出して,3つのメソッドを呼ぶだけです.
それを実行!!!
Debug interceptor: count=1 invocation=[Invocation: method=[public void study.Foo.one()] args=[Ljava.lang.Object;@289d2e] target is of class study.Foo] one Debug interceptor: next returned two Debug interceptor: count=2 invocation=[Invocation: method=[public void study.Foo.three()] args=[Ljava.lang.Object;@289d2e] target is of class study.Foo] three Debug interceptor: next returned
ばっちグー.
出だしで@remote
は抽象度が低いとか偉そうに書いておきながら,サンプルで使ったのが@@DebugAttribute()
とは... 心より恥じる.