JPA / Hibernate 单向一对一映射共享主键

15

我在尝试使用JPA(提供者:Hibernate)建立一个单向一对一的关系时遇到了很大的困难。我认为这不应该太麻烦,但显然JPA / Hibernate并不同意 ;-)

问题在于,我必须映射一个我无法更改的旧架构,并且此架构在两个实体之间使用共享主键,同时这个主键又是一个实体的外键。

我创建了一个简单的测试案例:

数据库如下:

CREATE TABLE PARENT (PARENT_ID Number primary key, Message varchar2(50));

CREATE TABLE CHILD (CHILD_ID Number primary key, Message varchar2(50),
CONSTRAINT FK_PARENT_ID FOREIGN KEY (CHILD_ID )REFERENCES PARENT (PARENT_ID));

CREATE SEQUENCE SEQ_PK_PARENT START WITH 1 INCREMENT BY 1 ORDER;

父实体(即一对一关系的拥有方)如下所示:

@Entity
@Table(name = "PARENT")
public class Parent implements java.io.Serializable {       
    private Long parentId;
    private String message;
    private Child child;

    @Id
    @Column(name = "PARENT_ID", unique = true, nullable = false, precision = 22, scale = 0)
    @SequenceGenerator(name="pk_sequence", sequenceName="SEQ_PK_PARENT")
    @GeneratedValue(generator="pk_sequence", strategy=GenerationType.SEQUENCE)
    public Long getParentId() {
        return this.parentId;
    }

    public void setParentId(Long parentId) {
        this.parentId = parentId;
    }

    @Column(name = "MESSAGE", length = 50)
    public String getMessage() {
        return this.message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    @OneToOne (cascade = CascadeType.ALL)
    @PrimaryKeyJoinColumn(name="PARENT_ID", referencedColumnName="CHILD_ID")
    public Child getTestOneToOneChild() {
        return this.child;
    }

    public void setTestOneToOneChild(Child child) {
        this.child = child;
    }
}

孩子:

@Entity
@Table(name = "TEST_ONE_TO_ONE_CHILD", schema = "EXTUSER")
public class Child implements java.io.Serializable {    
    private static final long serialVersionUID = 1L;
    private Long childId;       

    private String message;

    public Child() {
    }

    public Child(String message) {
        this.message = message;
    }

    @Id
    @Column(name = "CHILD_ID")    
    public Long getChildId() {
        return this.childId;
    }

    public void setChildId(Long childId) {
        this.childId = childId;
    }

    @Column(name = "MESSAGE", length = 50)
    public String getMessage() {
        return this.message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}

我完全理解JPA不知道如何为子对象分配ID的问题。然而,我也尝试使用Hibernate的“foreign”键生成器,但也没有成功,因为这需要从子对象中引用父对象,这是不可取的。

在我看来,这个问题并不太罕见,那么我错过了什么?有解决方案吗?如果纯JPA无法提供解决方案,我也可以使用Hibernate扩展。

我对正确行为的期望是:

  1. 从序列中获取ID,并将其设置到父对象上
  2. 持久化父对象
  3. 将父对象的ID设置到子对象上
  4. 持久化子对象

如果我尝试持久化一个“独立”的子对象(例如entityManager.persist(aChild)),我希望会抛出RuntimeException异常。

非常感谢任何帮助!


我没有尝试过,但是它可能会起作用,因为你在getter上放置了注释,这应该告诉Hibernate使用setter来填充你的POJO。你可以尝试在setParentId()和setChild()方法中设置子ID(如果还没有设置)。 - JB Nizet
不行,这不起作用:IdentifierGenerationException:在调用save()之前必须手动分配此类的ID。 - Korgen
你使用的是哪个版本的Hibernate? - axtavt
我查看了Maven POM文件。显然,那里有多个Hibernate版本,分别是3.2.6ga和3.3.0.SP1。由于该项目中的Maven设置有些麻烦,所以我需要一些时间来确定使用的版本。我会随时向您报告。 - Korgen
你必须从父类中删除@OneToOne的级联属性。请查看我的答案,了解单向一对一关系(您必须手动处理级联操作)。此外,仅添加子项将导致异常,因为未为其分配ID。https://dev59.com/JlzUa4cB1Zd3GeqP4ZPD#12318513 - srikanth yaradla
3个回答

5
对于你描述的数据库模式,你可以在依赖类(即Child类)上使用@MapsId注释来实现映射回父类,如下所示:
@Entity
class Parent {
  @Id
  @Column(name = "parent_id")
  @GeneratedValue 
  Long parent_id;
}

@Entity
class Child {
  @Id
  @Column(name = "child_id")
  Long child_id;

