Hibernate Save Child Entities: Cascade PERSIST and merge()

Save a parent and its child entities with one persist() call in Hibernate 7. See cascade PERSIST and MERGE, the helper method that sets both sides, managed and detached parents, getReference(), and the real SQL and errors.

logo

To save child entities with their parent in Hibernate, we add CascadeType.PERSIST to the parent’s @OneToMany association and set both sides of the association with a helper method. Without the cascade, em.persist() inserts only the entity we pass to it, and the children are never saved.

We save children with their parent whenever both are created in one step, such as a survey with its questions or a playlist with its songs. Each question is its own row with a survey_id foreign key, so it needs its own INSERT statement.

The following example maps the questions of a survey with a cascade and saves a survey with three questions in one persist() call.

@OneToMany(mappedBy = "survey", cascade = {CascadeType.PERSIST, CascadeType.MERGE})
private List<Question> questions = new ArrayList<>();

public void addQuestion(Question question) {
  questions.add(question);
  question.setSurvey(this);
}
Survey survey = new Survey("Coffee Habits");
survey.addQuestion(new Question("How many cups a day?", 1));
survey.addQuestion(new Question("Favorite drink?", 2));
survey.addQuestion(new Question("Milk or black?", 3));
em.persist(survey);     // at commit:
                        // insert into Survey (title,id) values (?,?)
                        // insert into Question (position,survey_id,text,id) values (?,?,?,?)   3 times

Notice that the helper addQuestion() also sets question.survey, which is the field Hibernate writes to the survey_id column.

Next, we look at how the cascade and the owning side work together. After that, we save questions for a new survey, a managed survey, a detached survey and a survey we know only by its ID.

1. How Does Hibernate Save Child Entities?

A parent and its children are stored in two tables, and the child table holds the foreign key, here question.survey_id, which points to the parent row. The method persist() makes one object managed and schedules one INSERT for it, which runs at the next flush (the moment Hibernate sends pending changes to the database), at the latest at commit. Two settings decide whether the questions are inserted too.

  • The cascade attribute on Survey.questions. CascadeType.PERSIST repeats the persist() call on every question in the list.
  • The survey field of each Question. Question.survey is the owning side of the association, so Hibernate fills the survey_id column from that field and ignores the list.
Object graph: em.persist(survey) reaches the Survey Coffee Habits; cascade PERSIST arrows go from the survey to three Question objects, each with survey set to Coffee Habits; below, the survey table has one row and the question table has three rows with survey_id 1, inserted after the survey
cascade = PERSIST carries the call from the survey to its questions. question.survey gives each row its survey_id.

The cascade type decides which EntityManager call (the JPA object that saves and loads entities) reaches the children.

Cascade on Survey.questionsWhich call saves new questions
None (the default)No call. The questions are ignored without an error
CascadeType.PERSISTem.persist(survey), and the flush of a managed survey
CascadeType.MERGEem.merge(survey) for a detached survey
CascadeType.ALLBoth persist() and merge(), and it also cascades REMOVE, REFRESH and DETACH

We use {PERSIST, MERGE} in the example. CascadeType.ALL saves children the same way, but it also deletes the questions when the survey is deleted.

2. Saving Child Entities Example

The following example is an online survey tool built with Hibernate 7.4.11, Java 25 and an in-memory H2 database. A survey has a title and an ordered list of questions, and each question has a text and a position. The project on GitHub prints the SQL of each case and checks each result with 19 JUnit tests.

2.1. Survey and Question Entities

Survey is the parent. Its list uses mappedBy = “survey”, which means “the survey field of Question maps this association”.

@Id
@GeneratedValue
private Long id;

private String title;

@OneToMany(mappedBy = "survey", cascade = {CascadeType.PERSIST, CascadeType.MERGE})
private List<Question> questions = new ArrayList<>();

public void addQuestion(Question question) {
  questions.add(question);
  question.setSurvey(this);
}

