javaspringhibernateshared-primary-key

@PrimaryKeyJoinColumn not picking the shared key


I got the @PrimaryKeyJoinColumn working before , now I'm trying with spring boot and I can't figure what I'm missing , it's very weird as it seems I have done everything right :

Person class :

    @Table(name = "PERSON")
@Entity

@Getter
@Setter
public class Person {

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


    @Column(name = "NAME")
    private String name;

    @OneToOne(cascade = CascadeType.PERSIST)
    @PrimaryKeyJoinColumn
    private Department department;
}

Department class :

    @Table(name = "DEPARTMENT")
@Entity
@Getter
@Setter
public class Department {

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

    @Column(name = "NAME")
    private String name;


    @OneToOne(mappedBy = "department")
    private Person person;
}

The service class :

   @Transactional
    public void addPerson() {
        Person person = new Person();
        person.setName("person 1");
        Department department = new Department();
        department.setName("test");
        department.setPerson(person);
        person.setDepartment(department);
        personRepository.save(person);
    }

That's what I get in the DB : Person table :

ID   Name
13  person 1

Department table :

ID  Name
18  test

Desired output : both ID should be identical ( Person ID should be 18 not 13) any idea ?


Update: hibernate always tries to insert Person without an ID , so if I remove auto increment from Person ID and tries to insert the person with existing department I get :

Hibernate: insert into person (name) values (?)

Field 'id' doesn't have a default value

Update 2: it seems the @PrimaryKeyJoinColumn won't handle the ID generation to be as the department class, so I need to use generator. I wonder because the same annotation works in Inheritance joined without doing anything on the ID of subclass. So I'm expecting an answer explaining why ID generation works in Inheritance joined , while OneToOne needs a generator


Solution

  • Although it is at least bad naming to make whole Department for just ONE Person (Department usually groups many people), I assume you really are looking for OneToOne relation with shared PRIMARY KEY.

    The best solution (optimal memory, speed and maintenance) is to use @MapsId:

    @Getter
    @Setter
    @Entity
    public class Person {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
    
        private String name;
    
        @OneToOne(mappedBy = "person", fetch = FetchType.LAZY)
        private Department department;
    }
    
    @Getter
    @Setter
    @Entity
    public class Department {
        @Id // Note no generation
        private Long id;
    
        private String name;
    
        @OneToOne(fetch = FetchType.LAZY)
        @MapsId
        @JoinColumn(name = "id", foreignKey = @ForeignKey(name = "department_belongs_to_person"))
        private Person person;
    }
    

    Here Department is owning relation (owning usually means that it would have column with FK in database, but here they just share primary key) and owns FK that binds it's PK to one generated by Person.

    Note that relation is OneToOne, bidirectional, with shared PK as FK. This is considered best practice, but has one small drawback of having to write one getter. :)

    Sources: https://vladmihalcea.com/the-best-way-to-map-a-onetoone-relationship-with-jpa-and-hibernate/

    Also - I highly recommend spending few days reading posts on this site and even implement few of them before designing anything bigger than few tables. :)

    EDIT

    I might be wrong here (I am sure now - look comments), but to my knowledge keeping IDs equal is not something JPA specifies. What it can specify is basically:

    "Hey, you guys (Person, Department) are brothers (OneToOne) and both know it (bidirectional with mappedBy="person" in Person entity). You will be older bro that will generate IDs (Person has @GeneratedValue), and you will be youger that should have same. You will use those fields (IDs, one generated, second not) to connect (@PrimaryKeyJoinColumn)."

    What I am trying to say is that just because you say "this connects you", it doesn't mean those are synchronized - YOU have to ensure it.

    Now as to how to ensure it - @MapsId is known to be best.

    If you are looking for different approaches there is also manually setting ID to be same as other with #setDepartment(Department) where you would set Department's ID to be same as calling Person (but this only works if said Person already has an ID generated, which basically breaks the idea).

    The other known to me is using foreign generator strategy.

    Person:

    @Id
    @GeneratedValue
    private long id;
    
    @OneToOne(mappedBy="person")
    private Department department;
    

    Department:

    @Id
    @GeneratedValue(generator="gen")
    @GenericGenerator(name="gen", strategy="foreign", parameters=@Parameter(name="property", value="person"))
    private long id;
    
    @OneToOne
    @PrimaryKeyJoinColumn
    private Person person;
    

    Now - this uses hibernate-specific annotations and is not pure JPA.