511 votes

Récursion infinie avec Jackson JSON et problème de Hibernate JPA

Lorsque j'essaie de convertir en JSON un objet JPA qui a une association bidirectionnelle, je continue à obtenir

org.codehaus.jackson.map.JsonMappingException: Infinite recursion (StackOverflowError)

Tout ce que j'ai trouvé est ce fil qui conclut essentiellement en recommandant d'éviter les associations bidirectionnelles. Quelqu'un a-t-il une idée de solution de contournement pour ce bogue de printemps ?

------ EDIT 2010-07-24 16:26:22 -------

Codesnippets :

Objet commercial 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 ...
}

Objet commercial 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;
}

Contrôleur :

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;
    }    
}

Implémentation JPA du DAO stagiaire :

@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();
    }
}

persistance.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>

0 votes

Ajouter @Transient a Trainee.bodyStats .

4 votes

A partir de 2017, @JsonIgnoreProperties est la solution la plus propre. Consultez La réponse de Zammel AlaaEddine pour plus de détails.

0 votes

En quoi est-ce la faute de ce printemps ?

745voto

Kurt Bourbaki Points 614

JsonIgnoreProperties [Mise à jour 2017] :

Vous pouvez maintenant utiliser JsonIgnoreProperties a supprimer la sérialisation des propriétés (pendant la sérialisation), ou ignorer le traitement des propriétés JSON lues (pendant la désérialisation) . Si ce n'est pas ce que vous recherchez, poursuivez votre lecture ci-dessous.