Question is the child, and its @ManyToOne field owns the survey_id column. LAZY fetching keeps Hibernate from loading the survey every time it loads a question.

@Id
@GeneratedValue
private Long id;

private String text;

private int position;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "survey_id")
private Survey survey;

2.2. Persisting a New Survey With Its Questions

We build the whole object graph in memory and call persist() once, on the survey. The SQL comments show when each statement runs.

emf.runInTransaction(em -> {
  Survey survey = new Survey("Coffee Habits");
  survey.addQuestion(new Question("How many cups a day?", 1));
  survey.addQuestion(new Question("Favorite drink?", 2));
  survey.addQuestion(new Question("Milk or black?", 3));

  em.persist(survey);         // select next value for Survey_SEQ
                              // select next value for Question_SEQ
                              // select next value for Question_SEQ
  Long id = survey.getId();   // 1, already set, but no INSERT yet
});                           // commit:
                              // insert into Survey (title,id) values (?,?)
                              // insert into Question (position,survey_id,text,id) values (?,?,?,?)
                              // insert into Question (position,survey_id,text,id) values (?,?,?,?)
                              // insert into Question (position,survey_id,text,id) values (?,?,?,?)

The survey is always inserted before its questions, because each question row needs the survey’s ID in survey_id. When the INSERT runs depends on how @GeneratedValue creates the ID.

Two rows: with SEQUENCE, em.persist() only selects the next sequence values and the IDs are known, and the four INSERT statements run at commit, survey first; with IDENTITY, the INSERT statements for the survey and its questions run inside em.persist() and nothing is left for the commit
With a sequence, persist() only reserves IDs and the *INSERT*s wait for the flush. With IDENTITY, the database creates the ID, so the INSERT runs at once.

The default @GeneratedValue uses a database sequence on H2. With GenerationType.IDENTITY (an auto-increment column), the same code inserts the rows during the persist() call.

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

em.persist(survey);         // insert into Survey (title,id) values (?,default)
                            // insert into Question (position,survey_id,text,id) values (?,?,?,default)
                            // insert into Question (position,survey_id,text,id) values (?,?,?,default)
Long id = survey.getId();   // 1

2.3. Why the Helper Method Sets Both Sides

A common mistake is to add the question to the list only. The cascade still finds the question and inserts it, but Question.survey is null, and that field is what Hibernate writes to survey_id.

// Wrong: the question is saved without its survey
survey.getQuestions().add(new Question("Beach or hills?", 1));
em.persist(survey);   // insert into Question (position,survey_id,text,id) values (?,?,?,?)
                      // the new row has survey_id = NULL

// Right: addQuestion() also sets question.survey
survey.addQuestion(new Question("Beach or hills?", 1));
em.persist(survey);   // insert into Question (position,survey_id,text,id) values (?,?,?,?)
                      // the new row has survey_id = 1

No error tells us about the missing link, and the row is saved with survey_id = NULL. We add questions only through addQuestion(), never through getQuestions().add().

2.4. What Happens Without Cascade

We remove cascade from Survey.questions and run the same code. The call persist(survey) inserts the survey and nothing else.

survey.addQuestion(new Question("How many cups a day?", 1));
em.persist(survey);   // insert into Survey (title,id) values (?,?)
                      // no INSERT for the question, no error

The reverse case fails. When we persist only the question, Hibernate finds a managed question whose survey field points to a survey that was never saved (a transient object), and the commit stops.

survey.addQuestion(question);
em.persist(question);   // commit fails, nothing is saved
jakarta.persistence.RollbackException: Error while committing the transaction [...]
Caused by: java.lang.IllegalStateException: org.hibernate.TransientPropertyValueException: ...
Caused by: org.hibernate.TransientPropertyValueException: Persistent instance of 'com.howtodoinjava.hibernate.savechild.nocascade.Question' references an unsaved transient instance of 'com.howtodoinjava.hibernate.savechild.nocascade.Survey' (persist the transient instance before flushing) [com.howtodoinjava.hibernate.savechild.nocascade.Question.survey -> com.howtodoinjava.hibernate.savechild.nocascade.Survey]

