it-swarm.com.de

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

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

Ist das möglich? Wenn ja, wie sollte ich die Beziehungen in der Entity-Klasse zuordnen? ["A" hat eine ID-Spalte.]

89
sanjayav

Ja, das ist möglich. Dies ist ein Sonderfall der bidirektionalen Standardbeziehung @ManyToOne/@OneToMany. Dies ist besonders, weil die Entität an jedem Ende der Beziehung dieselbe ist. Der allgemeine Fall wird in Abschnitt 2.10.2 der JPA 2.0-Spezifikation beschrieben.

Hier ist ein Beispiel. Zunächst 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 solche 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 für commit() ausgelöst. In Eclipselink ist dies eine RollbackException, die die Inkonsistenz angibt.

Dieses Verhalten kann über das Attribut cascade in den Annotationen @OneToMany Und @ManyToOne Von A konfiguriert werden. Wenn ich beispielsweise für beide Annotationen cascade=CascadeType.ALL Setze, kann ich eine der Entitäten sicher beibehalten und die anderen ignorieren. Angenommen, ich habe parent in meiner Transaktion beibehalten. Die JPA-Implementierung durchläuft die Eigenschaft parentchildren, da sie mit CascadeType.ALL Markiert ist. Die JPA-Implementierung findet dort son und daughter. Es bleiben dann beide Kinder in meinem Namen bestehen, obwohl ich es nicht ausdrücklich angefordert habe.

Noch eine Notiz. 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 Elternteil-Eigenschaft des Kindes entsprechend aktualisieren. Das Aktualisieren nur einer Seite einer bidirektionalen Beziehung ist ein Fehler unter JPA. 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 .

156
Dan LaRocque

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

@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 für hierarchische Strukturen hatte und JPA (basierend auf dem relationalen Modell) sehr schwach ist, um dies zu unterstützen, habe ich die Schnittstelle IHierarchyElement und den Entity-Listener HierarchyListener eingeführt:

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

}
7
topchef