JPA: как иметь отношение "один ко многим" одного и того же типа сущности

101

Есть сущность класса «А». У класса A могут быть дети того же типа «A». Также "A" должен содержать его родителя, если это ребенок.

Это возможно? Если да, то как мне сопоставить отношения в классе Entity? [«A» имеет столбец идентификаторов.]

санджаяв
источник

Ответы:

171

Да, это возможно. Это особый случай стандартного двунаправленного @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:

Обратите внимание, что именно приложение несет ответственность за поддержание согласованности взаимосвязей во время выполнения - например, за обеспечение того, чтобы «одна» и «многие» стороны двунаправленной взаимосвязи согласовывались друг с другом, когда приложение обновляет взаимосвязь во время выполнения. .

Дэн ЛаРок
источник
Большое спасибо за подробное объяснение! Пример точен и сработал при первом запуске.
sanjayav 03
@sunnyj Рад помочь. Удачи в ваших проектах.
Дэн ЛаРок,
Он встречал эту проблему раньше при создании объекта категории, который имеет подкатегории. Это полезно!
Чыонг Ха
@DanLaRocque, возможно, я неправильно понимаю (или у меня есть ошибка сопоставления сущностей), но я вижу неожиданное поведение. У меня отношения "один ко многим" между пользователем и адресом. Когда существующий пользователь добавляет адрес, я последовал вашему предложению обновить и пользователя, и адрес (и вызвать "сохранить" для обоих). Но это привело к тому, что в мою таблицу адресов была вставлена ​​повторяющаяся строка. Это потому, что я неправильно настроил свой CascadeType в поле адреса пользователя?
Alex
@DanLaRocque, можно ли определить эти отношения как однонаправленные ??
Али Арда Орхан
8

Для меня уловка заключалась в использовании отношения «многие ко многим». Предположим, что ваш объект 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);
        }
    }

}
Topchef
источник
1
Почему бы не использовать более простой вариант @OneToMany (mappedBy = "DIV_PARENT_ID") вместо @ManyToMany (...) с атрибутами, ссылающимися на себя? Повторный ввод таких имен таблиц и столбцов нарушает DRY. Может быть, для этого есть причина, но я ее не вижу. Кроме того, пример EntityListener аккуратный, но непереносимый, если предполагается Topсвязь. Страница 93 спецификации JPA 2.0, Слушатели сущностей и методы обратного вызова: «В общем, метод жизненного цикла переносимого приложения не должен вызывать операции EntityManager или Query, обращаться к другим экземплярам сущностей или изменять отношения». Правильно? Дай мне знать, если я уйду.
Дэн ЛаРок,
Моему решению 3 года с использованием JPA 1.0. Я адаптировал его без изменений из производственного кода. Я уверен, что смог бы вычеркнуть названия некоторых столбцов, но дело не в этом. Ваш ответ делает это точно и проще, не знаю, почему я тогда использовал "многие ко многим", но он действительно работает, и я уверен, что более сложное решение было не просто так. Хотя мне придется вернуться к этому сейчас.
topchef 03 авг.10
Да, верх - это ссылка на себя, следовательно, отношения. Строго говоря, я его не модифицирую - просто инициализирую. Кроме того, он однонаправлен, поэтому нет никакой зависимости наоборот, он не ссылается на другие сущности, кроме себя. В вашей спецификации цитаты есть «в целом», что означает, что это не строгое определение. Я считаю, что в этом случае риск переносимости очень низкий.
topchef 03