TransientPropertyValueException is a subclass of TransientObjectException. Hibernate 5 and 6 worded the same error as “object references an unsaved transient instance – save the transient instance before flushing”, which is the text most search results still quote.

Without cascade, we persist each entity ourselves. The order of the calls matters: when the child comes first, Hibernate inserts it with an empty foreign key and runs an extra UPDATE later.

em.persist(question);   // insert into Question (position,survey_id,text,id) values (?,?,?,?)
em.persist(survey);     // insert into Survey (title,id) values (?,?)
                        // update Question set position=?,survey_id=?,text=? where id=?

em.persist(survey);     // insert into Survey (title,id) values (?,?)
em.persist(question);   // insert into Question (position,survey_id,text,id) values (?,?,?,?)

2.5. Adding a Question to a Managed Survey

A survey loaded with em.find() or a query inside the current transaction is a managed entity, which means Hibernate tracks its changes until the commit. So we do not call persist() for a new question here. For example, an admin page adds a question to a published survey inside one service call. At flush, Hibernate applies PERSIST again to the survey’s associations and finds the new question.

emf.runInTransaction(em -> {
  Survey survey = em.find(Survey.class, surveyId);   // select s1_0.id,s1_0.title from Survey s1_0 where s1_0.id=?
  survey.addQuestion(new Question("Tea or coffee?", 4));
});                                                  // insert into Question (position,survey_id,text,id) values (?,?,?,?)

Calling em.persist(survey) on a managed survey is allowed but does nothing extra, because persist() ignores an entity that is already managed and still cascades to its children.

2.6. Adding a Question to a Detached Survey

A detached survey was loaded in an earlier transaction, for example to show it in a web form. The persistence context that loaded it is closed, so nothing tracks it anymore. We load the questions with join fetch so the list can be used after the transaction ends, and save the new question with em.merge().

Survey detached = emf.callInTransaction(em -> em
    .createQuery("select s from Survey s join fetch s.questions where s.id = :id", Survey.class)
    .setParameter("id", surveyId)
    .getSingleResult());

Question sugar = new Question("Sugar?", 5);
detached.addQuestion(sugar);

Survey managed = emf.callInTransaction(em -> em.merge(detached));
// select s1_0.id,s1_0.title,q1_0.survey_id,... from Survey s1_0 left join Question q1_0 on s1_0.id=q1_0.survey_id where s1_0.id=?
// insert into Question (position,survey_id,text,id) values (?,?,?,?)

Long sugarId = sugar.getId();                            // null
Long savedId = managed.getQuestions().get(4).getId();   // 5, the saved copy

The call merge() saves a copy of the question and returns the managed survey, whereas the sugar object we passed in stays unsaved. Code that runs after the merge must use the returned survey. The method merge() reaches the questions only with CascadeType.MERGE. Without it, the same code runs without an error, but the question is not saved.

The method persist() does not work here, because it only accepts new objects and throws an EntityExistsException for a detached survey.

jakarta.persistence.EntityExistsException: Detached entity passed to persist: com.howtodoinjava.hibernate.savechild.Survey

2.7. Saving a Question for an Existing Survey

Sometimes we have only the survey’s ID, for example from a URL such as /surveys/1/questions. Loading the survey with find() runs a SELECT we do not need. The method em.getReference() returns a proxy, which is an object that holds the ID and loads the row only when we read one of its other fields.

emf.runInTransaction(em -> {
  Question decaf = new Question("Decaf?", 6);
  decaf.setSurvey(em.getReference(Survey.class, surveyId));   // no SQL
  em.persist(decaf);
});                                                           // insert into Question (position,survey_id,text,id) values (?,?,?,?)

Here we persist the child itself, so no cascade is involved. We set only decaf.survey, because the survey’s list is not loaded and survey_id comes from the child.

2.8. Which Call Saves the Children?

The right call depends on where the parent object comes from, and the cascade type must match that call.

