JPA: jak mieć relację jeden do wielu tego samego typu jednostki

101

Istnieje klasa jednostek „A”. Klasa A może mieć elementy podrzędne tego samego typu „A”. Również „A” powinno trzymać swojego rodzica, jeśli jest dzieckiem.

czy to możliwe? Jeśli tak, jak mam odwzorować relacje w klasie Entity? [„A” ma kolumnę id.]

sanjayav
źródło

Odpowiedzi:

171

Tak, jest to możliwe. Jest to szczególny przypadek standardowego dwukierunkowego @ManyToOne/ @OneToManyrelacji. Jest wyjątkowy, ponieważ jednostka na każdym końcu związku jest taka sama. Ogólny przypadek opisano szczegółowo w sekcji 2.10.2 specyfikacji JPA 2.0 .

Oto praktyczny przykład. Po pierwsze, klasa encji 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...
}

Oto przybliżona main()metoda, która utrzymuje trzy takie jednostki:

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

W takim przypadku wszystkie trzy instancje jednostki muszą zostać utrwalone przed zatwierdzeniem transakcji. Jeśli nie uda mi się utrwalić jednej z encji na wykresie relacji rodzic-dziecko, zostanie zgłoszony wyjątek commit(). W Eclipselink jest to RollbackExceptionwyszczególnienie niespójności.

Takie zachowanie jest konfigurowany przez cascadeatrybutu A„s @OneToManyi @ManyToOneadnotacji. Na przykład, jeśli ustawię cascade=CascadeType.ALLobie te adnotacje, mogę bezpiecznie utrwalić jedną z jednostek i zignorować pozostałe. Powiedz, że wytrwałam parentw transakcji. Implementacja JPA przechodzi parentprzez childrenwłaściwość, ponieważ jest oznaczona znakiem CascadeType.ALL. Wdrożenie JPA znajduje soni daughtertam. Następnie oboje dzieci utrzymują się w moim imieniu, mimo że wyraźnie o to nie prosiłem.

Jeszcze jedna uwaga. Obowiązkiem programisty jest zawsze aktualizacja obu stron relacji dwukierunkowej. Innymi słowy, ilekroć dodaję dziecko do jakiegoś rodzica, muszę odpowiednio zaktualizować jego własność rodzicielską. Aktualizowanie tylko jednej strony relacji dwukierunkowej jest błędem w JPA. Zawsze aktualizuj obie strony relacji. Jest to jednoznacznie napisane na stronie 42 specyfikacji JPA 2.0:

Należy zauważyć, że to aplikacja ponosi odpowiedzialność za utrzymanie spójności relacji w czasie wykonywania - na przykład za zapewnienie, że „jedna” i „wiele” stron relacji dwukierunkowej są ze sobą spójne, gdy aplikacja aktualizuje relację w czasie wykonywania .

Dan LaRocque
źródło
Bardzo dziękuję za szczegółowe wyjaśnienie! Przykład jest na temat i działał w pierwszej serii.
sanjayav
@sunnyj Cieszę się, że mogłem pomóc. Powodzenia w Twoich projektach.
Dan LaRocque
Spotkałem się z tym problemem już podczas tworzenia encji kategorii, która ma podkategorie. To jest pomocne!
Truong Ha,
@DanLaRocque być może nie rozumiem (lub mam błąd mapowania encji), ale widzę nieoczekiwane zachowanie. Mam relację jeden do wielu między użytkownikiem a adresem. Kiedy istniejący użytkownik dodaje adres, postąpiłem zgodnie z twoją sugestią, aby zaktualizować zarówno użytkownika, jak i adres (i wezwać „zapisz” na obu). Ale to spowodowało wstawienie zduplikowanego wiersza do mojej tabeli adresów. Czy to dlatego, że źle skonfigurowałem CascadeType w polu adresu użytkownika?
Alex
@DanLaRocque czy można określić tę relację jako jednokierunkową?
Ali Arda Orhan
8

Dla mnie sztuczka polegała na wykorzystaniu relacji wiele do wielu. Załóżmy, że Twój byt A jest dywizją, która może mieć podpodziały. Następnie (pomijając nieistotne szczegóły):

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

Ponieważ miałem rozbudowaną logikę biznesową dotyczącą struktury hierarchicznej, a JPA (oparty na modelu relacyjnym) jest bardzo słaba, aby ją wspierać, wprowadziłem interfejs IHierarchyElementi odbiornik encji 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
źródło
1
Dlaczego nie użyć prostszego @OneToMany (mappedBy = "DIV_PARENT_ID") zamiast @ManyToMany (...) z atrybutami odwołującymi się do siebie? Zmiana typu nazw tabel i kolumn w ten sposób narusza DRY. Może jest ku temu powód, ale ja tego nie widzę. Ponadto przykład EntityListener jest zgrabny, ale nieprzenośny, zakładając, że Topjest to relacja. Strona 93 specyfikacji JPA 2.0, Entity Listeners i Callback Methods: „Ogólnie rzecz biorąc, metoda cyklu życia aplikacji przenośnej nie powinna wywoływać operacji EntityManager ani Query, uzyskiwać dostępu do innych instancji encji ani modyfikować relacji”. Dobrze? Daj mi znać, jeśli wyjdę.
Dan LaRocque
Moje rozwiązanie ma 3 lata i używa JPA 1.0. Dostosowałem go bez zmian z kodu produkcyjnego. Jestem pewien, że mógłbym usunąć nazwy niektórych kolumn, ale nie o to chodziło. Twoja odpowiedź robi to dokładnie i prościej, nie jestem pewien, dlaczego użyłem wtedy wielu do wielu - ale działa i jestem przekonany, że nie bez powodu było tam bardziej złożone rozwiązanie. Chociaż muszę teraz do tego wrócić.
topchef
Tak, góra to odniesienie do siebie, stąd związek. Ściśle mówiąc, nie modyfikuję go - po prostu zainicjuj. Ponadto jest jednokierunkowy, więc nie ma żadnej zależności na odwrót, nie odnosi się do innych bytów poza sobą. Zgodnie z cytatem specyfikacja zawiera „ogólnie”, co oznacza, że ​​nie jest to ścisła definicja. Uważam, że w tym przypadku istnieje bardzo niskie ryzyko przenoszenia.
topchef