14

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!

axtavt
  • 239,438
  • 41
  • 511
  • 482
Korgen
  • 5,191
  • 1
  • 29
  • 43
  • I have not tried it, but it might perhaps work since you put annotations on getters, which should tell Hibernate to populate your POJOs using the setters. You could try to set the child ID in the setParentId() and setChild() methods (if it doesn't already have one). – JB Nizet Jan 21 '11 at 08:08
  • Nope, that does not work: IdentifierGenerationException: ids for this class must be manually assigned before calling save() – Korgen Jan 21 '11 at 08:30
  • Which version of Hibernate do you use? – axtavt Jan 21 '11 at 13:22
  • @axtavt: I had a look into the maven poms. Apparently there are multiple hibernate versions there, being 3.2.6ga and 3.3.0.SP1. It will take me a while figuring out the used version as the maven setup in this project is kind of cumbersome. I'll keep you posted – Korgen Jan 24 '11 at 07:41
  • You have to remove the cascade property in @OneToOne in parent. see my answer for uni directional one-to-one relation (you have to handle the cascade operations manually). Also adding child alone will result in exception as the id is not assigned to it.http://stackoverflow.com/questions/7892571/hibernate-code-exception-in-onetoone-mapping/12318513#12318513 – srikanth yaradla Sep 07 '12 at 16:00

3 Answers3

5

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.

Tom Tresansky
  • 19,364
  • 17
  • 93
  • 129
  • OP does not want reference of parent in child. He wanted unidirectional one-to-one solution which i think has to be handled manually. My thoughts here http://stackoverflow.com/questions/7892571/hibernate-code-exception-in-onetoone-mapping/12318513#12318513 – srikanth yaradla Sep 07 '12 at 16:01
  • Ah, whoops. That bit about wanting it to be unidirectional failed to register for some reason. – Tom Tresansky Sep 07 '12 at 17:24
  • 2
    This example shows the PrimaryKeyJoinColumn annotation on the parent, but everywhere else on the web I see this used only in the child. Still puzzling about that. – Tom Lianza Nov 09 '12 at 18:59
  • [This tutorial](http://websystique.com/hibernate/hibernate-one-to-one-unidirectional-with-shared-primary-key-annotation-example/) gives a very clear way of achieving what was asked: unidirectional one-to-one with shared primary key – Lewis Munene Dec 13 '20 at 19:01
  • Thats is not correct because the request question was map unidirectional. Why the child must know the parent? It's not necessary, always I avoid coupling beetween classes. – Daniel Rch. Apr 15 '21 at 16:58
3

because that one needs to have a back reference to the parent from child which is not desirable

Well, if a Child can only exist if there's a Parent, then there is a relationship between them. You may just not want to express in OO, but it does exist in the relational model.

That said, I would say that the natural solution for this is to have a Parent in the Child.

But if you really don't want to do that, I would suggest taking a look at mapping the ID as a PK class, and share them with both classes, using an @EmbeddedId. I'm pretty sure it would solve your problem, with one exception:

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

If you decide to use the @EmbeddedId approach in a PK class, I think you'll need to handle the above case as a "business rule".

jpkroehling
  • 13,881
  • 1
  • 37
  • 39
  • 1
    Partenon, thanks for your answer. Are you sure that the EmbeddedId of the child would be populated after the parent's id was received from the DB sequence? – Korgen Jan 24 '11 at 07:21
0

The solution of this problem is using @PostPersist annotation on the parent Entity. You have to create a method in the parent entity class and annotate it with @PostPersist, so this method will be invoked after parent entity is persist, and in this method just set the id of the child entity class. See the example below.

@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
    }
}
Vivek Singh
  • 646
  • 3
  • 10
  • 25