Risposte:
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 RollbackException
dettaglio dell'incongruenza.
Questo comportamento è configurabile attraverso l' cascade
attributo A
's @OneToMany
e @ManyToOne
annotazioni. Ad esempio, se imposto cascade=CascadeType.ALL
entrambe le annotazioni, potrei mantenere in sicurezza una delle entità e ignorare le altre. Diciamo che ho insistito parent
nella mia transazione. L'implementazione JPA attraversa parent
la children
proprietà perché è contrassegnata con CascadeType.ALL
. L'implementazione dell'APP trova son
edaughter
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 .
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 IHierarchyElement
e 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);
}
}
}
Top
sia 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.