Відповіді:
Так, це можливо. Це особливий випадок стандартного двонаправленого @ManyToOne
/ @OneToMany
відносини. Він особливий тим, що сутність на кожному кінці відносин однакова. Загальний випадок детально описаний у розділі 2.10.2 специфікації JPA 2.0 .
Ось відпрацьований приклад. По-перше, клас сутності 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...
}
Ось приблизний main()
метод, який зберігає три такі сутності:
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();
}
У цьому випадку всі три екземпляри суб'єкта господарювання повинні зберігатися до здійснення транзакції. Якщо мені не вдасться зберегти одну з сутностей у графіку відносин батько-дитина, тоді застосовується виняток commit()
. На Eclipselink це RollbackException
деталізація неузгодженості.
Така поведінка може бути налаштована за допомогою cascade
атрибута A
's @OneToMany
та @ManyToOne
анотацій. Наприклад, якщо я встановив cascade=CascadeType.ALL
обидва ці анотації, я міг би безпечно зберігати одну з сутностей і ігнорувати інші. Скажіть, я наполягав parent
на своїй транзакції. Траверси реалізації JPA parent
«и children
власності , тому що він відзначений CascadeType.ALL
. Впровадження JPA знайде son
і daughter
там. Потім він зберігає обох дітей від мого імені, хоча я прямо не просив цього.
Ще одна примітка. Завжди відповідальність програміста за оновлення обох сторін двосторонніх відносин. Іншими словами, щоразу, коли я додаю дитину до якогось з батьків, я повинен відповідно оновити батьківське властивість дитини. Оновлення лише однієї сторони двонаправлених відносин є помилкою в рамках JPA. Завжди оновлюйте обидві сторони відносин. Це написано однозначно на сторінці 42 специфікації JPA 2.0:
Зауважте, що саме ця програма несе відповідальність за збереження послідовності відносин часу виконання, наприклад, за те, щоб гарантувати, що "одна" та "багато" сторони двосторонніх відносин узгоджуються між собою, коли програма оновлює відносини під час виконання. .
Для мене хитрість полягала у використанні відносин «багато-до-багатьох». Припустимо, що ваша сутність A - це підрозділ, який може мати підрозділи. Потім (пропускаючи невідповідні деталі):
@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;
}
...
}
Оскільки у мене була обширна ділова логіка навколо ієрархічної структури, і JPA (заснована на реляційній моделі) дуже слабка для її підтримки, я представив інтерфейс IHierarchyElement
і слухач сутності 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
це стосунки. Сторінка 93 специфікації JPA 2.0, слухачі сутності та методи зворотного виклику: "Взагалі метод життєвого циклу портативної програми не повинен викликати операції EntityManager або Query, звертатися до інших екземплярів об'єкта чи змінювати відносини". Правильно? Дайте мені знати, якщо я виходжу.