2010-08-03 25 views

Respuesta

128

Sí, esto es posible. Este es un caso especial de la relación estándar bidireccional @ManyToOne/@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 del JPA 2.0 spec.

Aquí hay un ejemplo trabajado. En primer lugar, 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... 
} 

He aquí un método main() áspera que persiste tres de estas 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 la entidad deben ser persistieron antes de confirmar la transacción. Si no puedo persistir en una de las entidades en el gráfico de las relaciones padre-hijo, se genera una excepción en commit(). En Eclipselink, este es un RollbackException que detalla la incoherencia.

Este comportamiento se puede configurar a través del atributo cascade en A's @OneToMany y @ManyToOne anotaciones. Por ejemplo, si configuro cascade=CascadeType.ALL en ambas anotaciones, podría persistir de forma segura una de las entidades e ignorar las demás. Digamos que persistí parent en mi transacción. La implementación de JPA atraviesa la propiedad parent de children porque está marcada con CascadeType.ALL. La implementación de JPA encuentra allí son y daughter. 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 principal del hijo en consecuencia. Actualizar solo un lado de una relación bidireccional es un error bajo JPA. Siempre actualice ambos lados de la relación. Esto está escrito de forma inequívoca en la página 42 de la APP 2.0 especificaciones:

en cuenta que es la aplicación que tiene la responsabilidad de mantener la consistencia de tiempo de ejecución de las relaciones, por ejemplo, para asegurar que el “uno” y los “muchos "Los lados de una relación bidireccional son consistentes entre sí cuando la aplicación actualiza la relación en tiempo de ejecución.

+0

¡Muchas gracias por la explicación detallada!El ejemplo es al grano y funcionó en la primera ejecución. – sanjayav

+0

@sunnyj Me alegra ayudar. Buena suerte con tus proyectos). –

+0

Se encontró con este problema antes al crear entidad de categoría que tiene subcategorías. ¡Es útil! –

5

Para mí, el truco era utilizar la relación muchos a muchos. Supongamos que su entidad A es una división que puede tener subdivisiones. A continuación (se pierden los 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; 
    } 
... 
} 

Ya que tenía alguna extensa lógica de negocio en torno a la estructura jerárquica y JPA (basado en el modelo relacional) es muy débil para soportar lo introduje interfaz IHierarchyElement y el oyente entidad 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

¿Por qué no utilizar el @OneToMany más simple (mappedBy = "DIV_PARENT_ID") en lugar de @ManyToMany (...) con atributos de autorreferencia? Los nombres de columna y columna de esa manera violan DRY. Tal vez haya una razón para eso, pero yo no lo veo. Además, el ejemplo de EntityListener es ordenado pero no portátil, suponiendo que 'Top' es una relación. Página 93 de la especificación JPA 2.0, Escucha de entidad y Métodos de devolución de llamada: "En general, el método del ciclo de vida de una aplicación portátil no debe invocar EntityManager o operaciones de consulta, acceder a otras instancias de entidad o modificar relaciones". ¿Derecha? Avísame si me voy. –

+0

Mi solución es de 3 años usando JPA 1.0. Lo adapté sin cambios desde el código de producción. Estoy seguro de que podría sacar algunos nombres de columnas pero no fue el punto. Su respuesta lo hace exactamente y más simple, no estoy seguro de por qué usé muchos a muchos en aquel entonces, pero funciona y confío en que haya una solución más compleja por algún motivo. Sin embargo, tendré que volver a visitar esto ahora. – topchef

+0

Sí, la parte superior es una autorreferencia, de ahí una relación. Estrictamente hablando, no lo modifico, solo inicializar. Además, es unidireccional por lo que no hay dependencia al revés, no hace referencia a otras entidades además de uno mismo. Según su cita, 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, en su caso. – topchef

Cuestiones relacionadas