Decision guide with four columns: a new survey uses em.persist(survey) and needs PERSIST; a survey loaded in this transaction needs no call because new questions are inserted at commit, and needs PERSIST; a detached survey uses em.merge(survey), returns a copy and needs MERGE; with only the survey id, use getReference and em.persist(question) with no cascade; below, a red note that em.persist on a detached survey throws EntityExistsException, and a gray note that Spring Data JPA save() calls persist when the id is null and merge otherwise
Pick the call by where the survey comes from. merge() returns a new managed copy.

3. Saving Child Entities FAQs

3.1. Does Spring Data JPA save() Save the Child Entities?

Yes, with the same cascade settings. The method save() in Spring Data JPA checks whether the entity is new. By default, an entity is new when its ID (or its @Version field of a wrapper type) is null.

Survey passed to save()What save() callsCascade needed
New, id == nullem.persist(survey)PERSIST
Loaded earlier, id setem.merge(survey)MERGE
Survey saved = surveyRepository.save(survey);   // use the returned object, as with merge()

Inside a @Transactional service method, a survey loaded through the repository is managed, so a new question is inserted at commit without calling save(), as in section 2.5. An entity with an ID that we assign ourselves is never null, so save() always calls merge() and runs a SELECT first.

3.2. What Replaced Session.save() in Hibernate 7?

The methods session.save(), saveOrUpdate() and update() no longer exist in Hibernate 7, and neither does the Hibernate-only SAVE_UPDATE cascade. The standard JPA methods replace them.

Hibernate 5 codeHibernate 7 codeCascade on the children
session.save(survey)em.persist(survey)CascadeType.PERSIST
session.update(survey)survey = em.merge(survey)CascadeType.MERGE
session.saveOrUpdate(survey)persist() for new, merge() for detachedBoth

Session extends EntityManager, so the same persist() and merge() calls work on a Session. Unlike save(), persist() returns void; we read the ID from the entity after the call.

3.3. Why Does Hibernate Run an UPDATE After Inserting the Children?

A unidirectional @OneToMany with @JoinColumn causes the extra UPDATE. In this mapping, Question has no survey field and the survey owns the foreign key, so Hibernate inserts the questions without survey_id and sets the column with one UPDATE per question.

@OneToMany(cascade = CascadeType.PERSIST)
@JoinColumn(name = "survey_id")
private List<Question> questions = new ArrayList<>();

em.persist(survey);   // insert into Survey (title,id) values (?,?)
                      // insert into Question (position,text,id) values (?,?,?)
                      // insert into Question (position,text,id) values (?,?,?)
                      // update Question set survey_id=? where id=?
                      // update Question set survey_id=? where id=?

The bidirectional mapping from section 2.1 writes survey_id in the INSERT and needs no UPDATE, so it runs fewer statements for every survey we save.

3.4. Does CascadeType.ALL Save Children the Same Way?

Yes. CascadeType.ALL includes PERSIST and MERGE, so the inserts are the same, and the IDENTITY example in section 2.2 uses this mapping. It also includes REMOVE, which changes what a delete does.

@OneToMany(mappedBy = "survey", cascade = CascadeType.ALL)
private List<Question> questions = new ArrayList<>();

em.persist(survey);                 // survey and 2 questions inserted
em.remove(survey);                  // survey and its 2 questions deleted
survey.removeQuestion(question);    // row kept, survey_id = NULL

Use CascadeType.ALL only when the questions must be deleted with their survey. Deleting a question that is removed from the list needs orphanRemoval = true.

4. Conclusion

Hibernate saves the children of a parent when the parent’s association has CascadeType.PERSIST (for persist() and managed parents) or CascadeType.MERGE (for detached parents), and when each child’s @ManyToOne field points to the parent. A helper method such as addQuestion() sets both sides in one place. For a child of an existing parent, getReference() plus persist() on the child saves it with a single INSERT.

5. References

Happy Learning !!

Source Code on Github

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.