2010-08-03 16 views

Répondre

128

Oui, cela est possible. C'est un cas particulier de la relation bidirectionnelle standard @ManyToOne/@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é à la section 2.10.2 du JPA 2.0 spec.

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 méthode main() grossière 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, doivent être conservées trois instances d'entités avant validation de la transaction. Si je ne parviens pas à persister une des entités dans le graphique des relations parent-enfant, une exception est lancée sur commit(). Sur Eclipselink, il s'agit d'un RollbackException détaillant l'incohérence.

Ce comportement est configurable via l'attribut cascade sur les annotations @OneToMany et @ManyToOne de A. Par exemple, si je mets cascade=CascadeType.ALL sur ces deux annotations, je peux en toute sécurité persister une des entités et ignorer les autres. Dire que j'ai persisté parent dans ma transaction. La mise en œuvre JPA traverse parentchildren car il est marqué CascadeType.ALL. L'implémentation JPA trouve son et daughter là. Il persiste alors les deux enfants en mon nom, même si je ne l'ai pas explicitement demandé.

Une note de plus. 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. Toujours mettre à jour les deux côtés de la relation. Ceci est écrit clairement à la page 42 de la spécification JPA 2.0:

Notez qu'il est l'application qui porte la responsabilité de maintenir la cohérence de runtime relations par exemple, pour assurer que le « un » et « beaucoup Les côtés d'une relation bidirectionnelle sont cohérents entre eux lorsque l'application met à jour la relation lors de l'exécution.

+0

Merci beaucoup pour l'explication détaillée!L'exemple est au point et a travaillé dans la première manche. – sanjayav

+0

@sunnyj Content de vous aider. Bonne chance avec votre projet (s). –

+0

Il a déjà rencontré ce problème lors de la création d'une entité de catégorie avec des sous-catégories. C'est utile! –

5

Pour moi, l'astuce consistait à utiliser une relation plusieurs-à-plusieurs. Supposons que votre entité A soit une division pouvant comporter des sous-divisions. Ensuite (sauter 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; 
    } 
... 
} 

Depuis que j'avais une logique d'affaires étendu autour de la structure hiérarchique et JPA (basée sur le modèle relationnel) est très faible pour le soutenir, j'ai présenté l'interface IHierarchyElement et auditeur de l'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); 
     } 
    } 

} 
+1

Pourquoi ne pas utiliser le @OneToMany plus simple (mappedBy = "DIV_PARENT_ID") au lieu de @ManyToMany (...) avec des attributs autoréférencés? Le réécryptage des noms de tables et de colonnes comme ceux-là viole DRY. Peut-être y a-t-il une raison à cela, mais je ne le vois pas. En outre, l'exemple EntityListener est propre mais non portable, en supposant que 'Top' est une relation. Spécification JPA 2.0, Entity Listeners et méthodes de rappel: «En général, la méthode du cycle de vie d'une application portable ne doit pas invoquer les opérations EntityManager ou Query, accéder à d'autres instances d'entité ou modifier les relations». Droite? Dites-moi si je pars. –

+0

Ma solution a 3 ans en utilisant JPA 1.0. Je l'ai adapté sans changement du code de production. Je suis sûr que je pourrais prendre quelques noms de colonnes mais ce n'était pas le point. Votre réponse le fait exactement et plus simplement, je ne sais pas pourquoi j'ai utilisé plusieurs-à-plusieurs à l'époque - mais cela fonctionne et je suis confiant qu'une solution plus complexe était là pour une raison. Cependant, je vais devoir revoir cela maintenant. – topchef

+0

Oui, top est une auto-référence, donc une relation. Strictement parlant, je ne le modifie pas - juste initialiser. En outre, il est unidirectionnel donc il n'y a pas de dépendance inverse, il ne fait référence à aucune autre entité que self. Par spécification de votre devis a "en général" ce qui signifie que ce n'est pas une définition stricte. Je crois que, dans ce cas, le risque de transférabilité est très faible. – topchef