Il y a une classe d'entité "A". La classe A peut avoir des enfants du même type "A". De plus, "A" devrait contenir son parent s'il s'agit d'un enfant.
Est-ce possible? Si oui, comment dois-je mapper les relations dans la classe Entity? ["A" a une colonne id.]
Oui, c'est possible Ceci 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é dans 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 approximative main()
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 persistées 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 levée sur commit()
. Sur Eclipselink, c'est un RollbackException
détaillant l'incohérence.
Ce comportement est configurable via l'attribut cascade
des annotations A
@OneToMany
Et @ManyToOne
. Par exemple, si je règle cascade=CascadeType.ALL
Sur ces deux annotations, je pourrais conserver l'une des entités en toute sécurité et ignorer les autres. Disons que j'ai persisté parent
dans ma transaction. L'implémentation JPA traverse la propriété parent
de children
car elle est marquée avec CascadeType.ALL
. L'implémentation JPA y trouve son
et daughter
. Il persiste ensuite les deux enfants en mon nom, même si je ne l’ai pas explicitement demandé.
Encore une note. Il incombe toujours au 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 sans ambiguïté à la page 42 de la spécification JPA 2.0:
Notez que c'est à l'application qu'il incombe de maintenir la cohérence des relations d'exécution, par exemple, de s'assurer de la cohérence des côtés "un" et "plusieurs" d'une relation bidirectionnelle lorsque l'application met à jour la relation au moment de l'exécution. .
Pour moi, l'astuce consistait à utiliser une relation plusieurs à plusieurs. Supposons que votre entité A soit une division pouvant avoir des sous-divisions. Puis (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;
}
...
}
Étant donné que je disposais d'une logique métier étendue autour de la structure hiérarchique et que JPA (basé sur un modèle relationnel) était très faible pour la prendre en charge, j'ai introduit l'interface IHierarchyElement
et l'entité écouteur 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);
}
}
}