【问题标题】:@OneToMany and composite primary keys?@OneToMany 和复合主键?
【发布时间】:2011-02-06 09:14:25
【问题描述】:

我正在使用带有注释的 Hibernate(在 spring 中),并且我有一个具有有序、多对一关系的对象,它的子对象具有复合主键,其中一个组件是外键回到父对象的id。

结构看起来像这样:

+=============+                 +================+
| ParentObj   |                 | ObjectChild    |
+-------------+ 1          0..* +----------------+
| id (pk)     |-----------------| parentId       |
| ...         |                 | name           |
+=============+                 | pos            |
                                | ...            |
                                +================+

我尝试了多种注释组合,但似乎都不起作用。这是我能想到的最接近的:

@Entity
public class ParentObject {
    @Column(nullable=false, updatable=false)
    @Id @GeneratedValue(generator="...")
    private String id;

    @OneToMany(mappedBy="parent", fetch=FetchType.EAGER, cascade={CascadeType.ALL})
    @IndexColumn(name = "pos", base=0)
    private List<ObjectChild> attrs;

    ...
}

@Entity
public class ChildObject {
    @Embeddable
    public static class Pk implements Serializable {
        @Column(nullable=false, updatable=false)
        private String parentId;

        @Column(nullable=false, updatable=false)
        private String name;

        @Column(nullable=false, updatable=false)
        private int pos;

        @Override
        public String toString() {
            return new Formatter().format("%s.%s[%d]", parentId, name, pos).toString();
        }

        ...
    }

    @EmbeddedId
    private Pk pk;

    @ManyToOne
    @JoinColumn(name="parentId")
    private ParentObject parent;

    ...
}

经过长时间的实验后,我得出了这个结论,在这些实验中,我的大多数其他尝试都产生了由于各种原因休眠甚至无法加载的实体。

更新:感谢大家的cmets;我已经取得了一些进展。我做了一些调整,我认为它更接近(我已经更新了上面的代码)。但是,现在问题在于插入。父对象似乎保存得很好,但子对象没有保存,我能够确定的是休眠没有填写子对象的(复合)主键的 parentId 部分,所以我m 得到一个非唯一错误:

