Home日記コラム書評HintsLinks自己紹介
 

フィンローダのあっぱれご意見番 第166回「はてしない物語」

← 一つ前 | 「フィンローダのあっぱれご意見番」一覧 | 次のをみる →

Hibernate というO/Rマッピングツールがある。 これについて議論をはじめると世界中から賛否両論が殺到し、 支持する人と支持しない人がハイレベルなバトルに突入するという恐ろしいツールだ。 ただ、日本ではいまいち人気があるのかないのかよく分からない。

 

※ タイトルの「はてしない物語」はエンデ作、 The Never Ending Story のこと。 この回でC MAGAZINE が休刊になった。

O/Rマッピングツールとは何なのか簡単に説明すると、 Object と RDB を対応させるものである。 キーワードは「永続化」。

  

という説明だと流石に簡単すぎるのでもう少し説明しておくと、 Hibernate の場合は、マッピングファイルなるものを使って、 Java のオブジェクトのどこがRDBの何に対応する、ということを指定する。 Hibernate の API を使うことで、 少なくともJava のコードの上では、 テーブルがどうとか SQL がどうとか意識しなくても、 自動的にオブジェクトをデータベースに保存してくれる。

 

※ 「少なくとも」と書いたが、 結局、マッピングテーブルを作る時にはテーブルがどうだという話になってしまう。 ちなみに、同じ型(クラス)のデータを別テーブルにより分けて入れたい場合は、 Entity という概念が出てくる。

と書いたら「素晴らしい」と単純に考えてしまいそうだが、 この種のツールを使っている方ならご存知の通り、 まず、マッピングファイルを用意しろというのが曲者だ。 Java のクラスの設計を変更すると、マッピングファイルもそれにあわせて変更しなければならないのだが、 これが面倒なうえに、かなりの確率で間違いが混入する。

  

そこで、対応付けを機械的にやろうという発想が出てくる。 マッピングファイルから Java のソースを自動生成するのは簡単だ。 Hibernate の場合はhbm2java を使って実現できる。

 

※ Hibernate tools のページを参照。

ソースの自動生成というと、定番の問題がある。 自動生成したクラスは、なぜか修正したくなるのだ。 単純なメソッドを追加しても、O/R マッピングの基本的な処理とは関係ない。 つまり、永続化の処理はそのまま動作する。 その時点では問題ないのだが、マッピングファイルを変更したくなると困る。 マッピングファイルにあわせて、Java のソースも修正する必要がある。 もちろん、Javaのソースを自動的に再生成すればいいのだが、そうすると、 後で手作業で加えた処理が消滅してしまうのである。

こんな場合は Generation Gap Pattern を使う、 というのが定石だ。 実際、hbm2java にも、そのための機能がある。 これがちょっと使い辛いような気がしたのと、それに、Gereration Gap Pattern を使っても、 実際にデータ構造が変更になったらたいていサブクラス側も変更することになるし、 永続化したいクラスがたくさんあったら、 Generation Gap Pattern を使うとクラスの数が2倍になって、 なんとなく直し忘れのようなミスも増えるような気がする。

ということで、前回生成した Java のソースを保存しておき、 再生成の結果がそれと同じだったら、手を加えたソースはそのまま使う、 というような処理を作ってみた。 これはそれほど珍しいアイデアではない。 ちなみに、自動生成したコードの後にマークを入れておいて判別できるようにする、 というような定番の手法もあったりする。

  

ところが、これが実は失敗した。 原因は単純だ。hbm2java の生成するソースが毎回違うのである。 ソースファイルのコメントの中に生成時刻が書き込まれるので、 単純に比較するとその行が必ず不一致になってしまうのだ。 Web ページの最終更新時刻が更新されていたので何が変わったか比較したら、 最新更新時刻のところだけ変わっていた、みたいな。

 

※ 結局、Hibernate が生成した時刻の行だけを無視して比較するプログラムを作って使っている。

§

Java 1.5 から Generics という概念が使えるようになって、 Collection に入れる要素のクラスを指定できるようになった。 これがプログラムの保守性、可読性を高めるという議論には全く異議はないが、 もう一つの私見として、 Collection の真骨頂は Object を格納できるという所にあるのだという考え方も捨てがたいのである。

  

例えば、List に格納する要素は、 べつに全て同じクラスに属していなくても構わないのだ。 String を入れた次に Integer を入れてもいいし、 さらに TimeStamp を追加してもいい。 自分で作ったクラスでも構わないし、何と List を入れることだってできる。 これは、List だけでかなり複雑なデータ構造に対応できることを意味している。

 

※ C言語の void へのポインタの配列みたいなものである。

複雑なデータ構造ということで、List に対して、親子関係を指定できるようにしよう。 頭が Hibernate 化していれば、各オブジェクトに ID を割り振って、 それを使って親子を表現する方法を思いつくだろう。 List 1 のような感じになる。

---- List 1 (親子関係を持つ List) ----

class ListTree {
    private int id;
    private int parentId;
    private List list;

    // getter, setter は省略
}

---- List 1 end ----
  

整数値の ID を割り当てるという発想は、RDB に洗脳されていれば簡単に出てくる。 単純にオブジェクトの親子関係を表現したいのなら、 List 2 のようなクラスを考えた方が自然かもしれない。

 

※ identifier を使うためには、それが unique、 つまり重複がないことを保証しなければならない。 これは結構面倒な話なのだ。

---- List 2 (ID でなく List で直接示す) ----

