双方向の関連を持つJPAオブジェクトをJSONに変換しようとすると、
org.codehaus.jackson.map.JsonMappingException: Infinite recursion (StackOverflowError)
私が見つけたのはこのスレッド基本的には、双方向の関連付けを避けることを推奨することになります。この Spring のバグを回避するアイデアをお持ちの方はいらっしゃいますか?
------ 編集 2010-07-24 16:26:22 -------
コードスニペット:
ビジネスオブジェクト1:
@Entity
@Table(name = "ta_trainee", uniqueConstraints = {@UniqueConstraint(columnNames = {"id"})})
public class Trainee extends BusinessObject {
@Id
@GeneratedValue(strategy = GenerationType.TABLE)
@Column(name = "id", nullable = false)
private Integer id;
@Column(name = "name", nullable = true)
private String name;
@Column(name = "surname", nullable = true)
private String surname;
@OneToMany(mappedBy = "trainee", fetch = FetchType.EAGER, cascade = CascadeType.ALL)
@Column(nullable = true)
private Set<BodyStat> bodyStats;
@OneToMany(mappedBy = "trainee", fetch = FetchType.EAGER, cascade = CascadeType.ALL)
@Column(nullable = true)
private Set<Training> trainings;
@OneToMany(mappedBy = "trainee", fetch = FetchType.EAGER, cascade = CascadeType.ALL)
@Column(nullable = true)
private Set<ExerciseType> exerciseTypes;
public Trainee() {
super();
}
//... getters/setters ...
}
ビジネスオブジェクト2:
import javax.persistence.*;
import java.util.Date;
@Entity
@Table(name = "ta_bodystat", uniqueConstraints = {@UniqueConstraint(columnNames = {"id"})})
public class BodyStat extends BusinessObject {
@Id
@GeneratedValue(strategy = GenerationType.TABLE)
@Column(name = "id", nullable = false)
private Integer id;
@Column(name = "height", nullable = true)
private Float height;
@Column(name = "measuretime", nullable = false)
@Temporal(TemporalType.TIMESTAMP)
private Date measureTime;
@ManyToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
@JoinColumn(name="trainee_fk")
private Trainee trainee;
}
コントローラ:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletResponse;
import javax.validation.ConstraintViolation;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
@Controller
@RequestMapping(value = "/trainees")
public class TraineesController {
final Logger logger = LoggerFactory.getLogger(TraineesController.class);
private Map<Long, Trainee> trainees = new ConcurrentHashMap<Long, Trainee>();
@Autowired
private ITraineeDAO traineeDAO;
/**
* Return json repres. of all trainees
*/
@RequestMapping(value = "/getAllTrainees", method = RequestMethod.GET)
@ResponseBody
public Collection getAllTrainees() {
Collection allTrainees = this.traineeDAO.getAll();
this.logger.debug("A total of " + allTrainees.size() + " trainees was read from db");
return allTrainees;
}
}
研修生DAOのJPA実装:
@Repository
@Transactional
public class TraineeDAO implements ITraineeDAO {
@PersistenceContext
private EntityManager em;
@Transactional
public Trainee save(Trainee trainee) {
em.persist(trainee);
return trainee;
}
@Transactional(readOnly = true)
public Collection getAll() {
return (Collection) em.createQuery("SELECT t FROM Trainee t").getResultList();
}
}
永続性.xml
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd"
version="1.0">
<persistence-unit name="RDBMS" transaction-type="RESOURCE_LOCAL">
<exclude-unlisted-classes>false</exclude-unlisted-classes>
<properties>
<property name="hibernate.hbm2ddl.auto" value="validate"/>
<property name="hibernate.archive.autodetection" value="class"/>
<property name="dialect" value="org.hibernate.dialect.MySQL5InnoDBDialect"/>
<!-- <property name="dialect" value="org.hibernate.dialect.HSQLDialect"/> -->
</properties>
</persistence-unit>
</persistence>
ベストアンサー1
JsonIgnoreProperties [2017 更新]:
使用できるようになりましたJsonIgnorePropertiesプロパティのシリアル化を抑制したり(シリアル化中)、読み取られた JSON プロパティの処理を無視したり (デシリアル化中) します。これが探しているものではない場合は、以下を読み続けてください。
(この点を指摘してくださった As Zammel AlaaEddine に感謝します)。
JsonManagedReference と JsonBackReference
Jackson 1.6 以降では、シリアル化中にゲッター/セッターを無視せずに無限再帰の問題を解決するために、次の 2 つのアノテーションを使用できます。@JsonManagedReference
そして@JsonBackReference
。
説明
Jackson が適切に動作するには、stackoverflow エラーの原因となる無限ループを回避するために、関係の 2 つの側のうちの 1 つをシリアル化しないでください。
そこで、Jackson は参照の前方部分 ( Set<BodyStat> bodyStats
Trainee クラス内) を取得し、それを JSON のようなストレージ形式に変換します。これがいわゆるマーシャリングプロセスです。次に、Jackson は参照の後方部分 (つまり BodyStat クラス内) を探し、それをシリアル化せずにそのままにします。関係のこの部分は、前方参照の逆シリアル化 (アンマーシャリング)Trainee trainee
中に再構築されます。
次のようにコードを変更できます (不要な部分は省略します)。
ビジネスオブジェクト1:
@Entity
@Table(name = "ta_trainee", uniqueConstraints = {@UniqueConstraint(columnNames = {"id"})})
public class Trainee extends BusinessObject {
@OneToMany(mappedBy = "trainee", fetch = FetchType.EAGER, cascade = CascadeType.ALL)
@Column(nullable = true)
@JsonManagedReference
private Set<BodyStat> bodyStats;
ビジネスオブジェクト2:
@Entity
@Table(name = "ta_bodystat", uniqueConstraints = {@UniqueConstraint(columnNames = {"id"})})
public class BodyStat extends BusinessObject {
@ManyToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
@JoinColumn(name="trainee_fk")
@JsonBackReference
private Trainee trainee;
これですべて正常に動作するはずです。
もっと詳しく知りたい方は、私が書いた記事をご覧ください。Keenformatics の Json と Jackson Stackoverflow の問題、 私のブログ。
編集:
もう一つの便利な注釈は@JsonIdentityInfo: これを使用すると、Jackson がオブジェクトをシリアル化するたびに、ID (または選択した別の属性) が追加され、毎回完全に「スキャン」し直す必要がなくなります。これは、より相互に関連するオブジェクト間のチェーン ループがある場合に役立ちます (例: Order -> OrderLine -> User -> Order など)。
この場合、オブジェクトの属性を複数回読み取る必要がある可能性があるため(たとえば、同じ販売者を共有する複数の製品を含む製品リストなど)、このアノテーションによってそれが防止されるため、注意が必要です。常に Firebug ログを確認して Json 応答を確認し、コード内で何が起こっているかを確認することをお勧めします。
出典:
- Keenformatics - JSON の無限再帰を解決する方法 Stackoverflow(私のブログ)
- 個人的体験