2010-08-03 15 views

risposta

128

Sì, questo è possibile. Questo è un caso speciale della relazione bidirezionale standard @ManyToOne/@OneToMany. È speciale perché l'entità su ciascuna estremità della relazione è la stessa. Il caso generale è dettagliato nella sezione 2.10.2 dello JPA 2.0 spec.

Ecco un esempio funzionante. In primo luogo, la classe 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... 
} 

Ecco un metodo di massima main() che persiste tre tali entità:

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

In questo caso, tutte e tre le istanze di entità devono essere persistenti prima transazione commesso. Se non riesco a mantenere una delle entità nel grafico delle relazioni genitore-figlio, viene generata un'eccezione su commit(). Su Eclipselink, questo è un RollbackException che dettaglia l'inconsistenza.

Questo comportamento è configurabile tramite l'attributo cascade nelle annotazioni @OneToMany e @ManyToOneA. Ad esempio, se imposto cascade=CascadeType.ALL in entrambe le annotazioni, posso tranquillamente mantenere una delle entità e ignorare le altre. Dite che ho persistito parent nella mia transazione. L'implementazione JPA attraversa la proprietà perché è contrassegnata con CascadeType.ALL. L'implementazione JPA trova son e daughter lì. Quindi persiste entrambi i bambini a mio nome, anche se non l'ho richiesto esplicitamente.

Un'altra nota. È sempre responsabilità del programmatore aggiornare entrambi i lati di una relazione bidirezionale. In altre parole, ogni volta che aggiungo un figlio a un genitore, devo aggiornare di conseguenza la proprietà genitore del bambino. L'aggiornamento di un solo lato di una relazione bidirezionale è un errore in JPA. Aggiorna sempre entrambi i lati della relazione. Questo è scritto in modo inequivocabile a pagina 42 delle specifiche JPA 2.0:

Si noti che è l'applicazione che ha la responsabilità di mantenere la coerenza di runtime rapporti, ad esempio, per assicurare che “uno” e “molti "I lati di una relazione bidirezionale sono coerenti l'uno con l'altro quando l'applicazione aggiorna la relazione in fase di runtime.

+0

Grazie mille per la spiegazione dettagliata!L'esempio è al punto e ha funzionato nella prima esecuzione. – sanjayav

+0

@sunnyj Felice di aiutare. Buona fortuna con i tuoi progetti). –

+0

Ha incontrato questo problema prima durante la creazione di un'entità di categoria con sottocategorie. È utile! –

5

Per me il trucco era usare la relazione molti-a-molti. Supponiamo che la tua entità A sia una divisione che può avere suddivisioni. Poi (saltando dettagli irrilevanti):

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

Da quando ho avuto una certa vasta logica di business intorno struttura gerarchica e JPA (a seconda del modello relazionale) è molto debole per sostenerlo ho introdotto l'interfaccia IHierarchyElement e l'entità ascoltatore 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

Perché non utilizzare il più semplice @OneToMany (mappedBy = "DIV_PARENT_ID") anziché @ManyToMany (...) con gli attributi autoreferenziali? La ridigitazione di tabelle e nomi di colonne come quelli viola DRY. Forse c'è una ragione per questo, ma non lo vedo. Inoltre, l'esempio EntityListener è pulito ma non portabile, assumendo che 'Top' sia una relazione. Pagina 93 delle specifiche JPA 2.0, Entity Listener e Callback Method: "In generale, il metodo del ciclo di vita di un'applicazione portatile non deve richiamare le operazioni di EntityManager o Query, accedere ad altre istanze di entità o modificare le relazioni". Destra? Fammi sapere se sono fuori. –

+0

La mia soluzione ha 3 anni utilizzando JPA 1.0. L'ho adattato invariato dal codice di produzione. Sono sicuro di poter estrarre alcuni nomi di colonne ma non era questo il punto. La tua risposta è esattamente e più semplice, non sono sicuro del motivo per cui ho usato molti-a-molti allora - ma funziona e sono sicuro che una soluzione più complessa fosse lì per un motivo. Però, dovrò rivisitare questo ora. – topchef

+0

Sì, in alto c'è un autoreferenziale, quindi una relazione. A rigor di termini, non lo modifico, ma solo inizializzandolo. Inoltre, è unidirezionale quindi non c'è dipendenza al contrario, non fa riferimento ad altre entità oltre a se stessi. Secondo la tua quot spec ha "in generale" il che significa che non è una definizione rigorosa. Credo che in questo caso vi sia un rischio di portabilità molto basso, se presente. – topchef

Problemi correlati