JPA: Wie man eine Eins-zu-Viele-Beziehung desselben Entitätstyps hat


99

Es gibt eine Entitätsklasse "A". Klasse A hat möglicherweise Kinder des gleichen Typs "A". Auch "A" sollte sein Elternteil halten, wenn es ein Kind ist.

Ist das möglich? Wenn ja, wie soll ich die Beziehungen in der Entitätsklasse zuordnen? ["A" hat eine ID-Spalte.]

Antworten:


170

Ja, das ist möglich. Dies ist ein Spezialfall der bidirektionalen Standard @ManyToOne/ @OneToManyBeziehung. Es ist etwas Besonderes, weil die Entität an jedem Ende der Beziehung dieselbe ist. Der allgemeine Fall ist in Abschnitt 2.10.2 der JPA 2.0-Spezifikation beschrieben .

Hier ist ein Beispiel. Erstens die Entitätsklasse 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...
}

Hier ist eine grobe main()Methode, die drei solcher Entitäten beibehält:

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

In diesem Fall müssen alle drei Entitätsinstanzen vor dem Festschreiben der Transaktion beibehalten werden. Wenn ich eine der Entitäten im Diagramm der Eltern-Kind-Beziehungen nicht beibehalten kann, wird eine Ausnahme ausgelöst commit(). Bei Eclipselink wird RollbackExceptiondie Inkonsistenz detailliert beschrieben.

Dieses Verhalten kann über das cascadeAttribut Aons @OneToManyund @ManyToOneAnmerkungen konfiguriert werden . Wenn ich beispielsweise cascade=CascadeType.ALLbeide Anmerkungen festlege , kann ich eine der Entitäten sicher beibehalten und die anderen ignorieren. Angenommen, ich habe parentmeine Transaktion fortgesetzt . Die JPA-Implementierung durchläuft parentdie childrenEigenschaft, da sie mit gekennzeichnet ist CascadeType.ALL. Die JPA-Implementierung findet sonund daughterdort. Es bleiben dann beide Kinder in meinem Namen bestehen, obwohl ich es nicht ausdrücklich angefordert habe.

Noch eine Anmerkung. Es liegt immer in der Verantwortung des Programmierers, beide Seiten einer bidirektionalen Beziehung zu aktualisieren. Mit anderen Worten, wenn ich einem Elternteil ein Kind hinzufüge, muss ich die Eltern-Eigenschaft des Kindes entsprechend aktualisieren. Das Aktualisieren nur einer Seite einer bidirektionalen Beziehung ist unter JPA ein Fehler. Aktualisieren Sie immer beide Seiten der Beziehung. Dies ist eindeutig auf Seite 42 der JPA 2.0-Spezifikation geschrieben:

Beachten Sie, dass die Anwendung für die Aufrechterhaltung der Konsistenz von Laufzeitbeziehungen verantwortlich ist, z. B. um sicherzustellen, dass die "eine" und die "viele" Seite einer bidirektionalen Beziehung miteinander konsistent sind, wenn die Anwendung die Beziehung zur Laufzeit aktualisiert .


Vielen Dank für die ausführliche Erklärung! Das Beispiel ist auf den Punkt und arbeitete im ersten Lauf.
Sanjayav

@sunnyj Freut mich zu helfen. Viel Glück mit Ihren Projekten.
Dan LaRocque

Dieses Problem wurde bereits beim Erstellen einer Kategorieentität mit Unterkategorien behoben. Es ist hilfreich!
Truong Ha

@DanLaRocque Vielleicht verstehe ich das falsch (oder habe einen Fehler bei der Entitätszuordnung), aber ich sehe unerwartetes Verhalten. Ich habe eine Eins-zu-Viele-Beziehung zwischen Benutzer und Adresse. Wenn ein vorhandener Benutzer eine Adresse hinzufügt, habe ich Ihren Vorschlag befolgt, sowohl den Benutzer als auch die Adresse zu aktualisieren (und bei beiden die Option "Speichern" aufzurufen). Dies führte jedoch dazu, dass eine doppelte Zeile in meine Adresstabelle eingefügt wurde. Liegt das daran, dass ich meinen CascadeType im Adressfeld des Benutzers falsch konfiguriert habe?
Alex

@DanLaRocque ist es möglich, diese Beziehung als unidirektional zu definieren?
Ali Arda Orhan

8

Für mich bestand der Trick darin, eine Viele-zu-Viele-Beziehung zu verwenden. Angenommen, Ihre Entität A ist eine Abteilung, die Unterteilungen haben kann. Dann (Überspringen irrelevanter Details):

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

Da ich eine umfangreiche Geschäftslogik in Bezug auf die hierarchische Struktur hatte und JPA (basierend auf dem relationalen Modell) sehr schwach ist, um dies zu unterstützen, habe ich den IHierarchyElementListener für Schnittstellen und Entitäten eingeführt 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
Warum nicht das einfachere @OneToMany (mappedBy = "DIV_PARENT_ID") anstelle von @ManyToMany (...) mit selbstreferenzierenden Attributen verwenden? Das erneute Eingeben solcher Tabellen- und Spaltennamen verstößt gegen DRY. Vielleicht gibt es einen Grund dafür, aber ich sehe es nicht. Das EntityListener-Beispiel ist außerdem ordentlich, aber nicht portierbar, vorausgesetzt, es Tophandelt sich um eine Beziehung. Seite 93 der JPA 2.0-Spezifikation, Entitätslistener und Rückrufmethoden: "Im Allgemeinen sollte die Lebenszyklusmethode einer tragbaren Anwendung keine EntityManager- oder Abfragevorgänge aufrufen, nicht auf andere Entitätsinstanzen zugreifen oder Beziehungen ändern." Richtig? Lass es mich wissen, wenn ich weg bin.
Dan LaRocque

Meine Lösung ist 3 Jahre alt mit JPA 1.0. Ich habe es unverändert vom Produktionscode angepasst. Ich bin sicher, ich könnte einige Spaltennamen herausnehmen, aber es war nicht der Punkt. Ihre Antwort macht es genau und einfacher, nicht sicher, warum ich damals viele zu viele verwendet habe - aber es funktioniert und ich bin zuversichtlich, dass es aus einem bestimmten Grund eine komplexere Lösung gab. Allerdings muss ich das jetzt noch einmal überdenken.
Topchef

Ja, oben ist eine Selbstreferenz, daher eine Beziehung. Genau genommen ändere ich es nicht - initialisiere es einfach. Außerdem ist es unidirektional, so dass es keine Abhängigkeit gibt, es verweist nicht auf andere Entitäten außer auf sich selbst. Gemäß Ihrer Angebotsspezifikation hat "im Allgemeinen", was bedeutet, dass es keine strenge Definition ist. Ich glaube, dass in diesem Fall ein sehr geringes Portabilitätsrisiko besteht, wenn überhaupt.
Topchef
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.