class ListTree {
    private List parent;
    private List list;

    // getter, setter は省略
}

---- List 2 end ----

これをこのまま Hibernate に処理させたら何が出てくるか、 というのも面白いネタなのだが、今回は捨て置くことにして、 もう少し先を考えてみる。 parentというのは何だ。 List の中には何でも入るのだから、例えば先頭要素は必ずparent を入れることにする、 というようなルールを決めておけば、目的のクラスは List 3 のようになる。

---- List 3 (究極の ListTree?) ----

class ListTree {
    private List list; // 先頭要素は親の ListTree であるものとする

    // getter, setter は省略
}

---- List 3 end ----

もちろん、これは馬鹿げた話である。 メンバー変数が List ひとつだけというクラスをわざわざ作る位なら、 最初から List をそのまま使えばよいのだ。 独自のメソッドを追加したり、 ある種のメソッドを override して拡張したいというなら話は別だが、 まさにそれは別の話なのである。

ということで、何か目的があって、仮に List 1 のような感じのクラスを作ったとしておこう。 ここで問題。 このクラスに public String toString(); を実装してほしい。

§

toString は Java の最も基本的なメソッドの一つだ。 Object クラスに実装されているから、 特に何もしなくてもあらゆるクラスで使うことができる。 Listは interface だが、 もちろんその実装である ArrayList などのクラスからも toStringが使える。

さて、Apache jakarta の commons.lang プロジェクトに、 org.apache.commons.lang.builder というパッケージがある。 この中の、ReflectionToStringBuilderというクラスを使うと、 toString はList 4 のように実装することができる。

---- List 4 (ReflectionToStringBuilder を使った実装) ----

    public String toString() {
        return ReflectionToStringBuilder.toString(this);
    }

---- List 4 end ----

このクラスは、わざわざ toString を override するために用意されている。 toString はこんなことをしなくても使えるのに、 なぜ override するかというと、 出力形式を設定したい場合に便利だからだそうです。

もちろん、さっき出した問題の解答がこれでも構わない。 車輪の再生産は避けるというのが基本だが、 今回は演習問題だと思って自力で書いてみてほしい。 こういうちょっとしたプログラムでも、実力は明白に現れるものである。

Eclipse を使えば JUnit Test Case の雛形は自動生成できるから、 核心な部分だけ紹介しておくと、 例えば、List 5 のようなテストケースを実行したらどうなりますか。

---- List 5 (JUnit test case) ----

    public final void testAll() {
        ListTree listTree = new ListTree();
        listTree.setList(new ArrayList());
        listTree.getList().add(listTree); // 自分自身を要素に持つ

        String string = listTree.toString();
        assertNotNull(string);
        System.out.println(string);
    }

---- List ----

List の要素に List を含むことができるというヒントは先に述べている。 こんな所でコケるようでは困るのだが、はて、自作のクラスではなく、 例えば ArrayList の toString をそのまま使ったらどうなるか? 例えば List 6 だとどうだ?

---- List 6 (自分自身を要素に持つ List の toString) ----

    List list = new ArrayList();
    list.add(list);
    System.out.println(list.toString());

---- List 6 end ----

これを実際に動かしてみると、画面には「[(this collection)]」と表示された。 つまり、ちゃんと回避しているのである。では、List 7 はどうか?

---- List 7 (少し複雑な再帰を持つリスト) ----

    List list1 = new ArrayList();
    List list2 = new ArrayList();
    list1.add(list2);
    list2.add(list1);
    System.out.println(list.toString());

---- List 7 end ----
  

こんどは java.lang.StackOverFlowError になってしまった。

 

※ 素直に子要素を見に行って無限地獄に陥ったのだ。 リファレンスマニュアルの List を調べると、 「注: リストにリスト自体を要素として格納することも可能ですが、十分注意してください。 そのようなリストでは equals メソッドおよび hashCode メソッドの動作は保証されません。」 と書いてある。これはそのような仕様なのだ。

§

  

この種の巡回問題は、サイト内をクロールするプログラムを作るときに必ず出てくる。 ページ内のリンクを順にたどれば、非常に高い確率でトップページか、 それに近いページに戻ってしまうのだ。 これを何とかしないと、いつまでたっても巡回が終わらない。 考えてみれば、List 7 は StackOverFlowError が出たのだから、 まだいい。 つまり「終わる」のである。本当に怖いのは、無限ループが終わらない場合なのだ。

 

※ 指定したサイト内のページをリンクをたどって全て get したい、という話。

toString の話に戻ろう。 この問題を解決するにはどうすればいいか。 と思ったらもう書く場所がないので、解決編は次回に続く、と思ったら次がないというから困ったものだが、 しかし、今は Web という、リアルだかバーチャルだか分からない世界が日常化した時代。 私が個人ページを公開してから10年経ったが、まだ何とか続いているから、 さらにしばらく続くだろう。 検索ページで「フィンローダ」と指定すれば簡単に見つかるから、 URL は特に紹介しないが、また何かの機会でどこかでお会いしたときに、 いろいろツッコミでも入れていただければありがたいと思いつつ、 今回はこれまでということで。最後に一言。 バグの数だけネタがある。

 

※ 最初に公開したのはいわゆる表ページで、 1996年頃だった。

(C MAGAZINE 2006年4月号掲載)
内容は雑誌に掲載されたものと異なることがあります。

修正情報:
2006-04-12 裏ページに転載。

(C) Phinloda 2006 All rights reserved.