JPA: Як мати відношення один до багатьох одного типу сутності


99

Існує предметний клас "А". У класі А можуть бути діти одного типу "А". Також "A" має містити батьків, якщо це дитина.

Чи можливо це? Якщо так, як я повинен відображати відносини в класі Entity? ["A" містить стовпчик ідентифікатора.]

Відповіді:


170

Так, це можливо. Це особливий випадок стандартного двонаправленого @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:

Зауважте, що саме ця програма несе відповідальність за збереження послідовності відносин часу виконання, наприклад, за те, щоб гарантувати, що "одна" та "багато" сторони двосторонніх відносин узгоджуються між собою, коли програма оновлює відносини під час виконання. .


Дуже дякую за детальне пояснення! Приклад до суті і працював у першому циклі.
sanjayav

@sunnyj Радий допомогти. Удачі у ваших проектах.
Dan LaRocque

Цю проблему вона зустрічала раніше, коли створювала сутність категорії, яка має підкатегорії. Це корисно!
Труонг Ха

@DanLaRocque, можливо, я нерозумію (або маю помилку відображення сутності), але я бачу несподівану поведінку. У мене взаємозв'язок між користувачем та адресою. Коли існуючий Користувач додає Адреса, я дотримувався вашої пропозиції оновити як Користувача, так і Адресу (і зателефонувати "зберегти" на обох). Але це призвело до того, що повторюваний рядок було вставлено в мою таблицю адрес. Це тому, що я неправильно налаштував свій CascadeType у полі адреси користувача?
Олексій

@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);
        }
    }

}

1
Чому б не використати простіший @OneToMany (mappedBy = "DIV_PARENT_ID") замість @ManyToMany (...) з атрибутами самопосилання? Розбирання назв таблиці та стовпців, як це, порушує DRY. Можливо, в цьому є причина, але я цього не бачу. Також приклад EntityListener є акуратним, але не портативним, припускаючи, що Topце стосунки. Сторінка 93 специфікації JPA 2.0, слухачі сутності та методи зворотного виклику: "Взагалі метод життєвого циклу портативної програми не повинен викликати операції EntityManager або Query, звертатися до інших екземплярів об'єкта чи змінювати відносини". Правильно? Дайте мені знати, якщо я виходжу.
Dan LaRocque

Моє рішення - 3 роки, використовуючи JPA 1.0. Я адаптував його незмінним від виробничого коду. Я впевнений, що я міг би вийняти кілька назв стовпців, але це було не в чому. Ваша відповідь робить це точно і простіше, не впевнений, чому я тоді використовував багато-багато-багато - але це справді спрацьовує, і я впевнений, що складніше рішення було з причини. Хоча, мені доведеться переглянути це зараз.
топчеф

Так, верх - це самонаправлення, отже, стосунки. Строго кажучи, я його не змінюю - просто ініціалізую. Крім того, він є односпрямованим, тому немає залежності навпаки, він не посилається на інші сутності, крім себе. За вашою цитатою специфікація має "загалом", що означає, що це не суворе визначення. Я вважаю, що в цьому випадку існує дуже низький ризик переносимості, якщо такий є.
топчеф
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.