92 votes

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.]

161voto

Dan LaRocque Points 2923

Oui, c'est possible. C'est un cas particulier de la norme bidirectionnel ManyToOne/relation OneToMany. Il est spécial, parce que l'entité sur chaque extrémité de la relation est la même. Le cas général est détaillé dans la Section 2.10.2 de la JPA 2.0 spec.

Voici un exemple concret. Tout d'abord, la classe entity 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 rude main() méthode qui persiste de trois de trois 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, tous les trois instances d'entité doit être conservé avant validation de transaction. Si je ne persistent de l'une des entités dans le graphe de relations parent-enfant, alors une exception est levée sur commit(). Sur Eclipselink, c'est un RollbackException détaillant l'incohérence.

Ce comportement est configurable par l' cascade attribut As'OneToMany et ManyToOne annotations. Par exemple, si j'ai mis en cascade=CascadeType.ALL sur les deux de ces annotations, je pouvais en toute sécurité persistent, l'une des entités et ignore les autres. Dire que j'ai persisté parent de mes transactions. L'implémentation JPA traverse parents' children propriété, parce qu'il est marqué d' CascadeType.ALL. L'implémentation JPA trouve son et daughter . Ensuite, il persiste deux enfants en mon nom, même si je n'ai pas de demande explicite.

Une remarque plus. C'est toujours le programmeur a la responsabilité de mettre à jour les deux côtés d'une relation bidirectionnelle. En d'autres termes, chaque fois que j'ai ajouter un enfant à un parent, je dois mettre à jour le parent de l'enfant de la propriété en conséquence. La mise à jour d'un seul côté d'une relation bidirectionnelle est une erreur en vertu de la JPA. Toujours mettre à jour des deux côtés de la relation. C'est écrit sans ambiguïté à la page 42 de la JPA 2.0 spec:

Notez que c'est la demande qui porte la responsabilité du maintien de la cohérence de l'exécution des relations-par exemple, pour s'assurer que le "un" et "plusieurs" côtés du une relation bidirectionnelle sont compatibles les uns avec les autres lors de l'application des mises à jour de la la relation au moment de l'exécution.

8voto

topchef Points 7473

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 commerciale é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'auditeur d'entité 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);
        }
    }

}
 

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X