JPA: Comment avoir une relation un-à-plusieurs du même type d'entité

101

Il existe une classe d'entité "A". La classe A peut avoir des enfants du même type «A». De plus, "A" doit contenir son parent s'il s'agit d'un enfant.

Est-ce possible? Si tel est le cas, comment dois-je mapper les relations dans la classe Entity? ["A" a une colonne d'identifiant.]

sanjayav
la source

Réponses:

171

Oui, c'est possible. Il s'agit d'un cas particulier de la relation bidirectionnelle @ManyToOne/ standard @OneToMany. C'est spécial parce que l'entité à chaque extrémité de la relation est la même. Le cas général est détaillé dans la section 2.10.2 de la spécification JPA 2.0 .

Voici un exemple travaillé. Tout d'abord, la classe d'entité A:

@Entity
public class A implements Serializable {

    @Id
    @GeneratedValue(strategy=GenerationType.AUTO)
    private Long id;
    @ManyToOne
    private A parent;
    @OneToMany(mappedBy="parent")
    private Collection<A> children;

    // Getters, Setters, serialVersionUID, etc...
}

Voici une main()méthode approximative qui persiste trois de ces entités:

public static void main(String[] args) {

    EntityManager em = ... // from EntityManagerFactory, injection, etc.

    em.getTransaction().begin();

    A parent   = new A();
    A son      = new A();
    A daughter = new A();

    son.setParent(parent);
    daughter.setParent(parent);
    parent.setChildren(Arrays.asList(son, daughter));

    em.persist(parent);
    em.persist(son);
    em.persist(daughter);

    em.getTransaction().commit();
}

Dans ce cas, les trois instances d'entité doivent être persistantes avant la validation de la transaction. Si je ne parviens pas à conserver l'une des entités dans le graphique des relations parent-enfant, une exception est lancée commit(). Sur Eclipselink, c'est un RollbackExceptiondétail de l'incohérence.

Ce comportement est configurable par l' cascadeattribut A« s @OneToManyet @ManyToOneannotations. Par exemple, si je mets cascade=CascadeType.ALLsur ces deux annotations, je pourrais persister en toute sécurité l'une des entités et ignorer les autres. Disons que j'ai persisté parentdans ma transaction. L'implémentation JPA traverse parentla childrenpropriété de car elle est marquée par CascadeType.ALL. L'implémentation JPA trouve sonet daughterlà. Il persiste alors les deux enfants en mon nom, même si je ne l'ai pas explicitement demandé.

Encore une note. Il est toujours de la responsabilité du programmeur de mettre à jour les deux côtés d'une relation bidirectionnelle. En d'autres termes, chaque fois que j'ajoute un enfant à un parent, je dois mettre à jour la propriété parent de l'enfant en conséquence. La mise à jour d'un seul côté d'une relation bidirectionnelle est une erreur sous JPA. Mettez toujours à jour les deux côtés de la relation. Ceci est écrit sans ambiguïté à la page 42 de la spécification JPA 2.0:

Notez que c'est l'application qui a la responsabilité de maintenir la cohérence des relations d'exécution - par exemple, pour s'assurer que les côtés «un» et «plusieurs» d'une relation bidirectionnelle sont cohérents l'un avec l'autre lorsque l'application met à jour la relation au moment de l'exécution. .

Dan LaRocque
la source
Merci beaucoup pour l'explication détaillée! L'exemple est au point et a fonctionné dans la première exécution.
sanjayav
@sunnyj Heureux de vous aider. Bonne chance avec vos projets.
Dan LaRocque
Il a déjà rencontré ce problème lors de la création d'une entité de catégorie comportant des sous-catégories. C'est utile!
Truong Ha
@DanLaRocque peut-être que je ne comprends pas bien (ou que j'ai une erreur de mappage d'entité), mais je constate un comportement inattendu. J'ai une relation un-à-plusieurs entre l'utilisateur et l'adresse. Lorsqu'un utilisateur existant ajoute une adresse, j'ai suivi votre suggestion de mettre à jour l'utilisateur et l'adresse (et d'appeler «enregistrer» sur les deux). Mais cela a entraîné l'insertion d'une ligne en double dans ma table d'adresses. Est-ce parce que j'ai mal configuré mon CascadeType dans le champ d'adresse de l'utilisateur?
Alex
@DanLaRocque est-il possible de définir cette relation comme unidirectionnelle ??
Ali Arda Orhan
8

Pour moi, le truc était d'utiliser la relation plusieurs à plusieurs. Supposons que votre entité A soit une division qui peut avoir des sous-divisions. Ensuite (en sautant les détails non pertinents):

