JPA:如何具有相同实体类型的一对多关系


99

有一个实体类“ A”。A类可能具有相同类型“ A”的子级。如果“ A”是孩子,则也应保留它的父母。

这可能吗?如果是这样,我应该如何在Entity类中映射关系?[“ A”有一个id列。]

Answers:


170

是的,这是可能的。这是标准双向@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详细说明不一致的地方。

此行为是通过可配置cascade的属性A@OneToMany@ManyToOne注释。例如,如果我同时设置cascade=CascadeType.ALL了这两个注释,则可以安全地保留其中一个实体,而忽略其他实体。说我坚持parent了我的交易。JPA实现遍历parentchildren属性是因为它用标记CascadeType.ALL。JPA实现的发现sondaughter那里。然后,即使我没有明确要求,它也代表我保留了两个孩子。

还有一张便条。更新双向关系的两端始终是程序员的责任。换句话说,每当我向某个父级添加子级时,都必须相应地更新该子级的父级属性。在JPA下,仅更新双向关系的一侧是错误。始终更新关系的双方。这是明确写在JPA 2.0规范的第42页上的:

请注意,应用程序负责维护运行时关系的一致性,例如,当应用程序在运行时更新关系时,确保双向关系的“一侧”和“许多”侧彼此一致。


非常感谢您的详细解释!这个例子很重要,并且在第一次运行中就起作用了。
sanjayav

@sunnyj很高兴提供帮助。祝您的项目顺利。
丹·拉洛克

在创建具有子类别的类别实体之前,它已解决了此问题。有帮助!
Truong Ha

@DanLaRocque也许我误会了(或有一个实体映射错误),但是我看到了意外的行为。我与用户和地址之间存在一对多关系。当现有用户添加地址时,我按照您的建议更新了用户和地址(并在两者上都调用了“保存”)。但这导致将重复的行插入到我的地址表中。这是因为我在“用户的地址”字段上配置了我的CascadeType吗?
亚历克斯

@DanLaRocque是否可以将这种关系定义为单向?
阿里·阿尔达·奥尔罕

8

对我而言,诀窍是使用多对多关系。假设您的实体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操作,访问其他实体实例或修改关系”。对?让我知道我是否要离开。
丹·拉洛克

我的解决方案使用JPA 1.0已经3年了。我将其与生产代码保持不变。我确定我可以取出一些列名,但这不是重点。您的答案确实准确,简单,不确定当时我为什么要使用多对多方法-但它确实有效,并且我相信有一个更复杂的解决方案是有原因的。不过,我现在必须重新审视一下。
topchef

是的,top是一种自我参照,因此是一种关系。严格来说,我不修改它-只是初始化。另外,它是单向的,因此没有依赖性,除了self之外,它不引用其他实体。根据您的报价,规范中包含“一般”,这意味着它不是严格的定义。我认为,在这种情况下,可移植性风险非常低(如果有的话)。
topchef
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.