org.hibernate.NonUniqueObjectException:
   a different object with the same identifier value was already associated 
   with the session: [org.kpruden.ObjectChild#null.attr1[0]]

我在自己的代码中填充了namepos 属性,但是我当然不知道父ID,因为它还没有保存。关于如何说服 hibernate 填写此内容的任何想法?

谢谢!

【问题讨论】:

  • 祝你好运。由于 Hibernate 的支持不佳,我参与的一个项目不得不完全放弃复合键,转而使用合成键。
  • 这个异常可能是你使用List引起的。您必须使用 Set 通过使用 hashCode 和 equals 定义来避免键重复。 Hibernate 将列表中的两个“逻辑上相同”的元素视为两个元素,然后引发异常。

标签: java hibernate one-to-many annotations composite-key


【解决方案1】:

Manning 书籍Java Persistence with Hibernate 在第 7.2 节中有一个示例概述了如何执行此操作。幸运的是,即使您没有这本书,您也可以通过下载 Caveat Emptor 示例项目的 JPA 版本(直接链接 here)并检查 Category 和 @987654325 类来查看此源代码示例@在auction.model 包中。

我还将总结下面的关键注释。如果仍然不行,请告诉我。

父对象:

@Entity
public class ParentObject {
   @Id @GeneratedValue
   @Column(name = "parentId", nullable=false, updatable=false)
   private Long id;

   @OneToMany(mappedBy="parent", fetch=FetchType.EAGER)
   @IndexColumn(name = "pos", base=0)
   private List<ChildObject> attrs;

   public Long getId () { return id; }
   public List<ChildObject> getAttrs () { return attrs; }
}

子对象:

@Entity
public class ChildObject {
   @Embeddable
   public static class Pk implements Serializable {
       @Column(name = "parentId", nullable=false, updatable=false)
       private Long objectId;

       @Column(nullable=false, updatable=false)
       private String name;

       @Column(nullable=false, updatable=false)
       private int pos;
       ...
   }

   @EmbeddedId
   private Pk id;

   @ManyToOne
   @JoinColumn(name="parentId", insertable = false, updatable = false)
   @org.hibernate.annotations.ForeignKey(name = "FK_CHILD_OBJECT_PARENTID")
   private ParentObject parent;

   public Pk getId () { return id; }
   public ParentObject getParent () { return parent; }
}

【讨论】:

  • 遗憾的是,这并没有帮助:我得到了同样的错误。我看到的唯一变化是添加了 @ForeignKey 属性,以及在父属性上添加了 {insertable,updatable}=false。还有什么我没看到的吗?谢谢!
  • 还有其他一些变化。 (1)尝试将parentId数据类型改为Long; (2) 我认为getter是必要的; (3) 非常关注ParentObject#id、ParentObject#attrs、ChildObject.Pk#objectId和ChildObject#parent的列名; (4) ParentObject 的 id 字段需要@Id 注释(如果您不手动提供 id,则需要 @GeneratedValue); (5) 我将 ChildObject#pk 重命名为 ChildObject#id,虽然我不认为这个改变是必要的。如果你掌握了所有这些,你能提供一些周围上下文的阅读代码吗?
  • Re (2) 上面:getter 和 setter 不是必需的。 Hibernate 可以构建代理来做需要做的事情。我觉得最好考虑一下这种“魔法”。
  • @John:感谢您对 getter/setter 的澄清。 @Kris:我也不认为 parentId 字段实际上 需要 是 Long;我只是想尽量减少我的工作代码和你的代码之间的差异。
  • 您必须使用 Set 来避免使用 hashCode 和 equals 定义的键重复。 Hibernate 将列表中的两个“逻辑相同”元素视为两个元素
【解决方案2】:

您应该将 ParentObject 引用合并到 ChildObject.Pk 中,而不是单独映射 parent 和 parentId:

(getter、setter、Hibernate 属性与问题无关,省略成员访问关键字)

class ChildObject { 
    @Embeddable
    static class Pk {
        @ManyToOne...
        @JoinColumn(name="parentId")
        ParentObject parent;

        @Column...
        String name...
        ...
    }

    @EmbeddedId
    Pk id;
}

ParentObject 中输入@OneToMany(mappedBy="id.parent") 即可。

【讨论】:

    【解决方案3】:

    首先,在ParentObject 中,“修复”应设置为"parent"mappedBy 属性。另外(但这可能是一个错字)添加一个@Id 注释:

    @Entity
    public class ParentObject {
        @Id
        @GeneratedValue
        private String id;
    
        @OneToMany(mappedBy="parent", fetch=FetchType.EAGER)
        @IndexColumn(name = "pos", base=0)
        private List<ObjectChild> attrs;
    
        // getters/setters
    }
    

    然后,在ObjectChild 中,将name 属性添加到复合键中的objectId

    @Entity
    public class ObjectChild {
        @Embeddable
        public static class Pk implements Serializable {
            @Column(name = "parentId", nullable = false, updatable = false)
            private String objectId;
    
            @Column(nullable = false, updatable = false)
            private String name;
    
            @Column(nullable = false, updatable = false)
            private int pos;
        }
    
        @EmbeddedId
        private Pk pk;
    
        @ManyToOne
        @JoinColumn(name = "parentId", insertable = false, updatable = false)
        private ParentObject parent;
    
        // getters/setters
    
    }
    

    AND还将insertable = false, updatable = false 添加到@JoinColumn,因为我们在此实体的映射中重复parentId 列。

    通过这些更改,持久化和读取实体对我来说工作正常(使用 Derby 测试)。

    【讨论】:

    • 只花了半个小时试图解决这个问题,我的问题是我的旧数据库在表中的外键为“forecastId”,这就是我复制到我的 JPA 注释中的内容,原因未知, Spring 决定因此该列在数据库中必须是“forecast_id”,因此它倒下了。必须是一种算法来自动识别稍有错误的列。我将@Column(name= 更改为“forecastid”(全部小写),Spring 决定采取行动。
    【解决方案4】:

    经过多次试验和挫折,我最终确定我不能完全按照自己的意愿行事。

    最终,我继续为子对象提供了自己的合成密钥,并让 Hibernate 管理它。这并不理想,因为密钥几乎和其他数据一样大,但它可以工作。

    【讨论】:

    • '@JoinColumns({ @JoinColumn(name="userfirstname_fk", referencedColumnName="firstName"), @JoinColumn(name="userlastname_fk", referencedColumnName="lastName") })'
    • 一个好决定。尝试在 Hibernate 中做除了基本的东西之外的任何事情都需要太多不直观的混乱。我通常也会这样做——给所有东西一个主要的——它让 Hibernate 更容易理解。
    【解决方案5】:

    发现这个问题正在寻找它的问题的答案,但它的答案并没有解决我的问题,因为我正在寻找 @OneToMany,它不适合我所追求的表结构。 @ElementCollection 适合我的情况。我相信其中的一个问题是,它将整个关系行视为唯一的,而不仅仅是行 ID。

    @Entity
    public class ParentObject {
    @Column(nullable=false, updatable=false)
    @Id @GeneratedValue(generator="...")
    private String id;
    
    @ElementCollection
    @CollectionTable( name = "chidren", joinColumns = @JoinColumn( name = "parent_id" ) )
    private List<ObjectChild> attrs;
    
    ...
    }
    
    @Embeddable
    public static class ObjectChild implements Serializable {
        @Column(nullable=false, updatable=false)
        private String parentId;
    
        @Column(nullable=false, updatable=false)
        private String name;
    
        @Column(nullable=false, updatable=false)
        private int pos;
    
        @Override
        public String toString() {
            return new Formatter().format("%s.%s[%d]", parentId, name, pos).toString();
        }
    
        ... getters and setters REQUIRED (at least they were for me)
    }
    

    【讨论】:

      【解决方案6】:

      看来你已经很接近了,我正在尝试在我当前的系统中做同样的事情。我从代理键开始,但想删除它以支持由父主键和列表中的索引组成的复合主键。

      通过使用“外部”生成器,我能够获得共享主表中 PK 的一对一关系:

      @Entity
      @GenericGenerator(
          name = "Parent",
          strategy = "foreign",
          parameters = { @Parameter(name = "property", value = "parent") }
      )
      public class ChildObject implements Serializable {
          @Id
          @GeneratedValue(generator = "Parent")
          @Column(name = "parent_id")
          private int parentId;
      
          @OneToOne(mappedBy = "childObject")
          private ParentObject parentObject;
          ...
      }
      

      不知道是否可以添加@GenericGenerator 和@GeneratedValue 来解决Hibernate 在插入过程中不分配父级新获取的PK 的问题。

      【讨论】:

        【解决方案7】:

        保存 Parent 对象后,您必须在 Child 对象中显式设置 parentId 才能使 Child 对象上的插入工作。

        【讨论】:

          【解决方案8】:

          花了三天时间,我想我找到了解决方案,但说实话,我不喜欢它,它肯定可以改进。但是,它有效并解决了我们的问题。

          这是您的实体构造函数,但您也可以在 setter 方法中执行此操作。 另外,我使用了一个 Collection 对象,但它应该与 List 相同或相似:

          ...
          public ParentObject(Collection<ObjectChild> children) {
              Collection<ObjectChild> occ = new ArrayList<ObjectChild>();
              for(ObjectChild obj:children){
                  obj.setParent(this);
                  occ.add(obj);
              }
              this.attrs = occ;
          }
          ...
          

          基本上,正如其他人建议的那样,我们必须先手动设置所有孩子的父母ID,然后再保存父母(连同所有孩子)

          【讨论】:

            【解决方案9】:

            我一直在寻找答案,但找不到有效的解决方案。虽然我在父母中正确地拥有了 OneToMany 并且在孩子中正确地拥有了 ManyToOne,但在父母的保存期间,孩子的密钥没有被分配,这是来自父母的自动生成的值。

            在子实体(Java 类)的 @ManyToOne 映射上方添加注释 javax.persistence.MapsId 后,我的问题得到了解决

            @MapsId("java_field_name_of_child's_composite_key_that_needs_the_value_from_parent")
            @ManyToOne(fetch = FetchType.LAZY)
            @JoinColumn(name = "PARENT_ID", nullable = false, insertable = false, updatable = false)
            private Parent parent;
            

            这是@Pascal Thivent 回答的内容之上(2010 年 4 月 10 日 1:40 回答)

            请参阅本帖前面的帖子中的示例代码 sn-p。

            谢谢, PJR。

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 2021-10-07
              • 2011-11-26
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2017-08-05
              相关资源
              最近更新 更多