An Easier Java ORM(5) Active Recordパターンを実装する

http://www.javalobby.org/articles/activeobjects/の日本語訳です。<< An Easier Java ORM(4) 複雑な問い合わせ - 目次に戻る - An Easier Java ORM(6) 動く! >>

Active Recordパターンを実装する - Implementing the Active Record Pattern

Martin FowlerはActive Recordパターンを以下のように定義しています。

Martin Fowler defines the Active Record pattern as:

データベースのテーブルやビューの行をラップし、データベースアクセスをカプセル化し、そのデータにドメインロジックを追加したオブジェクトです。

An object that wraps a row in a database table or view, encapsulates the database access, and adds domain logic on that data.

このパターンを最初に定義したのはFowlerではありませんが、彼の定義が最も熟考されています。事実、この定義は、Ruby on RailsActiveRecordフレームワークの作成につながりました。Active Recordは、ORMの作成に役立つとても堅実なデザインパターンだと考えられています。問題もありますが、多くの場面ではとても役立ちます。われわれの目的は、90%の場面で役立つ単純なORMを作ることなので、Active Recordは真似るべきよいデザインパターンだと思います。しかし今のところ、われわれの正式でない仕様では、この定義の一部のデータベースアクセスのカプセル化しか扱っていません。エンティティへのドメインロジックの追加する方法が必要です。

Even though Fowler wasn't the first to posit this pattern, his definition is considered to be among the best. In fact, it was this definition which directly led to the creation of the ActiveRecord framework within Ruby on Rails. Active Record is considered to be a fairly solid design pattern to use in ORM construction. Granted, it does have issues, but for most scenarios it's pretty useful. Since our goal is to create a useful and simple ORM for 90% of use-cases, Active Record is probably a good design pattern to follow. But at the moment, our informal spec only handles the first part of the definition: database access encapsulation. We need to have some way of adding domain logic to our entities.

実際のところ、一見してわかるよりも大きな問題があります。われわれのORMでは、エンティティをインタフェースで表現しています。したがって、エンティティにメソッド定義を含めることはできません。したがって、ドメインロジックを含めることができません。クラスの考えに戻ったとしても、Javaの動的プロキシは、インタフェースだけをサポートしており、クラスはサポートしません。もちろん、CGLIBのようなハックもありますが、バイトコードの変更というのはせいぜい怪しげな研究であると以前から考えています。すると、われわれのエンティティをクラスに変えることもできませんし、われわれのインタフェースエンティティにドメインロジックを追加することもできません。つまり、お手上げです。

This actually poses a bigger problem than it would seem at first glance. In our ORM, entities are represented by interfaces. Thus, they cannot contain any method definitions; hence no domain logic. We could go back to the idea of classes, but Java doesn't support dynamic proxies for classes, only interfaces. Of course, there are always hacks like CGLIB, but I've always considered bytecode modification to be a dubious pursuit at best. So, we can't change our entities to classes, and we can't put domain logic in our interface-entities. In short, we're stuck.

ドメインロジックをエンティティから別のクラスに分けることで解決します。モデルのロジックをデータベースアクセスから分けることに納得するOOD純粋主義者は多いでしょう。残念ながら、これはActive Record純粋主義者には異端と映るでしょう。ここまでにやってきた「コードによる設計」を再開して、会社名からティッカーシンボルを自動的に生成するためのドメインロジックをCompanyエンティティに追加できます:

The solution is to factor the domain logic out into a separate class from the entity. This probably makes sense already to most OOD purists since it separates the model logic from the database access itself. Unfortunately, to the Active Record purists, this is probably going to look like heresy. To continue with our tradition of "designing in code", we can add domain logic to the Company entity to auto-generate the ticker symbol from the company name:

@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());  
        }  
    }  
} 

ご覧のように、われわれのエンティティの全てのドメインロジックを含む「実装クラス」を表すために、アノテーションが使えます。データの設定等のために元のエンティティを呼び出す方法がこのクラスには必要です。そこで、実装クラスのコンストラクタで対応するインスタンスを受け取り、実装の状態の一部として保存します。実装クラスは、何の機能の継承もしないので、特定の上位クラスを継承する必要はありません。

As you see, we can use an annotation to specify an "implementation class" which can contain all of the domain logic for our entity. In that class, we will of course need some way of calling back to the original entity to set data and so on. Thus, in the implementation class constructor, we'll accept the corresponding instance and save it as part of the implementor state. We don't really need the implementation classes to extend any particular superclass, since they aren't actually inheriting any functionality.

一旦、実装クラスを定義すれば、一致するメソッドシグネチャをリフレクションで問い合わせることで、メソッド呼び出しを自由にメソッド実装に割り当てられます。

