ORM と infinite recursion の問題

これまで、hiberante などの ORM を使ったサンプルコードで双方向の関係を取り上げなかった理由は、いわゆる infinite recursion (無限循環)の問題にけっこう悩まされるから。

このサイトでは、対象を初学者〜中級者に想定しているが、おそらくそういった層に infinite recursion に関係するバグが発生しかねないサンプルを掲げたら、混乱は必至でしょう。

それで、意識的に避けてました。

無限循環は大抵の場合、<親-子-孫-・・・> の関係があるときに発生する。

つまり、親-子が1対多であるときだ。

この場合、例えば、hibernate であれば、親側の子を示す変数(データベースではカラム)に @OneToMany のアノテーションをつけ、子側に親の当該変数を指し示す変数に @ManyToOne のアノテーションを付与する。

教科書的にはこれで双方向の関係が定義できたことにはなる。

何も問題なさそうじゃないか?という声が聞こえてきそうだが、実際には困った挙動を示すことがある。

子のオブジェクトを生成し、これを JSON などで表示したいときに、当然、親側の情報も持っているから、ORM はこれを引っ張ってくる。
ところが、親は子の情報も持っているから、そこから子の情報を探そうとする。
探しだされた子は、さらに親の情報を取り出そうとして・・・

と、立派な無限循環が完成してしまう(笑)。

具体的にはこういうやつ↓です。


Infinite recursion (StackOverflowError)
(through reference chain: model.Parent["child"]
->org.hibernate.collection.spi.PersistentBag[0]
->model.Child["parent"]->model.Parent["child"]
->org.hibernate.collection.spi.PersistentBag[0]
->model.Child["parent"]->model.Parent["child"]
->org.hibernate.collection.spi.PersistentBag[0]
->model.Child["parent"]->model.Parent["child"]
->org.hibernate.collection.spi.PersistentBag[0]
->model.Child["parent"]->model.Parent["child"]
以下、無限に続く。。。(笑...えない)

使い慣れた ORM であれば、どこら辺が危険かある程度わかっているから、経験的に回避できなくもないが、仕様もうろ覚えのような ORM ではひとたびこの問題が発生したらハマること必至でしょう。

意識的に避けてきた理由はそこにある。

特に初学者に人は、まず習得したい ORM がどう動作しているか視覚的にわかるようなところから始めるといいと思う。

以前に Person – Phone の例を挙げたとき、person_phone などという一見邪魔くさいテーブルが生成されたが、わからなくなったら意識的にああいうテーブルをつくったらいいと思う。

遠いようでいて速いと思う。

 

参考

こことか。探せばいくらでも出てきそう。

コメント返し

コメントにこういうのがあった。
なんか、私の近くにいそうな人(ツィ友?)のような気もするが…。

>双方向はバグの温床となる気がするので、片方向だけでなんとかするというアプローチはありでしょうか?

ううむ。
やってやれなくはないので、そうしても構わないと思います。システム構築が大変だと思いますが。
ですが、無限循環が問題になるのはキケンな関係にあるクラス(のインスタンス)をダイレクトに参照しようとするときです。

実用的には、こういった直接参照を使わないということでもいいと思います。

具体的には、表示したいオブジェクトがあるときは、まず、その上流のオブジェクトにアクセスし、ゲッターで本当に操作したいオブジェクトを取得する、みたいなやり方です。

あるいは @JsonIgnore あたりのアノテーションを駆使するとか。

循環参照への対策

Resourceクラス(JavaBean)をJSONやXML形式にシリアライズする際に、相互参照関係のオブジェクトをプロパティに保持していると、循環参照となりStackOverflowErrorやOutOfMemoryErrorなどが発生するので、注意が必要である。

循環参照を回避するためには、

Jacksonを使用してJSON形式にシリアライズする場合は、シリアライズ対象から除外するプロパティに@com.fasterxml.jackson.annotation.JsonIgnoreアノテーション

JAXBを使用してXML形式にシリアライズする場合は、シリアライズ対象から除外するプロパティにjakarta.xml.bind.annotation.XmlTransientアノテーション

を付与すればよい。
https://terasolunaorg.github.io/guideline/current/ja/ArchitectureInDetail/WebServiceDetail/REST.html 

この手のアノテーションは便利だとは思います。
ですが、これらを多用すると可読性が落ちる気がしますし、仕様が変わってしまえば「動かない」コードになってしまいがちです。

システムならなんでもそうだと思いますが、基本設計をなるべくシンプルにして、どうしようもないところは(姑息的になるかもしれませんが)ワザを繰り出していくというのがワイの基本ポリシーです。

今回のネタに関して言えば、ここぞというところでは双方向も使っていいんじゃないでしょうか。

 

クリックclose

“ORM と infinite recursion の問題” への3件の返信

  1. 双方向はバグの温床となる気がするので、片方向だけでなんとかするというアプローチはありでしょうか?

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です