(Merci à As Zammel AlaaEddine de l'avoir signalé).


JsonManagedReference et JsonBackReference

Depuis Jackson 1.6, vous pouvez utiliser deux annotations pour résoudre le problème de récursion infinie sans ignorer les getters/setters pendant la sérialisation : @JsonManagedReference y @JsonBackReference .

Explication

Pour que Jackson fonctionne bien, l'un des deux côtés de la relation ne doit pas être sérialisé, afin d'éviter la boucle infidèle qui cause votre erreur de stackoverflow.

Ainsi, Jackson prend la partie avant de la référence (votre Set<BodyStat> bodyStats dans la classe Trainee), et le convertit dans un format de stockage de type json ; c'est ce qu'on appelle le "système d'information". marshalling processus. Ensuite, Jackson recherche la partie arrière de la référence (c'est-à-dire Trainee trainee dans la classe BodyStat) et le laisse tel quel, sans le sérialiser. Cette partie de la relation sera reconstruite lors de la désérialisation ( Démarchage ) de la référence avant.

Vous pouvez modifier votre code comme ceci (je saute les parties inutiles) :

Objet commercial 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;

Objet commercial 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;

Maintenant, tout devrait fonctionner correctement.

Si vous voulez plus d'informations, j'ai écrit un article sur le sujet. Json et Jackson : questions Stackoverflow sur Keenformatics mon blog.

EDIT :

Une autre annotation utile que vous pourriez vérifier est @JsonIdentityInfo : en l'utilisant, chaque fois que Jackson sérialise votre objet, il lui ajoute un ID (ou un autre attribut de votre choix), de sorte qu'il ne le "scanne" pas entièrement à chaque fois. Cela peut être utile lorsque vous avez une boucle en chaîne entre plusieurs objets interdépendants (par exemple : Commande -> Ligne de commande -> Utilisateur -> Commande et encore une fois).

Dans ce cas, vous devez faire attention, car vous pourriez avoir besoin de lire les attributs de votre objet plus d'une fois (par exemple dans une liste de produits avec plusieurs produits qui partagent le même vendeur), et cette annotation vous empêche de le faire. Je suggère de toujours jeter un coup d'oeil aux logs de firebug pour vérifier la réponse Json et voir ce qui se passe dans votre code.

Sources :

36 votes

Merci pour cette réponse claire. C'est une solution plus pratique que de mettre @JsonIgnore au dos de la référence.

3 votes

C'est définitivement la bonne façon de faire. Si vous procédez de la sorte côté serveur parce que vous y utilisez Jackson, le mappeur json que vous utilisez côté client n'a aucune importance et vous n'avez pas à définir manuellement le lien enfant-parent. Cela fonctionne tout simplement. Merci Kurt

1 votes

Belle explication détaillée et certainement une approche meilleure et plus descriptive que celle de la Commission européenne. @JsonIgnore .

350voto

axtavt Points 126632

Vous pouvez utiliser @JsonIgnore pour briser le cycle ( référence ).

Vous devez importer org.codehaus.jackson.annotate.JsonIgnore (anciennes versions) ou com.fasterxml.jackson.annotation.JsonIgnore (versions actuelles).

1 votes

J'avais le même problème et @JsonIgnore l'a résolu. J'avais annoté la méthode avec @XmlTransient qui aurait dû faire la même chose (et a fonctionné en utilisant Jettison). Vous pensiez pouvoir utiliser l'annotation jaxb avec Jackson, alors pourquoi cela ne fonctionne-t-il pas ?

1 votes

@Ben : En fait, je ne sais pas. Peut-être que son support n'a pas été activé : wiki.fasterxml.com/JacksonJAXBAnnotations

47 votes

Depuis Jackson 1.6, il existe une meilleure solution : vous pouvez utiliser deux nouvelles annotations pour résoudre le problème de récursion infinie sans ignorer les getters/setters pendant la sérialisation. Voir ma réponse ci-dessous pour plus de détails.

53voto

Marcus Points 167

En outre, avec Jackson 2.0+, vous pouvez utiliser @JsonIdentityInfo . Cela fonctionne beaucoup mieux pour mes classes hibernées que @JsonBackReference y @JsonManagedReference qui a posé des problèmes pour moi et n'a pas résolu le problème. Ajoutez simplement quelque chose comme :

@Entity
@Table(name = "ta_trainee", uniqueConstraints = {@UniqueConstraint(columnNames = {"id"})})
@JsonIdentityInfo(generator=ObjectIdGenerators.IntSequenceGenerator.class, property="@traineeId")
public class Trainee extends BusinessObject {

@Entity
@Table(name = "ta_bodystat", uniqueConstraints = {@UniqueConstraint(columnNames = {"id"})})
@JsonIdentityInfo(generator=ObjectIdGenerators.IntSequenceGenerator.class, property="@bodyStatId")
public class BodyStat extends BusinessObject {

et cela devrait fonctionner.

0 votes

Pouvez-vous expliquer "Cela a bien mieux marché" ? Y a-t-il un problème avec la référence gérée ?

0 votes

@UtkuÖzdemir j'ai ajouté des détails sur @JsonIdentityInfo dans ma réponse ci-dessus.

2 votes

C'est la meilleure solution que nous ayons trouvée jusqu'à présent, car lorsque nous avons utilisé " @JsonManagedReference ", la méthode get a retourné avec succès les valeurs sans aucune erreur de stackoverflow. Mais, lorsque nous avons essayé de sauvegarder les données en utilisant le post, il a retourné une erreur de 415 (unsupported media error)

19voto

StaxMan Points 34626

En outre, Jackson 1.6 prend en charge traitement des références bidirectionnelles ... ce qui semble être ce que vous recherchez ( cet article de blog mentionne également cette fonction)

Et depuis juillet 2011, il y a aussi " jackson-module-hibernate "qui pourrait être utile pour certains aspects de la gestion des objets Hibernate, mais pas nécessairement pour cet objet en particulier (qui nécessite des annotations).

1 votes

Les liens sont morts. Pourriez-vous les mettre à jour ou modifier votre réponse ?

12voto

Eugene Retunsky Points 7071

Désormais, Jackson permet d'éviter les cycles sans ignorer les champs :

Jackson - sérialisation des entités avec des relations birectionnelles (éviter les cycles)

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X