Once we have the defined implementation instance, we can reflectively interrogate it for matching method signatures and redirect method calls to the corresponding method implementations at will. If there is no matching signature, we'll just execute the database operation as per normal and move on.

Companyの例では、setName(String)メソッドにだけ実装メソッドがあります。名前が変更された時だけ、ティッカーシンボルを変更する必要があるためです。上記の実装は、名前の最初の4文字を大文字に変換してティッカーシンボルに設定しています。

In the case of Company, the only method with any implementation is the setName(String) method. This is because we only need to set the ticker symbol when the name changes. In the implementation above, we just set the ticker symbol to the first four characters of the name, converted to upper-case.

鋭い方は上記の例での差し迫った問題に気づくでしょう。再帰しているのです。メソッドシグネチャの一致によって、エンティティのメソッド呼び出しから定義された実装へリダイレクトを行うと、上記の定義での制御の流れは以下のようになります。

Astute observers will see an immediate problem with the provided example: it's recursive. If we're redirecting method calls on the entity with matching method signatures to the defined implementation, then our control flow as defined above will look something like this:

  1. 利用者のコードがCompany#setName(String)を呼び出す。
  2. Invocation handlerは、setName(String)のメソッドシグネチャがCompanyImplと一致することに気が付き、データベースに対応する実装は呼び出さずに、CompanyImplのメソッド呼び出しを行いま
  3. 定義された実装は、company.setName(name)を呼び出します。
  4. Invocation handlerは、setName(String)のメソッドシグネチャがCompanyImplと一致することに気が付き、データベースに対応する実装は呼び出さずに、CompanyImplのメソッド呼び出しを行います。
  5. ...
  1. Consumer code calls Company#setName(String)
  2. Invocation handler sees setName(String) method signature within CompanyImpl and passes it the method call, skipping the database-peered implementation
  3. Defined implementation calls company.setName(name)
  4. Invocation handler sees setName(String) method signature within CompanyImpl and passes it the method call, skipping the database-peered implementation
  5. ...

これはささいな問題です。

Minor issues we hath.

定義された実装からのすべてのメソッド呼び出しを実装自身にフィードバックしないようにすれば、この問題を明らかに回避できます。これにより、無限再帰は起こらなくなり、定義された実装からエンティティへ安全にアクセスできます。実際にメソッドの呼び出し元を検出するには、仕掛けが必要です。

The obvious way to avoid this issue is to make sure that any method invocations coming from the defined implementation are never fed back to the implementation itself. This breaks the infinite recursion and still allows the defined implementations to actually access the entity safely. The trick is in actually detecting the source of the method call.

幸運にもJavaには、分かりにくくあまり知られていませんが、開発者が呼び出しスタックに動的にアクセスするための仕組みを提供しています。このようなアクセスの仕組みは二つありますが、一方はアプリケーションをデバッグモードで起動する必要があるため、フレームワークのユーザに求めることができません。好ましい方の仕組みでは、Javaの例外を作る仕組みを利用します。

Fortunately, Java does provide a mechanism - albeit obfuscated and little-known - which allows developers to get access to the call stack dynamically. Actually, it defines two mechanisms for such access, but one would require starting the application in debug mode, which is something we can't mandate of a framework's users. The preferred mechanism is to take advantage of the way Java constructs exceptions.

一般的なRuntimeExceptionを作成すると、Javaは現在のスタックトレースのメタ情報を実際にコピーし、例外中に貼り付けます。これは、printStackTrace()の表示のためです。Javaは、例外中のこのスタックトレースに動的に(文字列を解析することなく)アクセスするために、API(Throwable#getStackTrace():StackTraceElement[])を提供しています。これを使って、スタックの一段上が定義された実装かどうかを確認します。定義された実装がメソッド呼び出しをしていれば、定義された実装の再呼び出しはせずに、普通のメソッド呼び出しを実行します。したがって、定義された実装からエンティティへの呼び出しでは、実装のロジックは呼び出されないため、再帰が避けられます。

In creating a generic RuntimeException, Java actually copies out the meta for the current stack trace and sticks it in the exception. This is so that printStackTrace() has something to print. Java even provides an API (Throwable#getStackTrace():StackTraceElement[]) to access this stack trace within the exception dynamically (and without string parsing). We can use this to check for the defined implementation one step up on the stack. If we find that it initiated the method call, we'll skip the re-invocation of the defined implementation and actually execute the method call normally. Thus, any calls to an entity from its defined implementation will skip any implementation logic, avoiding recursion.

<< An Easier Java ORM(4) 複雑な問い合わせ - 目次に戻る - An Easier Java ORM(6) 動く! >>