JPA: Cómo tener una relación de uno a varios del mismo tipo de entidad

99

Hay una clase de entidad "A". La clase A puede tener hijos del mismo tipo "A". También "A" debe contener a su padre si es un niño.

es posible? Si es así, ¿cómo debo asignar las relaciones en la clase Entity? ["A" tiene una columna de identificación.]

Sanjayav
fuente

Respuestas:

170

Si, esto es posible. Este es un caso especial de la relación @ManyToOne/ bidireccional estándar @OneToMany. Es especial porque la entidad en cada extremo de la relación es la misma. El caso general se detalla en la Sección 2.10.2 de la especificación JPA 2.0 .

Aquí hay un ejemplo trabajado. Primero, la clase de entidad 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...
}

Aquí hay un main()método aproximado que persiste en tres de esas entidades:

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

En este caso, las tres instancias de entidad deben persistir antes de confirmar la transacción. Si no persisto una de las entidades en el gráfico de relaciones entre padres e hijos, se lanza una excepción commit(). En Eclipselink, esto es un RollbackExceptiondetalle de la inconsistencia.

Este comportamiento es configurable a través del cascadeatributo en A's @OneToManyy @ManyToOneanotaciones. Por ejemplo, si establezco cascade=CascadeType.ALLambas anotaciones, podría persistir con seguridad una de las entidades e ignorar las otras. Digamos que persistí parenten mi transacción. La implementación de JPA atraviesa parentla childrenpropiedad porque está marcada con CascadeType.ALL. La implementación de JPA encuentra sony daughterallí. Luego persiste a ambos niños en mi nombre, aunque no lo solicité explícitamente.

Una nota más. Siempre es responsabilidad del programador actualizar ambos lados de una relación bidireccional. En otras palabras, cada vez que agrego un hijo a algún padre, debo actualizar la propiedad del padre del hijo en consecuencia. Actualizar solo un lado de una relación bidireccional es un error en JPA. Siempre actualice ambos lados de la relación. Esto está escrito de forma inequívoca en la página 42 de la especificación JPA 2.0:

Tenga en cuenta que es la aplicación la que tiene la responsabilidad de mantener la coherencia de las relaciones en tiempo de ejecución, por ejemplo, para asegurarse de que los lados "uno" y "muchos" de una relación bidireccional sean coherentes entre sí cuando la aplicación actualiza la relación en tiempo de ejecución. .

Dan LaRocque
fuente
¡Muchas gracias por la explicación detallada! El ejemplo va al grano y funcionó en la primera ejecución.
sanjayav
@sunnyj Encantado de ayudar. Buena suerte con tus proyectos).
Dan LaRocque
Se encontró con este problema antes al crear una entidad de categoría que tiene subcategorías. ¡Es útil!
Truong Ha
@DanLaRocque tal vez estoy entendiendo mal (o tengo un error de mapeo de entidades), pero veo un comportamiento inesperado. Tengo una relación de uno a varios entre el usuario y la dirección. Cuando un usuario existente agrega una dirección, seguí su sugerencia de actualizar tanto el usuario como la dirección (y llamar a 'guardar' en ambos). Pero esto resultó en la inserción de una fila duplicada en mi tabla de direcciones. ¿Se debe a que configuré mal mi CascadeType en el campo de dirección del usuario?
Alex
@DanLaRocque ¿es posible definir esta relación como unidireccional?
Ali Arda Orhan
8

Para mí, el truco consistía en utilizar la relación de varios a varios. Suponga que su entidad A es una división que puede tener subdivisiones. Luego (omitiendo detalles irrelevantes):

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

Como tenía una lógica comercial extensa en torno a la estructura jerárquica y JPA (basado en el modelo relacional) es muy débil para admitirlo, presenté la interfaz IHierarchyElementy el oyente de entidades 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);
        }
    }

}
el mejor chef
fuente
1
¿Por qué no usar @OneToMany (mappedBy = "DIV_PARENT_ID") más simple en lugar de @ManyToMany (...) con atributos de autorreferencia? Volver a escribir nombres de tablas y columnas como ese viola DRY. Quizás haya una razón para eso, pero no la veo. Además, el ejemplo de EntityListener es ordenado pero no portátil, asumiendo que Topes una relación. Página 93 de la especificación JPA 2.0, Entity Listeners y Callback Methods: "En general, el método de ciclo de vida de una aplicación portátil no debe invocar operaciones de EntityManager o Query, acceder a otras instancias de entidad o modificar relaciones". ¿Correcto? Avísame si me voy.
Dan LaRocque
Mi solución tiene 3 años usando JPA 1.0. Lo adapté sin cambios del código de producción. Estoy seguro de que podría eliminar algunos nombres de columnas, pero ese no era el punto. Su respuesta lo hace exactamente y más simple, no estoy seguro de por qué usé muchos a muchos en ese entonces, pero funciona y estoy seguro de que había una solución más compleja por una razón. Sin embargo, tendré que revisar esto ahora.
topchef
Sí, top es una autorreferencia, por lo tanto, una relación. Estrictamente hablando, no lo modifico, solo lo inicializo. Además, es unidireccional, por lo que no hay dependencia al revés, no hace referencia a otras entidades además de sí mismo. Según su cotización, la especificación tiene "en general", lo que significa que no es una definición estricta. Creo que en este caso hay un riesgo de portabilidad muy bajo, si lo hay.
topchef