@Entity
@Table(name = "DIVISION")
@EntityListeners( { HierarchyListener.class })
public class Division implements IHierarchyElement {

  private Long id;

  @Id
  @Column(name = "DIV_ID")
  public Long getId() {
        return id;
  }
  ...
  private Division parent;
  private List<Division> subDivisions = new ArrayList<Division>();
  ...
  @ManyToOne
  @JoinColumn(name = "DIV_PARENT_ID")
  public Division getParent() {
        return parent;
  }

  @ManyToMany
  @JoinTable(name = "DIVISION", joinColumns = { @JoinColumn(name = "DIV_PARENT_ID") }, inverseJoinColumns = { @JoinColumn(name = "DIV_ID") })
  public List<Division> getSubDivisions() {
        return subDivisions;
  }
...
}

Comme j'avais une logique métier étendue autour de la structure hiérarchique et que JPA (basé sur un modèle relationnel) est très faible pour le prendre en charge, j'ai introduit l'interface IHierarchyElementet l'écouteur d'entité HierarchyListener:

public interface IHierarchyElement {

    public String getNodeId();

    public IHierarchyElement getParent();

    public Short getLevel();

    public void setLevel(Short level);

    public IHierarchyElement getTop();

    public void setTop(IHierarchyElement top);

    public String getTreePath();

    public void setTreePath(String theTreePath);
}


public class HierarchyListener {

    @PrePersist
    @PreUpdate
    public void setHierarchyAttributes(IHierarchyElement entity) {
        final IHierarchyElement parent = entity.getParent();

        // set level
        if (parent == null) {
            entity.setLevel((short) 0);
        } else {
            if (parent.getLevel() == null) {
                throw new PersistenceException("Parent entity must have level defined");
            }
            if (parent.getLevel() == Short.MAX_VALUE) {
                throw new PersistenceException("Maximum number of hierarchy levels reached - please restrict use of parent/level relationship for "
                        + entity.getClass());
            }
            entity.setLevel(Short.valueOf((short) (parent.getLevel().intValue() + 1)));
        }

        // set top
        if (parent == null) {
            entity.setTop(entity);
        } else {
            if (parent.getTop() == null) {
                throw new PersistenceException("Parent entity must have top defined");
            }
            entity.setTop(parent.getTop());
        }

        // set tree path
        try {
            if (parent != null) {
                String parentTreePath = StringUtils.isNotBlank(parent.getTreePath()) ? parent.getTreePath() : "";
                entity.setTreePath(parentTreePath + parent.getNodeId() + ".");
            } else {
                entity.setTreePath(null);
            }
        } catch (UnsupportedOperationException uoe) {
            LOGGER.warn(uoe);
        }
    }

}
excellent chef
la source
1
Pourquoi ne pas utiliser le plus simple @OneToMany (mappedBy = "DIV_PARENT_ID") au lieu de @ManyToMany (...) avec des attributs d'auto-référencement? Retaper les noms de table et de colonne comme cela enfreint DRY. Il y a peut-être une raison à cela, mais je ne la vois pas. En outre, l'exemple EntityListener est soigné mais non portable, en supposant qu'il Tops'agit d'une relation. Page 93 de la spécification JPA 2.0, Entity Listeners et méthodes de rappel: "En général, la méthode de cycle de vie d'une application portable ne doit pas appeler les opérations EntityManager ou Query, accéder à d'autres instances d'entité ou modifier les relations". Droite? Faites-moi savoir si je pars.
Dan LaRocque
Ma solution a 3 ans avec JPA 1.0. Je l'ai adapté tel quel par rapport au code de production. Je suis sûr que je pourrais supprimer certains noms de colonnes, mais ce n'était pas le but. Votre réponse le fait exactement et plus simplement, je ne sais pas pourquoi j'utilisais plusieurs à plusieurs à l'époque - mais cela fonctionne et je suis convaincu qu'une solution plus complexe existait pour une raison. Cependant, je vais devoir revoir cela maintenant.
topchef
Oui, le haut est une auto-référence, donc une relation. À proprement parler, je ne le modifie pas, je l'initialise simplement. De plus, il est unidirectionnel donc il n'y a pas de dépendance dans l'autre sens, il ne fait pas référence à d'autres entités que soi. Selon votre devis, la spécification a "en général", ce qui signifie que ce n'est pas une définition stricte. Je pense que dans ce cas, le risque de portabilité est très faible, le cas échéant.
topchef