views:

53

answers:

2

There's an Entity Class "A". Class A might have children of the same type "A". Also "A" should hold it's parent if it is a child.

Is this possible? If so how should I map the relations in the Entity class? ["A" has an id column.]

+2  A: 

Yes, this is possible. This is a special case of the standard bidirectional ManyToOne/OneToMany relationship. It is special because the entity on each end of the relationship is the same. The general case is detailed in Section 2.10.2 of the JPA 2.0 spec.

Here's a worked example. First, the entity class 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...
}

Here's a rough main() method that persists three such entities:

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 this case, all three entity instances must be persisted before transaction commit. If I fail to persist one of the entities in the graph of parent-child relationships, then an exception is thrown on commit(). On Eclipselink, this is a RollbackException detailing the inconsistency.

This behavior is configurable through the cascade attribute on A's OneToMany and ManyToOne annotations. For instance, if I set cascade=CascadeType.ALL on both of those annotations, I could safely persist one of the entities and ignore the others. Say I persisted parent in my transaction. The JPA implementation traverses parent's children property because it is marked with CascadeType.ALL. The JPA implementation finds son and daughter there. It then persists both children on my behalf, even though I didn't explicitly request it.

One more note. It is always the programmer's responsibility to update both sides of a bidirectional relationship. In other words, whenever I add a child to some parent, I must update the child's parent property accordingly. Updating only one side of a bidirectional relationship is an error under JPA. Always update both sides of the relationship. This is written unambiguously on page 42 of the JPA 2.0 spec:

Note that it is the application that bears responsibility for maintaining the consistency of runtime relationships—for example, for insuring that the “one” and the “many” sides of a bidirectional relationship are consistent with one another when the application updates the relationship at runtime.

Dan LaRocque
Thank you very much for the detailed explanation! The example is to the point and worked in the first run.
sunnyj
@sunnyj Glad to help. Good luck with your project(s).
Dan LaRocque
It met this issue before when creating category entity which has sub categories. It is helpful!
Truong Ha
A: 

For me the trick was to use many-to-many relationship. Suppose that your entity A is a division that can have sub-divisions. Then (skipping irrelevant details):

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

Since I had some extensive business logic around hierarchical structure and JPA (based on relational model) is very weak to support it I introduced interface IHierarchyElement and entity listener 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);
        }
    }

}
grigory
Why not use the simpler @OneToMany(mappedBy="DIV_PARENT_ID") instead of @ManyToMany(...) with self-referencing attributes? Retyping table and column names like that violates DRY. Maybe there's a reason for that, but I don't see it. Also, the EntityListener example is neat but non-portable, assuming `Top` is a relationship. Page 93 of the JPA 2.0 spec, Entity Listeners and Callback Methods: "In general, the lifecycle method of a portable application should not invoke EntityManager or Query operations, access other entity instances, or modify relationships". Right? Let me know if I'm off.
Dan LaRocque
My solution is 3 years old using JPA 1.0. I adapted it unchanged from the production code. I am sure I could take out some columns names but it wasn't the point. Your answer does it exactly and simpler, not sure why I used many-to-many back then - but it does work and I am confident that more complex solution was there for a reason. Though, I'll have to revisit this now.
grigory
Yes, top is a self-reference, hence a relationship. Strictly speaking, I don't modify it - just initialize. Also, it's unidirectional so there is no dependency the other way around, it doesn't reference other entities besides self. Per your quote spec has "in general" which means that it's not strict definition. I believe that in this case there is very low portability risk if any.
grigory