  @MapsId 
  @OneToOne
  @JoinColumn(name = "child_id")
  Parent parent;
}

在父子关系中添加映射,您需要使用@PrimaryKeyJoinColumn注释,就像您列出的那样,使完整的双向一对一映射如下所示:

@Entity
class Parent {
  @Id
  @Column(name = "parent_id")
  @GeneratedValue 
  Long parent_id;

  @OneToOne
  @PrimaryKeyJoinColumn(name="parent_id", referencedColumnName="child_id")
  public Child;
}

@Entity
class Child {
  @Id
  @Column(name = "child_id")
  Long child_id;

  @MapsId 
  @OneToOne
  @JoinColumn(name = "child_id")
  Parent parent;
}

我使用了字段而不是方法访问(并删除了与关系无关的任何内容),但是应将相同的注释应用于您的getter。
另请参见第2.2.3.1节的最后一部分here,以获取@MapsId的另一个示例。

1
OP不希望在子类中引用父类。他想要单向一对一的解决方案,我认为这需要手动处理。我的想法在这里:https://dev59.com/JlzUa4cB1Zd3GeqP4ZPD#12318513 - srikanth yaradla
啊,糟糕。关于想要它是单向的那一点,出于某种原因没有被记录下来。 - Tom Tresansky
2
这个例子展示了在父类上使用PrimaryKeyJoinColumn注解,但是在网上其他地方我只看到它在子类中使用。仍然对此感到困惑。 - Tom Lianza
1
这个教程提供了一个非常清晰的方法来实现所要求的内容:使用共享主键的单向一对一关系。 - Lewis Munene
1
这不正确,因为请求问题是单向映射。为什么子类必须知道父类?这并不是必要的,我总是避免类之间的耦合。 - Daniel Rch.

3
由于需要从子级到父级进行反向引用,这并不理想。
如果一个Child只有在有Parent的情况下才存在,那么它们之间就存在关系。你可能只是不想以面向对象的方式表达它,但在关系模型中确实存在。
也就是说,我会建议在Child中加入一个Parent,这是一种自然的解决方案。
如果你真的不想这样做,我会建议考虑将ID作为PK类映射,并使用@EmbeddedId将其与两个类分享。我非常确定它可以解决你的问题,但有一个例外:
如果我尝试持久化“独立”的Child(例如entityManager.persist(aChild)),我会期望抛出RuntimeException。
如果你决定在PK类中使用@EmbeddedId方法,我认为你需要将上述情况视为“业务规则”。

1
Partenon,感谢您的答复。您确定在从数据库序列获取父级ID后,子级的EmbeddedId将被填充吗? - Korgen

0
这个问题的解决方案是在父实体上使用@PostPersist注释。 您需要在父实体类中创建一个方法,并用@PostPersist进行注释,这样在父实体持久化后将调用此方法,并在此方法中仅设置子实体类的ID。请参见下面的示例。
@Entity
@Table(name = "PARENT")
public class Parent implements java.io.Serializable {       
    private Long parentId;
    private String message;
    private Child child;

    @Id
    @Column(name = "PARENT_ID", unique = true, nullable = false, precision = 22, scale = 0)
    @SequenceGenerator(name="pk_sequence", sequenceName="SEQ_PK_PARENT")
    @GeneratedValue(generator="pk_sequence", strategy=GenerationType.SEQUENCE)
    public Long getParentId() {
        return this.parentId;
    }

    public void setParentId(Long parentId) {
        this.parentId = parentId;
    }

    @OneToOne (cascade = CascadeType.ALL)
    @PrimaryKeyJoinColumn
    public Child getTestOneToOneChild() {
        return this.child;
    }

    public void setTestOneToOneChild(Child child) {
        this.child = child;
    }


   @PostPersist
    public void initializeCandidateDetailID()
    {
        System.out.println("reached here");// for debugging purpose
        this.child.setChildId(parentId); // set child id here
        System.out.println("reached here"+Id); // for debugging purpose
    }
}

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