78

I’m working with an existing schema that I’d rather not change. The schema has a one-to-one relationship between tables Person and VitalStats, where Person has a primary key and VitalStats uses the same field as both its primary key and its foreign key to Person, meaning its value is the value of the corresponding PK of Person.

These records are created by external processes, and my JPA code never needs to update VitalStats. For my object model I’d like my Person class to contain a VitalStats member, BUT:

When I try

@Entity
public class Person{
    private long id;
    @Id
    public long getId(){ return id; }

    private VitalStats vs;
    @OneToOne(mappedBy = “person”)
    public VitalStats getVs() { return vs; }
}

@Entity
    public class VitalStats{
     private Person person;
    @OneToOne
    public Person getPerson() { return person; }
}

I have the problem that VitalStats lacks an @Id, which doesn’t work for an @Entity.\

If I try

@Id @OneToOne
public Person getPerson() { return person; }

that solves the @Id problem but requires that Person be Serializable. We’ll get back to that.

I could make VitalStats @Embeddable and connect it to Person via an @ElementCollection, but then it would have to be accessed as a collection, even though I know that there’s only one element. Doable, but both a little bit annoying and a little bit confusing.

So what’s preventing me from just saying that Person implements Serializable? Nothing, really, except that I like everything in my code to be there for a reason, and I can’t see any logic to this, which makes my code less readable.

In the meantime I just replaced the Person field in VitalStats with a long personId and made that VitalStats’s @Id, so now the @OneToOne works.

All of these solutions to what seems (to me) like a straightforward issue are a bit clunky, so I’m wondering whether I’m missing anything, or whether someone can at least explain to me why Person has to be Serializable.

TIA

Michael
  • 1,351
  • 1
  • 11
  • 25

2 Answers2

108

To map one-to-one association using shared primary keys use @PrimaryKeyJoinColumn and @MapsId annotation.

Relevant sections of the Hibernate Reference Documentation:

PrimaryKeyJoinColumn

The PrimaryKeyJoinColumn annotation does say that the primary key of the entity is used as the foreign key value to the associated entity.

MapsId

The MapsId annotation ask Hibernate to copy the identifier from another associated entity. In the Hibernate jargon, it is known as a foreign generator but the JPA mapping reads better and is encouraged

Person.java

@Entity
public class Person {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "person_id")
    private Long id;

    @OneToOne(cascade = CascadeType.ALL)
    @PrimaryKeyJoinColumn
    private VitalStats vitalStats;       
}

VitalStats.java

@Entity
public class VitalStats 
{
    @Id @Column(name="vitalstats_id") Long id;

    @MapsId 
    @OneToOne(mappedBy = "vitalStats")
    @JoinColumn(name = "vitalstats_id")   //same name as id @Column
    private Person person;

    private String stats;
}

Person Database Table

CREATE TABLE  person (
  person_id   bigint(20) NOT NULL auto_increment,
  name        varchar(255) default NULL,
  PRIMARY KEY  (`person_id`)
) 

VitalStats Database Table

CREATE TABLE  vitalstats 
(
  vitalstats_id  bigint(20) NOT NULL,
  stats          varchar(255) default NULL,
  PRIMARY KEY  (`vitalstats_id`)
)
s.alem
  • 12,579
  • 9
  • 44
  • 72
Joel Hudon
  • 3,145
  • 1
  • 20
  • 13
  • Thanks, but I'm afraid this doesn't really help. You've put an id field in VitalStats in addition to the Person field, whereas I can't add a new field to the schema and have to use the FK that underlies 'person' as the PK of VitalStats. That was the whole point. – Michael Jul 27 '11 at 09:25
  • 2
    No need to add another column because of the MapById annotation on the person attribute. The exemple i post was not really clear, i modify the exemple to add more details to VitalStats (See #JoinColumn on the person attribute). I add the VitalStats and Person database table and you can see that the VitalStats table contain only a PK that is the FK for person. I think is the same as what you want? If it's not what you want can you add more details on the table layout ? @MapId has the advantage that the person entity does not need to be serializable. I updated the exemple. – Joel Hudon Jul 28 '11 at 12:53
  • @JoelHudon How did you know to put the `@JoinColumn` annotation in place? – Kevin Bowersox Nov 15 '14 at 22:18
  • I tried this but I get an error like - "trying to store a null one-to-one property from person". The only difference between the above snippet and mine is I used a sequence for generating the primary key in person entity. – kondu Apr 14 '15 at 05:46
  • I solved my above issue by setting the parent object in child as described in Person.java – kondu Apr 14 '15 at 08:05
  • Merci Joël. Thank you for also including the sql definition of both tables. Your answer also solved my problem. The annotations seem to be the same if you use EclipseLink as the JPA provider instead of Hibernate. – Pierre C Mar 05 '16 at 03:23
  • I'm exhausted debugging the poorly-described exception information all day, trial-and-error for every entity to find which cause the exception. Finally I found this solution to solve it. I can't thank you enough. – Nier Mar 25 '16 at 04:21
  • This does work with JPA 2.1, however you still need to manually assign the instance of a parent object to a child before saving the parent. – ievgen Apr 24 '18 at 10:00
  • I am getting error in spring boot project, here is my git repo https://github.com/m-aslam/demo – rogue lad Jun 29 '18 at 15:33
  • 2
    This case does not support optional one-to-one (attempted to assign id from null one-to-one property). – Yura Shinkarev Dec 18 '19 at 08:54
23

In my case this made the trick:

Parent class:

public class User implements Serializable {
  private static final long serialVersionUID = 1L;

  /** auto generated id (primary key) */
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(unique = true, nullable = false)
  private Long id;

  /** user settings */
  @OneToOne(cascade = CascadeType.ALL, mappedBy = "user")
  private Setting setting;
}

Child class:

public class Setting implements Serializable {
  private static final long serialVersionUID = 1L;

  /** setting id = user id */
  @Id
  @Column(unique = true, nullable = false)
  private Long id;

  /** user with this associated settings */
  @MapsId
  @OneToOne
  @JoinColumn(name = "id")
  private User user;
}
camposer
  • 5,152
  • 2
  • 17
  • 15
  • 2
    This is a correct answer to me. It creates a foreign key in the database, and this is a problem with other answers. Unfortunately, I couldn't force hibernate to do just 1 query to get entities like User & Setting. Always doing 2 queries, even if the foreign key is created and fetch=FetchType.EAGER... – dorsz Jun 18 '16 at 07:38
  • `@MapsId` should be `@MapsId("id")`. Read [the problem that I had](https://stackoverflow.com/questions/60508864/hibernate-complains-for-null-id-in-onetoone-even-if-it-is-not-null) without it. – George Z. Mar 03 '20 at 14:45