Существует класс сущности "A". Класс A может иметь детей одного типа "A". Также "A" должен содержать его родительский, если он является дочерним.
Возможно ли это? Если да, то как мне сопоставить отношения в классе Entity? [ "A" имеет столбец id.]
Существует класс сущности "A". Класс A может иметь детей одного типа "A". Также "A" должен содержать его родительский, если он является дочерним.
Возможно ли это? Если да, то как мне сопоставить отношения в классе Entity? [ "A" имеет столбец id.]
Да, это возможно. Это особый случай стандартного двунаправленного отношения @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
@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);
}
}
}