Spring Frameworkを使ってREST APIを作りつつ、その返却値としてJPAのEntityを使いまわそうとしたところエラーに悩まされ。その解決策について、日本語で書かれている情報がなかったので、ここに共有します。
問題が発生する条件
- JPAのEntityを返却値として用いている
- Entityの中でリレーションを定義している
が条件となります。
どのような問題が発生するか
JPAでリレーションを設定する際、親から子へのリレーションだけでなく、子から親へのリレーションも定義する必要があります。
具体的にいうと
/** 親テーブル */ @Entity @Table(name = "parent") public class Parent implements Serializable { @Id String id; String name; @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true) List<Child> children; // 子の定義 // 略 @Override public String toString() { return "Parent{" + "id='" + id + '\'' + ", name='" + name + '\'' + ", children=" + children + '}'; } }
/** 子テーブル */ @Entity @Table(name = "child") @IdClass(ChildPk.class) public class Child implements Serializable { private static final long serialVersionUID = 1L; @Id @ManyToOne Parent parent; // 親の定義 @Id String childId; String name; // 略 @Override public String toString() { return "Child{" + "parent=" + parent.getId() + ", childId='" + childId + '\'' + ", name='" + name + '\'' + '}'; } }
といった形になります。
つまり、オブジェクトの形としては、親は子を持ち、子を親を持っているわけで、循環参照の形態をとります。
これを
@RequestMapping(value = "") public Parent create() { Parent result = service.getData(); System.out.println(result); return result; }
としてParentを戻りとしてcontrollerを呼び出すと、consoleでは
Parent{id='1', name='one', children=[Child{parent=1, childId='a', name='1-a'}]}
と表示されますが、ブラウザ上では先ほどの循環参照の問題にあたり
としてグロ画像的な表示になり
2016-03-20 00:27:34.369 ERROR 18687 --- [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.http.converter.HttpMessageNotWritableException: Could not write content: Infinite recursion (StackOverflowError) (through reference chain: com.example.entity.Parent["children"]->org.hibernate.collection.internal.PersistentBag[0]->com.example.entity.Child["parent"]->com.example.entity.Parent["children"]->org.hibernate.collection.internal.PersistentBag[0]->com.example.entity.Child["parent"]->com.example.entity.Parent["children"]->org.hibernate.collection.internal.PersistentBag[0]->com.example.entity.Child["parent"]->〜略〜com.example.entity.Parent["children"])] with root cause java.lang.StackOverflowError: null at java.lang.ClassLoader.defineClass1(Native Method) ~[na:1.8.0_65] 〜略〜
とStackOverflowErrorを起こしてしまいます。
解決方法
に解決方法がありました。
@JsonIdentityReferenceを指定してあげるだけです。JSONに変換する際に、持っているオブジェクトを展開するのではなく、そのオブジェクトのIDを使うようにしてあげる感じですね。
先ほどの例だとParent parent;
に対して
@Id @ManyToOne @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id") // このアノテーションを追加 @JsonIdentityReference(alwaysAsId = true) // このアノテーションを追加 Parent parent;
としてあげることで
とJSONが出来上がりました。説明するまでもないかもしれませんが、@JsonIdentityInfo
にて何を当該オブジェクトの識別子として利用するか明示し、@JsonIdentityReference
にて、参照先展開の代わりに識別子を利用しろと命じている感じです。
なお、@JsonIgnoreProperties
というアノテーションをクラスに指定し
@Entity @Table(name = "child") @IdClass(ChildPk.class) @JsonIgnoreProperties({"parent"}) // これを有効化することで、parent自体をjsonに含めない指定も可能 public class Child implements Serializable { 〜略〜 }
として本例でいうところのchildが持つparentをJSONに出力しないことも可能です。
ソースコード
ということで、日本語でJsonIdentityReferenceとグーグル検索しても出てこなかったので記事にしてみました。
同じ問題に長時間はまる人が1人でも減ることを祈ってます(RESTなアプリケーション作るのに、Javaって選択肢がそもそもあんまり無いのかなぁとも思ったりしますが)