Search code examples
javahibernatespring-data-jpahibernate-cascade

Using composite identifiers @EmbeddedId with @OneToOne


My example below is for learning purpose. The goal is to create some examples using the Composite identifiers. Unfortunately, I'm getting some strange results.

Entities: Course, Teacher, CoursePk.

Each course should be given by only one Teacher.

I'm using a bidirectional relation @OneToOne() between the class Teacher and the class Course.

Each Course contains a composite primary key. In my example I'm simplifying and I'm using a composite primary key that use only one attribute.

The goal of my example is to persist the associated teacher when the course object is persisted.

The Teacher Class:

@Entity(name = "Teacher")
public class Teacher {

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

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

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

    @OneToOne(mappedBy = "officialTeacher")
    private Course course;

    //setter getter constructors are omitted for brevity
}

The Course class:

@Entity(name = "Course")
public class Course {

    @EmbeddedId
    private CoursePk pkCourse;

    @OneToOne(cascade = CascadeType.PERSIST)
    private Teacher officialTeacher;

    //setter getter constructors are omitted for brevity
}

The CoursePk that should represents a composite primary key.

@Embeddable
public class CoursePk implements Serializable {

    @Column(name = "courseName")
    private Integer courseId;
}

My running example:

private void composedPrimaryKey() {
        Teacher teacher = new Teacher("Jeff", "Boss");
        CoursePk composedPK = new CoursePk(1);
        Course course = new Course(composedPK, teacher);
        Course savedCourse = courseRepository.save(course);
    }

With that my tables looks like:

course table

course_name | official_teacher_id
     1      |      1

teacher table

id  | first_name |last_name
 1  |   null     |  null

As you can see, the information of the teacher are not persisted, but only the id field.

Magically when I change the cascade type to CascadeType.ALL, everything is persisted.

id  | first_name |last_name
 1  |   Jeff     |  Boss

Could someone explains why it didn't works with only CascadeType.PERSIST.


Solution

  • Spring Data JPA: CrudRepository.save(…) method behaviour

    Given that Spring Data JPA is used, there is some specifics on how the CrudRepository.save(…) method detects the method call to be performed: either EntityManager.persist(…) or EntityManager.merge(…):

    5.2.1. Saving Entities

    Saving an entity can be performed with the CrudRepository.save(…) method. It persists or merges the given entity by using the underlying JPA EntityManager. If the entity has not yet been persisted, Spring Data JPA saves the entity with a call to the entityManager.persist(…) method. Otherwise, it calls the entityManager.merge(…) method.

    Entity State-detection Strategies

    Spring Data JPA offers the following strategies to detect whether an entity is new or not:

    • Id-Property inspection (default): By default Spring Data JPA inspects the identifier property of the given entity. If the identifier property is null, then the entity is assumed to be new. Otherwise, it is assumed to be not new.

    <…>

    Spring Data JPA - Reference Documentation.

    Back to the question

    Coming back to the piece of code mentioned in the question.
    Since the identifier property (the primary key) of the Course instance is not null, Spring Data JPA considers it as an existing (not new) entity and calls the EntityManager.merge(…) method instead of the EntityManager.persist(…) method.

    Additional references

    Additionally, please, refer to the related question: java - JPA CascadeType Persist doesn't work with spring data.