今日学んだこと

読書感想文とか、勉強した内容とか

SpringにおいてJPAのEntityをRESTのJSON返却値としてそのまま用いる際の注意点

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'}]}

と表示されますが、ブラウザ上では先ほどの循環参照の問題にあたり

f:id:nakazye:20160320003125p:plain

としてグロ画像的な表示になり

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を起こしてしまいます。

解決方法

stackoverflow.com

に解決方法がありました。

@JsonIdentityReferenceを指定してあげるだけです。JSONに変換する際に、持っているオブジェクトを展開するのではなく、そのオブジェクトのIDを使うようにしてあげる感じですね。

先ほどの例だとParent parent;に対して

    @Id
    @ManyToOne
    @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id") // このアノテーションを追加
    @JsonIdentityReference(alwaysAsId = true) // このアノテーションを追加
    Parent parent;

としてあげることで

f:id:nakazye:20160320004332p:plain

JSONが出来上がりました。説明するまでもないかもしれませんが、@JsonIdentityInfoにて何を当該オブジェクトの識別子として利用するか明示し、@JsonIdentityReferenceにて、参照先展開の代わりに識別子を利用しろと命じている感じです。

なお、@JsonIgnorePropertiesというアノテーションをクラスに指定し

@Entity
@Table(name = "child")
@IdClass(ChildPk.class)
 @JsonIgnoreProperties({"parent"}) // これを有効化することで、parent自体をjsonに含めない指定も可能
public class Child  implements Serializable {
〜略〜
}

として本例でいうところのchildが持つparentをJSONに出力しないことも可能です。

ソースコード

github.com

ということで、日本語でJsonIdentityReferenceとグーグル検索しても出てこなかったので記事にしてみました。

同じ問題に長時間はまる人が1人でも減ることを祈ってます(RESTなアプリケーション作るのに、Javaって選択肢がそもそもあんまり無いのかなぁとも思ったりしますが)