JPA / Hibernate unidirectional one-to-one mapping with shared primary key

Korgen picture Korgen · Jan 21, 2011 · Viewed 16.6k times · Source

I'm having a very hard time trying to get a unidirectional one-to-one relationship to work with JPA (Provider: Hibernate). In my opinion this should not be too much of a hassle but apparently JPA / Hibernate disagrees on that ;-)

The problem is that I have to map a legacy schema which I cannot change and that this schema uses a shared primary key between two entities which at the same time is the foreign key for one entity.

I created a simple TestCase:

DB looks as follows:

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;

The parent(=owning side of one-to-one) looks as follows:

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

The 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;
    }
}

I totally see the problem that JPA does not know how to assign the id for the child. However I also tried using Hibernates "foreign" key Generator with also no success because that one needs to have a back reference to the parent from child which is not desirable. This problem does not seem too uncommon to me, so what am I missing here? Is there a solution at all? I can also use hibernate extensions if pure JPA does not provide a solution.

My expectations for a correct behavior would be: If I try to persist the parent with a child attached:

  1. get ID from sequence, set it on the parent
  2. persist parent
  3. set parent's ID on child
  4. persist child

If I try to persist a "standalone" child (e.g. entityManager.persist(aChild)) I would expect a RuntimeException.

Any help is greatly appreciated!

Answer

Tom Tresansky picture Tom Tresansky · Sep 7, 2012

For the db schema you described, you can use @MapsId annotation on the dependent class (your Child class) to achieve the mapping back to the parent, like so:

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

Adding the mapping from parent to child you use the @PrimaryKeyJoinColumn annotation as you had listed, making the complete bi-directional one-to-one mapping look like this:

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

I used field rather than method access (and removed anything extraneous to the relationships), but it would be the same annotations applied to your getters.

Also see the last bit of section 2.2.3.1 here for another example of @MapsId.