Why does Hibernate not throw an exception and save null/0 values in this OneToOne case?

Intro

Hi, I encountered a surprising behavior while using Hibernate with a @OneToOne relationship and only CascadeType.PERSIST set.
I expected throwing exception when trying to save a transient child entity without CascadeType.MERGE during a save() (which triggers merge() internally in Spring Data JPA).
But instead of throwing an exception, Hibernate silently persisted the Parent entity, and inserted a Child with all fields being either null or 0 (its ID was generated, though).

Environment
> spring-boot:3.3.12
> spring-data-jpa:3.3.12
> hibernate-core:6.5.3.final

Source Code

@Entity
class Parent(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "parent_id")
    val id: Long = 0L,

    @OneToOne(cascade = [CascadeType.PERSIST]) // <------- HERE: Only PERSIST
    @JoinColumn(name = "child_id")
    var child: Child? = null
)

@Entity
class Child(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "child_id")
    val id: Long = 0L,

    val parentId: Long,

    val name: String,
)

@Service
class MyService(
    private val parentRepository: ParentRepository
) {
    fun create(id: Long) {
        val parent = parentRepository.findById(id).orElseThrow()
        val child = Child(parentId = id, name = "test")
        parent.child = child
        parentRepository.save(parent) // <------- triggers merge()
    }
}

Result in MySQL

mysql> select * from parent;
+-----------+----------+
| parent_id | child_id |
+-----------+----------+
|         1 |       40 |
+-----------+----------+

mysql> select * from child;
+----------+-----------+------+
| child_id | parent_id | name |
+----------+-----------+------+
|       40 |         0 | NULL |
+----------+-----------+------+

As you can see, Hibernate inserted the Child, even though no cascade MERGE or explicit childRepository.save(child) was used.
The key point is that parent_id = 0 and name = null in child table.

Question

  • Why doesn’t Hibernate throw a exception or prevent child record writing?
  • Why does it silently insert a Child entity with only the ID populated and other fields null/zero?
  • Is this behavior intentional or considered a bug / limitation?

Please try updating to ORM 6.6. I think the stricter validation was only added in this version.

As you suggested, I updated Hibernate ORM to version 6.6.0.Final and tested again.

However, the result remains the same — no exception is thrown during the merge process, and the Child entity is still inserted with parent_id = 0 and name = NULL, as shown below:

mysql> select * from parent;
+-----------+----------+
| parent_id | child_id |
+-----------+----------+
|         1 |       44 |
+-----------+----------+
1 row in set (0.02 sec)

mysql> select * from child;
+----------+-----------+------+
| child_id | parent_id | name |
+----------+-----------+------+
|       44 |         0 | NULL |
+----------+-----------+------+
1 row in set (0.02 sec)

Do you realize that the latest version is 6.6.22.Final? Please try again.

I retested with org.hibernate.orm:hibernate-core:6.6.22.Final, but the outcome remained unchanged.

mysql> select * from child;
+----------+-----------+------+
| child_id | parent_id | name |
+----------+-----------+------+
|       47 |         0 | NULL |
+----------+-----------+------+
1 row in set (0.00 sec)

Well, then this could be a bug, but I can’t say for sure since you have Kotlin and Spring in use which could also be the reason for the problem. Please try to create a reproducer with our test case template and if you are able to reproduce the issue, create a bug ticket in our issue tracker and attach that reproducer.