JPA: come avere una relazione uno-a-molti dello stesso tipo di entità


99

C'è una classe di entità "A". La classe A potrebbe avere figli dello stesso tipo "A". Anche "A" dovrebbe contenere il genitore se è un bambino.

È possibile? In caso affermativo, come devo mappare le relazioni nella classe Entity? ["A" ha una colonna id.]

Risposte:


170

Sì, è possibile. Questo è un caso speciale della relazione @ManyToOne/ bidirezionale standard @OneToMany. È speciale perché l'entità a ciascuna estremità della relazione è la stessa. Il caso generale è descritto in dettaglio nella sezione 2.10.2 della specifica JPA 2.0 .

Ecco un esempio funzionante. Innanzitutto, la classe di 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 main()metodo approssimativo che persiste tre di queste 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 dell'entità devono essere mantenute prima del commit della transazione. Se non riesco a persistere una delle entità nel grafico delle relazioni genitore-figlio, viene generata un'eccezione commit(). Su Eclipselink, questo è un RollbackExceptiondettaglio dell'incongruenza.

Questo comportamento è configurabile attraverso l' cascadeattributo A's @OneToManye @ManyToOneannotazioni. Ad esempio, se imposto cascade=CascadeType.ALLentrambe le annotazioni, potrei mantenere in sicurezza una delle entità e ignorare le altre. Diciamo che ho insistito parentnella mia transazione. L'implementazione JPA attraversa parentla childrenproprietà perché è contrassegnata con CascadeType.ALL. L'implementazione dell'APP trova sonedaughter lì. Quindi persiste entrambi i bambini per mio conto, anche se non l'ho richiesto esplicitamente.

Ancora una 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 figlio. 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 delle relazioni di runtime, ad esempio per garantire che i lati "uno" e "molti" di una relazione bidirezionale siano coerenti l'uno con l'altro quando l'applicazione aggiorna la relazione in fase di esecuzione .


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

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

Ha incontrato questo problema in precedenza durante la creazione di un'entità categoria con sottocategorie. È utile!
Truong Ha

@DanLaRocque forse sto fraintendendo (o ho un errore di mappatura delle entità), ma vedo un comportamento imprevisto. Ho una relazione uno-a-molti tra Utente e Indirizzo. Quando un utente esistente aggiunge un indirizzo, ho seguito il tuo suggerimento per aggiornare sia l'utente che l'indirizzo (e chiamare "salva" su entrambi). Ma questo ha comportato l'inserimento di una riga duplicata nella mia tabella degli indirizzi. È perché ho configurato male il mio CascadeType nel campo dell'indirizzo dell'utente?
Alex

@DanLaRocque è possibile definire questo rapporto come unidirezionale ??
Ali Arda Orhan

8

Per me il trucco era usare la relazione molti-a-molti. Supponi che la tua entità A sia una divisione che può avere suddivisioni. Quindi (ignorando 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;
  }
...
}

Dato che avevo una logica di business estesa intorno alla struttura gerarchica e JPA (basato sul modello relazionale) è molto debole per supportarlo, ho introdotto l'interfaccia IHierarchyElemente l'ascoltatore di 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
Perché non utilizzare il più semplice @OneToMany (mappedBy = "DIV_PARENT_ID") invece di @ManyToMany (...) con attributi autoreferenziali? La ridigitazione di nomi di tabelle e colonne in questo modo viola DRY. Forse c'è una ragione per questo, ma non lo vedo. Inoltre, l'esempio EntityListener è chiaro ma non portabile, supponendo che Topsia una relazione. Pagina 93 della specifica JPA 2.0, Entity Listeners e metodi di callback: "In generale, il metodo del ciclo di vita di un'applicazione portatile non deve richiamare le operazioni EntityManager o Query, accedere ad altre istanze dell'entità o modificare le relazioni". Destra? Fammi sapere se me ne vado.
Dan LaRocque

La mia soluzione è di 3 anni usando JPA 1.0. L'ho adattato invariato rispetto al codice di produzione. Sono sicuro che avrei potuto eliminare alcuni nomi di colonne ma non era questo il punto. La tua risposta lo fa esattamente e in modo 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. Tuttavia, dovrò rivisitare questo ora.
topchef

Sì, la parte superiore è un'autoreferenzialità, quindi una relazione. A rigor di termini, non lo modifico, basta inizializzare. Inoltre, è unidirezionale, quindi non c'è dipendenza al contrario, non fa riferimento ad altre entità oltre al sé. Secondo il tuo preventivo, le specifiche hanno "in generale", il che significa che non è una definizione rigorosa. Credo che in questo caso il rischio di portabilità sia molto basso.
topchef
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.