JPA:如何实现同一实体类型的一对多关系

114

有一个实体类"A",它可能有与自身相同类型的子类"A"。此外,如果"A"是子类,它应该保存它的父类。

这是否可行?如果是,我应该如何映射实体类中的关系? ["A"有一个id列。]

2个回答

184
是的,这是可能的。这是标准的双向@ManyToOne/@OneToMany关系的特殊情况。它很特殊,因为关系两端的实体是相同的。一般情况在JPA 2.0规范的第2.10.2节中详细说明。
下面是一个实际例子。首先,是实体类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...
}

这是一个粗略的main()方法,用于持久化三个这样的实体:
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();
}

在这种情况下,在事务提交之前必须持久化所有三个实体实例。如果我未能持久化父子关系图中的任何一个实体,则会在commit()上抛出异常。在Eclipselink中,这是一个详细说明不一致性的RollbackException
通过A@OneToMany@ManyToOne注释上的cascade属性可以配置此行为。例如,如果我在这些注释上设置了cascade=CascadeType.ALL,则可以安全地持久化其中一个实体并忽略其他实体。假设我在事务中持久化了parent。JPA实现遍历parentchildren属性,因为它被标记为CascadeType.ALL。JPA实现在那里找到sondaughter。然后,它代表我持久化了两个孩子,即使我没有明确请求。
还有一点需要注意。程序员始终有责任更新双向关系的双方。换句话说,每当我将一个孩子添加到某个父级时,我必须相应地更新孩子的父属性。仅更新双向关系的一侧在JPA下是错误的。始终更新关系的双方。这在JPA 2.0规范的第42页上明确写着。
注意,应用程序负责维护运行时关系的一致性,例如,在应用程序更新关系时确保双向关系的“一”和“多”方彼此一致。请保留"{{"和"}}"和html标签。

非常感谢您的详细解释!这个例子很到位,第一次运行就成功了。 - sanjayav
@sunnyj 很高兴能帮忙。祝你的项目顺利。 - Dan LaRocque
以前创建有子类别的分类实体时,遇到过这个问题。这很有帮助! - Truong Ha
@DanLaRocque 或许我误解了(或者有一个实体映射错误),但是我看到了意外的行为。我在用户和地址之间建立了一对多的关系。当现有用户添加地址时,我按照您的建议更新了用户和地址(并在两者上都调用了“save”)。但是这导致我的地址表中插入了重复的行。这是因为我在用户的地址字段上配置了错误的CascadeType吗? - Alex
此解决方案默认假设父级为null,对于父级不为null的情况无法工作,是否有任何解决父级不为null的方法? - user1503117
显示剩余2条评论

9

对于我来说,关键是使用多对多的关系。假设您的实体A是可以拥有子部门的部门。然后(跳过不相关的细节):

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

由于我有一些围绕层次结构的广泛业务逻辑,而JPA(基于关系模型)非常薄弱,不支持它,因此我引入了接口IHierarchyElement和实体侦听器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
为什么不使用更简单的@OneToMany(mappedBy="DIV_PARENT_ID"),而要使用具有自引用属性的@ManyToMany(...)?这样重新输入表和列名称会违反DRY原则。也许有理由这样做,但我看不出来。此外,EntityListener示例很好,但是不可移植,假设Top是一个关系。JPA 2.0规范第93页,实体侦听器和回调方法:“一般来说,可移植应用程序的生命周期方法不应调用EntityManager或Query操作,访问其他实体实例或修改关系”。对吗?如果我错了,请告诉我。 - Dan LaRocque
我的解决方案使用JPA 1.0已经有3年了。我从生产代码中未经修改地适应了它。我确信我可以删除一些列名,但那不是重点。你的答案做得很好,而且更简单,我不确定为什么当时要使用多对多关系,但它确实有效,我相信更复杂的解决方案是有原因的。不过,现在我需要重新审视这个问题。 - topchef
是的,top是一个自我引用,因此是一种关系。严格来说,我没有修改它 - 只是初始化。此外,它是单向的,因此没有其他依赖关系,除了自我引用之外不引用其他实体。根据您的报价规范,“一般而言”意味着它不是严格的定义。我相信在这种情况下,如果有任何可移植性风险,那么风险非常低。 - topchef

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接