web-dev-qa-db-fra.com

JPA: Comment avoir une relation un-à-plusieurs du même type d'entité

Il y a une classe d'entité "A". La classe A peut avoir des enfants du même type "A". De plus, "A" devrait contenir son parent s'il s'agit d'un enfant.

Est-ce possible? Si oui, comment dois-je mapper les relations dans la classe Entity? ["A" a une colonne id.]

89
sanjayav

Oui, c'est possible Ceci est un cas particulier de la relation bidirectionnelle standard @ManyToOne/@OneToMany. C'est spécial parce que l'entité à chaque extrémité de la relation est la même. Le cas général est détaillé dans la section 2.10.2 du JPA 2.0 spec .

Voici un exemple travaillé. Tout d'abord, la classe d'entité 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...
}

Voici une méthode approximative main() qui persiste trois de ces entités:

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

Dans ce cas, les trois instances d'entité doivent être persistées avant la validation de la transaction. Si je ne parviens pas à conserver l'une des entités dans le graphique des relations parent-enfant, une exception est levée sur commit(). Sur Eclipselink, c'est un RollbackException détaillant l'incohérence.

Ce comportement est configurable via l'attribut cascade des annotations A@OneToMany Et @ManyToOne. Par exemple, si je règle cascade=CascadeType.ALL Sur ces deux annotations, je pourrais conserver l'une des entités en toute sécurité et ignorer les autres. Disons que j'ai persisté parent dans ma transaction. L'implémentation JPA traverse la propriété parent de children car elle est marquée avec CascadeType.ALL. L'implémentation JPA y trouve son et daughter. Il persiste ensuite les deux enfants en mon nom, même si je ne l’ai pas explicitement demandé.

Encore une note. Il incombe toujours au programmeur de mettre à jour les deux côtés d'une relation bidirectionnelle. En d'autres termes, chaque fois que j'ajoute un enfant à un parent, je dois mettre à jour la propriété parent de l'enfant en conséquence. La mise à jour d'un seul côté d'une relation bidirectionnelle est une erreur sous JPA. Toujours mettre à jour les deux côtés de la relation. Ceci est écrit sans ambiguïté à la page 42 de la spécification JPA 2.0:

Notez que c'est à l'application qu'il incombe de maintenir la cohérence des relations d'exécution, par exemple, de s'assurer de la cohérence des côtés "un" et "plusieurs" d'une relation bidirectionnelle lorsque l'application met à jour la relation au moment de l'exécution. .

156
Dan LaRocque

Pour moi, l'astuce consistait à utiliser une relation plusieurs à plusieurs. Supposons que votre entité A soit une division pouvant avoir des sous-divisions. Puis (en sautant les détails non pertinents):

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

Étant donné que je disposais d'une logique métier étendue autour de la structure hiérarchique et que JPA (basé sur un modèle relationnel) était très faible pour la prendre en charge, j'ai introduit l'interface IHierarchyElement et l'entité écouteur 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);
        }
    }

}
7
topchef