【问题标题】:JPA: How to have one-to-many relation of the same Entity typeJPA:如何具有相同实体类型的一对多关系
【发布时间】:2011-03-24 12:55:34
【问题描述】:

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

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

【问题讨论】:

    标签: java orm jpa hierarchy one-to-many


    【解决方案1】:

    是的,这是可能的。这是标准双向@ManyToOne/@OneToMany 关系的特例。之所以特殊,是因为关系两端的实体是相同的。一般情况在JPA 2.0 spec的第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 页上明确写的:

    请注意,应用程序负责维护运行时关系的一致性——例如,当应用程序更新 运行时的关系。

    【讨论】:

    • 非常感谢您的详细解释!这个例子很中肯,在第一次运行中就奏效了。
    • @sunnyj 很高兴为您提供帮助。祝你的项目好运。
    • 之前在创建具有子类别的类别实体时遇到了这个问题。很有帮助!
    • @DanLaRocque 也许我误解了(或有实体映射错误),但我看到了意外的行为。我在用户和地址之间有一对多的关系。当现有用户添加地址时,我按照您的建议更新了用户和地址(并在两者上调用“保存”)。但这导致将重复的行插入到我的地址表中。这是因为我在用户地址字段中错误配置了 CascadeType?
    • @DanLaRocque 是否可以将这种关系定义为单向的??
    【解决方案2】:

    对我来说,诀窍是使用多对多关系。假设您的实体 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);
            }
        }
    
    }
    

    【讨论】:

    • 为什么不使用更简单的@OneToMany(mappedBy="DIV_PARENT_ID") 而不是带有自引用属性的@ManyToMany(...)?像这样重新键入表和列名违反了 DRY。也许是有原因的,但我不明白。此外,假设Top 是一个关系,EntityListener 示例很简洁但不可移植。 JPA 2.0 规范的第 93 页,实体侦听器和回调方法:“通常,可移植应用程序的生命周期方法不应调用 EntityManager 或 Query 操作、访问其他实体实例或修改关系”。正确的?如果我下班了,请告诉我。
    • 我的解决方案使用 JPA 1.0 已有 3 年历史。我从生产代码中修改了它。我确信我可以取出一些列名,但这不是重点。您的答案准确而简单,不知道为什么我当时使用了多对多 - 但它确实有效,我相信有更复杂的解决方案是有原因的。不过,我现在必须重新审视它。
    • 是的,top 是一个自引用,因此是一个关系。严格来说,我不修改它——只是初始化。此外,它是单向的,因此不存在相反的依赖关系,它不引用除 self 之外的其他实体。根据您的报价规范具有“一般”,这意味着它不是严格的定义。我相信在这种情况下,如果有的话,移植风险非常低。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-05-02
    • 1970-01-01
    • 1970-01-01
    • 2012-11-25
    • 1970-01-01
    • 2023-03-05
    相关资源
    最近更新 更多