Sunday, May 4, 2014

@OneToOne with shared primary key, revisited :)

Long time ago, I wrote a post @OneToOne with shared primary key. Today I would like to return to this problem, with solution based on @MapsId annotation introduced in JPA 2.0

Again we have two entities: Primus and Secundus. Both entities have primary key using Long Java type. They are related 1-1, and Secundus should use the same primary key as Primus.
3 Years after my initial post they will look slightly different ;)
@Entity
@Table(name = "PRIMUS")
public class Primus {

    public static Primus newInstance() {
        Primus primus = new Primus();
        primus.secundus = new Secundus(primus);
        return primus;
    }

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @OneToOne(cascade = CascadeType.ALL, mappedBy = "primus")
    private Secundus secundus;

    Primus() {
        super();
    }
    ...

}
Not much changed here, ;). Really important changes were made on the Secundus:
@Entity
@Table(name = "SECUNDUS")
public class Secundus {

    @Id
    private Long id;

    @JoinColumn(name = "ID")
    @OneToOne
    @MapsId
    private Primus primus;

    Secundus() {
        super();
    }

    public Secundus(Primus primus) {
        this();
        this.primus = primus;
    }
As you see, the @PrimaryKeyJoinColumn annotation is replaced with two annotations: @MapsId, which defines that Secundus identifier will be determined by Primus identifier, and @JoinColumn, specifying which column in SECUNDUS table will be used for joining. 

Nothing more is needed :) - JPA Provider should automatically ask Primus for its identifier, when persisting Secundus, as long as you take care of correct entities correlation, like in newInstance method of Primus.

Long live JPA 2.0+ ;) :) 

13 comments:

  1. Thanks for you post, it's extremely helpful. You mention towards the end in the second last paragraph that '@JoinColumn, specifying which column in SECUNDUS table will be used for joining.'. Should this not be '@JoinColumn, specifying which column in PRIMUS table will be used for joining.'?

    I thought the JoinColumn annotation takes the column name from the entity that you can joining to, in this case the Primus entity.

    ReplyDelete
  2. I was banging my head on the keyboard, trying to solve this exact problem, until I found this post.
    Thank You very, very much.

    ReplyDelete
  3. Thank you...fixed my issue after 4 hours of trial and error.

    ReplyDelete
  4. Thank you...fixed my issue after 4 hours of trial and error.

    ReplyDelete
  5. Thanks for helping me with the fantastic solution. I am getting a org.springframework.orm.jpa.JpaSystemException: attempted to assign id from null one-to-one property [com.example.model.ApplicantAddressDetails.index]; nested exception is org.hibernate.id.IdentifierGenerationException: attempted to assign id from null one-to-one property [com.example.model.ApplicantAddressDetails.index]
    How do i resolve this ?? I have exactly as you have explained in the blog.

    ReplyDelete
  6. Thank you very much for this simple and great solution!

    ReplyDelete
  7. Please show example code to create new instances just to avoid common errors that I seem to be making :)

    Should the secundus table be defined with a foreign-key constraint? When I define that constraint I get exceptions like this:

    Cannot add or update a child row: a foreign key constraint fails

    ReplyDelete
  8. I am using hibernate-jpa-2.1. I tried with same One to One scenario with Primary entity having Auto generated Id. Now I tried using the same scenario in secondary entity( Disabled auto id generation and used MapsId). But When I save or persist data I get the following error.

    My code:

    @Entity
    @Table(name="employee")
    public class Employee {

    @Id
    @GeneratedValue
    @Column(name="empid")
    private Integer empId;

    ****etc code

    @Entity
    @Table(name="projects")
    public class Projects {

    @Id
    private Integer id;

    @JoinColumn(name = "ID")
    @OneToOne
    @MapsId
    private Employee emp;


    ****etc code


    ========================================================================


    Exception in thread "main" org.hibernate.id.IdentifierGenerationException: ids for this class must be manually assigned before calling save(): com.test.hibernate.Projects
    at org.hibernate.id.Assigned.generate(Assigned.java:33)
    at org.hibernate.event.def.AbstractSaveEventListener.saveWithGeneratedId(AbstractSaveEventListener.java:98)
    at org.hibernate.event.def.DefaultSaveOrUpdateEventListener.saveWithGeneratedOrRequestedId(DefaultSaveOrUpdateEventListener.java:186)
    at org.hibernate.event.def.DefaultSaveOrUpdateEventListener.entityIsTransient(DefaultSaveOrUpdateEventListener.java:175)
    at org.hibernate.event.def.DefaultSaveOrUpdateEventListener.performSaveOrUpdate(DefaultSaveOrUpdateEventListener.java:98)
    at org.hibernate.event.def.DefaultSaveOrUpdateEventListener.onSaveOrUpdate(DefaultSaveOrUpdateEventListener.java:70)
    at org.hibernate.impl.SessionImpl.fireSaveOrUpdate(SessionImpl.java:507)
    at org.hibernate.impl.SessionImpl.saveOrUpdate(SessionImpl.java:499)
    at org.hibernate.engine.CascadingAction$5.cascade(CascadingAction.java:218)
    at org.hibernate.engine.Cascade.cascadeToOne(Cascade.java:268)
    at org.hibernate.engine.Cascade.cascadeAssociation(Cascade.java:216)
    at org.hibernate.engine.Cascade.cascadeProperty(Cascade.java:169)
    at org.hibernate.engine.Cascade.cascade(Cascade.java:130)
    at org.hibernate.event.def.AbstractSaveEventListener.cascadeAfterSave(AbstractSaveEventListener.java:437)
    at org.hibernate.event.def.AbstractSaveEventListener.performSaveOrReplicate(AbstractSaveEventListener.java:326)
    at org.hibernate.event.def.AbstractSaveEventListener.performSave(AbstractSaveEventListener.java:180)
    at org.hibernate.event.def.AbstractSaveEventListener.saveWithGeneratedId(AbstractSaveEventListener.java:121)
    at org.hibernate.event.def.DefaultSaveOrUpdateEventListener.saveWithGeneratedOrRequestedId(DefaultSaveOrUpdateEventListener.java:186)
    at org.hibernate.event.def.DefaultSaveEventListener.saveWithGeneratedOrRequestedId(DefaultSaveEventListener.java:33)
    at org.hibernate.event.def.DefaultSaveOrUpdateEventListener.entityIsTransient(DefaultSaveOrUpdateEventListener.java:175)
    at org.hibernate.event.def.DefaultSaveEventListener.performSaveOrUpdate(DefaultSaveEventListener.java:27)
    at org.hibernate.event.def.DefaultSaveOrUpdateEventListener.onSaveOrUpdate(DefaultSaveOrUpdateEventListener.java:70)
    at org.hibernate.impl.SessionImpl.fireSave(SessionImpl.java:535)
    at org.hibernate.impl.SessionImpl.save(SessionImpl.java:523)
    at org.hibernate.impl.SessionImpl.save(SessionImpl.java:519)
    at com.test.hibernate.StoreQuery.main(StoreQuery.java:110)

    ReplyDelete
  9. I also see this behaviour.
    Does anyone have a solution?

    ReplyDelete
  10. https://hibernate.atlassian.net/browse/HHH-12436

    ReplyDelete
  11. Thanks a lot for this article...I have been struggling to resolve this issue.

    ReplyDelete