A beginner’s guide to JPA and Hibernate Cascade Types

Introduction

JPA translates entity state transitions to database DML statements. Because it’s common to operate on entity graphs, JPA allows us to propagate entity state changes from Parents to Child entities.

This behavior is configured through the CascadeType mappings.

JPA vs Hibernate Cascade Types

Hibernate supports all JPA Cascade Types and some additional legacy cascading styles. The following table draws an association between JPA Cascade Types and their Hibernate native API equivalent:

JPA EntityManager action JPA CascadeType Hibernate native Session action Hibernate native CascadeType Event Listener
detach(entity) DETACH evict(entity) DETACH or EVICT Default Evict Event Listener
merge(entity) MERGE merge(entity) MERGE Default Merge Event Listener
persist(entity) PERSIST persist(entity) PERSIST Default Persist Event Listener
refresh(entity) REFRESH refresh(entity) REFRESH Default Refresh Event Listener
remove(entity) REMOVE delete(entity) REMOVE or DELETE Default Delete Event Listener
saveOrUpdate(entity)

SAVE_UPDATE Default Save Or Update Event Listener
replicate(entity, replicationMode)

REPLICATE Default Replicate Event Listener
lock(entity, lockModeType) buildLockRequest(entity, lockOptions)

LOCK Default Lock Event Listener
All the above EntityManager methods ALL All the above Hibernate Session methods ALL

From this table we can conclude that:

  • There’s no difference between calling persist, merge or refresh on the JPA EntityManager or the Hibernate Session.
  • The JPA remove and detach calls are delegated to Hibernate delete and evict native operations.
  • Only Hibernate supports replicate and saveOrUpdate. While replicate is useful for some very specific scenarios (when the exact entity state needs to be mirrored between two distinct DataSources), the persist and merge combo is always a better alternative than the native saveOrUpdate operation.

    As a rule of thumb, you should always use persist for TRANSIENT entities and merge for DETACHED ones.

    The saveOrUpdate shortcomings (when passing a detached entity snapshot to a Session already managing this entity) had lead to the merge operation predecessor: the now extinct saveOrUpdateCopy operation.

  • The JPA lock method shares the same behaviour with Hibernate lock request method.
  • The JPA CascadeType.ALL doesn’t only apply to EntityManager state change operations, but to all Hibernate CascadeTypes as well.

    So if you mapped your associations with CascadeType.ALL, you can still cascade Hibernate specific events. For example, you can cascade the JPA lock operation (although it behaves as reattaching, instead of an actual lock request propagation), even if JPA doesn’t define a CascadeType.LOCK.

Cascading best practices

Cascading only makes sense only for ParentChild associations (the Parent entity state transition being cascaded to its Child entities). Cascading from Child to Parent is not very useful and usually, it’s a mapping code smell.

Next, I’m going to take analyse the cascading behaviour of all JPA ParentChild associations.

One-To-One

The most common One-To-One bidirectional association looks like this:

@Entity
public class Post {

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

    private String name;

    @OneToOne(mappedBy = "post",
        cascade = CascadeType.ALL, orphanRemoval = true)
    private PostDetails details;

    public Long getId() {
        return id;
    }

    public PostDetails getDetails() {
        return details;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void addDetails(PostDetails details) {
        this.details = details;
        details.setPost(this);
    }

    public void removeDetails() {
        if (details != null) {
            details.setPost(null);
        }
        this.details = null;
    }
}

@Entity
public class PostDetails {

    @Id
    private Long id;

    @Column(name = "created_on")
    @Temporal(TemporalType.TIMESTAMP)
    private Date createdOn = new Date();

    private boolean visible;

    @OneToOne
    @JoinColumn(name = "id")
    @MapsId
    private Post post;

    public Long getId() {
        return id;
    }

    public void setVisible(boolean visible) {
        this.visible = visible;
    }

    public void setPost(Post post) {
        this.post = post;
    }
}

The Post entity plays the Parent role and the PostDetails is the Child.

The bidirectional associations should always be updated on both sides, therefore the Parent side should contain the addChild and removeChild combo. These methods ensure we always synchronize both sides of the association, to avoid object or relational data corruption issues.

In this particular case, the CascadeType.ALL and orphan removal make sense because the PostDetails life-cycle is bound to that of its Post Parent entity.

Cascading the one-to-one persist operation

The CascadeType.PERSIST comes along with the CascadeType.ALL configuration, so we only have to persist the Post entity, and the associated PostDetails entity is persisted as well:

Post post = new Post();
post.setName("Hibernate Master Class");

PostDetails details = new PostDetails();

post.addDetails(details);

session.persist(post);

Generating the following output:

INSERT INTO post(id, NAME) 
VALUES (DEFAULT, Hibernate Master Class'')

insert into PostDetails (id, created_on, visible) 
values (1, '2015-03-03 10:17:19.14', false)

Cascading the one-to-one merge operation

The CascadeType.MERGE is inherited from the CascadeType.ALL setting, so we only have to merge the Post entity and the associated PostDetails is merged as well:

Post post = newPost();
post.setName("Hibernate Master Class Training Material");
post.getDetails().setVisible(true);

doInTransaction(session -> {
    session.merge(post);
});

The merge operation generates the following output:

SELECT onetooneca0_.id     AS id1_3_1_,
   onetooneca0_.NAME       AS name2_3_1_,
   onetooneca1_.id         AS id1_4_0_,
   onetooneca1_.created_on AS created_2_4_0_,
   onetooneca1_.visible    AS visible3_4_0_
FROM   post onetooneca0_
LEFT OUTER JOIN postdetails onetooneca1_ 
    ON onetooneca0_.id = onetooneca1_.id
WHERE  onetooneca0_.id = 1

UPDATE postdetails SET 
    created_on = '2015-03-03 10:20:53.874', visible = true 
WHERE  id = 1

UPDATE post SET 
    NAME = 'Hibernate Master Class Training Material' 
WHERE  id = 1

Cascading the one-to-one delete operation

The CascadeType.REMOVE is also inherited from the CascadeType.ALL configuration, so the Post entity deletion triggers a PostDetails entity removal too:

Post post = newPost();

doInTransaction(session -> {
    session.delete(post);
});

Generating the following output:

delete from PostDetails where id = 1
delete from Post where id = 1

The one-to-one delete orphan cascading operation

If a Child entity is dissociated from its Parent, the Child Foreign Key is set to NULL. If we want to have the Child row deleted as well, we have to use the orphan removal support.

doInTransaction(session -> {
    Post post = (Post) session.get(Post.class, 1L);
    post.removeDetails();
});

The orphan removal generates this output:

SELECT onetooneca0_.id         AS id1_3_0_,
       onetooneca0_.NAME       AS name2_3_0_,
       onetooneca1_.id         AS id1_4_1_,
       onetooneca1_.created_on AS created_2_4_1_,
       onetooneca1_.visible    AS visible3_4_1_
FROM   post onetooneca0_
LEFT OUTER JOIN postdetails onetooneca1_
    ON onetooneca0_.id = onetooneca1_.id
WHERE  onetooneca0_.id = 1

delete from PostDetails where id = 1

Unidirectional one-to-one association

Most often, the Parent entity is the inverse side (e.g. mappedBy), the Child controlling the association through its Foreign Key. But the cascade is not limited to bidirectional associations, we can also use it for unidirectional relationships:

@Entity
public class Commit {

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

    private String comment;

    @OneToOne(cascade = CascadeType.ALL)
    @JoinTable(
        name = "Branch_Merge_Commit",
        joinColumns = @JoinColumn(
            name = "commit_id", 
            referencedColumnName = "id"),
        inverseJoinColumns = @JoinColumn(
            name = "branch_merge_id", 
            referencedColumnName = "id")
    )
    private BranchMerge branchMerge;

    public Commit() {
    }

    public Commit(String comment) {
        this.comment = comment;
    }

    public Long getId() {
        return id;
    }

    public void addBranchMerge(
        String fromBranch, String toBranch) {
        this.branchMerge = new BranchMerge(
             fromBranch, toBranch);
    }

    public void removeBranchMerge() {
        this.branchMerge = null;
    }
}

@Entity
public class BranchMerge {

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

    private String fromBranch;

    private String toBranch;

    public BranchMerge() {
    }

    public BranchMerge(
        String fromBranch, String toBranch) {
        this.fromBranch = fromBranch;
        this.toBranch = toBranch;
    }

    public Long getId() {
        return id;
    }
}

Cascading consists in propagating the Parent entity state transition to one or more Child entities, and it can be used for both unidirectional and bidirectional associations.

One-To-Many

The most common ParentChild association consists of a one-to-many and a many-to-one relationship, where the cascade being useful for the one-to-many side only:

@Entity
public class Post {

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

    private String name;

    @OneToMany(cascade = CascadeType.ALL, 
        mappedBy = "post", orphanRemoval = true)
    private List<Comment> comments = new ArrayList<>();

    public void setName(String name) {
        this.name = name;
    }

    public List<Comment> getComments() {
        return comments;
    }

    public void addComment(Comment comment) {
        comments.add(comment);
        comment.setPost(this);
    }

    public void removeComment(Comment comment) {
        comment.setPost(null);
        this.comments.remove(comment);
    }
}

@Entity
public class Comment {

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

    @ManyToOne
    private Post post;

    private String review;

    public void setPost(Post post) {
        this.post = post;
    }

    public String getReview() {
        return review;
    }

    public void setReview(String review) {
        this.review = review;
    }
}

Like in the one-to-one example, the CascadeType.ALL and orphan removal are suitable because the Comment life-cycle is bound to that of its Post Parent entity.

Cascading the one-to-many persist operation

We only have to persist the Post entity and all the associated Comment entities are persisted as well:

Post post = new Post();
post.setName("Hibernate Master Class");

Comment comment1 = new Comment();
comment1.setReview("Good post!");
Comment comment2 = new Comment();
comment2.setReview("Nice post!");

post.addComment(comment1);
post.addComment(comment2);

session.persist(post);

The persist operation generates the following output:

insert into Post (id, name) 
values (default, 'Hibernate Master Class')

insert into Comment (id, post_id, review) 
values (default, 1, 'Good post!')

insert into Comment (id, post_id, review) 
values (default, 1, 'Nice post!')

Cascading the one-to-many merge operation

Merging the Post entity is going to merge all Comment entities as well:

Post post = newPost();
post.setName("Hibernate Master Class Training Material");

post.getComments()
    .stream()
    .filter(comment -> comment.getReview().toLowerCase()
         .contains("nice"))
    .findAny()
    .ifPresent(comment -> 
        comment.setReview("Keep up the good work!")
);

doInTransaction(session -> {
    session.merge(post);
});

Generating the following output:

SELECT onetomanyc0_.id    AS id1_1_1_,
       onetomanyc0_.NAME  AS name2_1_1_,
       comments1_.post_id AS post_id3_1_3_,
       comments1_.id      AS id1_0_3_,
       comments1_.id      AS id1_0_0_,
       comments1_.post_id AS post_id3_0_0_,
       comments1_.review  AS review2_0_0_
FROM   post onetomanyc0_
LEFT OUTER JOIN comment comments1_
    ON onetomanyc0_.id = comments1_.post_id
WHERE  onetomanyc0_.id = 1

update Post set 
    name = 'Hibernate Master Class Training Material' 
where id = 1

update Comment set 
    post_id = 1, 
    review='Keep up the good work!' 
where id = 2

Cascading the one-to-many delete operation

When the Post entity is deleted, the associated Comment entities are deleted as well:

Post post = newPost();

doInTransaction(session -> {
    session.delete(post);
});

Generating the following output:

delete from Comment where id = 1
delete from Comment where id = 2
delete from Post where id = 1

The one-to-many delete orphan cascading operation

The orphan-removal allows us to remove the Child entity whenever it’s no longer referenced by its Parent:

newPost();

doInTransaction(session -> {
    Post post = (Post) session.createQuery(
        "select p " +
                "from Post p " +
                "join fetch p.comments " +
                "where p.id = :id")
        .setParameter("id", 1L)
        .uniqueResult();
    post.removeComment(post.getComments().get(0));
});

The Comment is deleted, as we can see in the following output:

SELECT onetomanyc0_.id    AS id1_1_0_,
       comments1_.id      AS id1_0_1_,
       onetomanyc0_.NAME  AS name2_1_0_,
       comments1_.post_id AS post_id3_0_1_,
       comments1_.review  AS review2_0_1_,
       comments1_.post_id AS post_id3_1_0__,
       comments1_.id      AS id1_0_0__
FROM   post onetomanyc0_
INNER JOIN comment comments1_
    ON onetomanyc0_.id = comments1_.post_id
WHERE  onetomanyc0_.id = 1

delete from Comment where id = 1

Many-To-Many

The many-to-many relationship is tricky because each side of this association plays both the Parent and the Child role. Still, we can identify one side from where we’d like to propagate the entity state changes.

We shouldn’t default to CascadeType.ALL because the CascadeType.REMOVE might end-up deleting more than we’re expecting (as you’ll soon find out):

@Entity
public class Author {

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

    @Column(name = "full_name", nullable = false)
    private String fullName;

    @ManyToMany(mappedBy = "authors", 
        cascade = {CascadeType.PERSIST, CascadeType.MERGE})
    private List<Book> books = new ArrayList<>();

    private Author() {}

    public Author(String fullName) {
        this.fullName = fullName;
    }

    public Long getId() {
        return id;
    }

    public void addBook(Book book) {
        books.add(book);
        book.authors.add(this);
    }

    public void removeBook(Book book) {
        books.remove(book);
        book.getAuthors().remove(this);
    }

    public void remove() {
        for(Book book : new ArrayList<>(books)) {
            removeBook(book);
        }
    }
}

@Entity
public class Book {

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

    @Column(name = "title", nullable = false)
    private String title;

    @ManyToMany(cascade = 
        {CascadeType.PERSIST, CascadeType.MERGE})
    @JoinTable(name = "Book_Author",
        joinColumns = {
            @JoinColumn(
                name = "book_id", 
                referencedColumnName = "id"
            )
        },
        inverseJoinColumns = {
            @JoinColumn(
                name = "author_id", 
                referencedColumnName = "id"
            )
        }
    )
    private List<Author> authors = new ArrayList<>();

    private Book() {}

    public Book(String title) {
        this.title = title;
    }

    public List<Author> getAuthors() {
        retrun authors;
    }
}

Cascading the many-to-many persist operation

Persisting the Author entities will persist the Books as well:

Author _John_Smith = new Author("John Smith");
Author _Michelle_Diangello = 
    new Author("Michelle Diangello");
Author _Mark_Armstrong = 
    new Author("Mark Armstrong");

Book _Day_Dreaming = new Book("Day Dreaming");
Book _Day_Dreaming_2nd = 
    new Book("Day Dreaming, Second Edition");

_John_Smith.addBook(_Day_Dreaming);
_Michelle_Diangello.addBook(_Day_Dreaming);

_John_Smith.addBook(_Day_Dreaming_2nd);
_Michelle_Diangello.addBook(_Day_Dreaming_2nd);
_Mark_Armstrong.addBook(_Day_Dreaming_2nd);

session.persist(_John_Smith);
session.persist(_Michelle_Diangello);
session.persist(_Mark_Armstrong);

The Book and the Book_Author rows are inserted along with the Authors:

insert into Author (id, full_name) 
values (default, 'John Smith')

insert into Book (id, title) 
values (default, 'Day Dreaming')

insert into Author (id, full_name) 
values (default, 'Michelle Diangello')

insert into Book (id, title) 
values (default, 'Day Dreaming, Second Edition')

insert into Author (id, full_name) 
values (default, 'Mark Armstrong')

insert into Book_Author (book_id, author_id) values (1, 1)
insert into Book_Author (book_id, author_id) values (1, 2)
insert into Book_Author (book_id, author_id) values (2, 1)
insert into Book_Author (book_id, author_id) values (2, 2)
insert into Book_Author (book_id, author_id) values (2, 3)

Dissociating one side of the many-to-many association

To delete an Author, we need to dissociate all Book_Author relations belonging to the removable entity:

doInTransaction(session -> {
    Author _Mark_Armstrong =
        getByName(session, "Mark Armstrong");
    _Mark_Armstrong.remove();
    session.delete(_Mark_Armstrong);
});

This use case generates the following output:

SELECT manytomany0_.id        AS id1_0_0_,
       manytomany2_.id        AS id1_1_1_,
       manytomany0_.full_name AS full_nam2_0_0_,
       manytomany2_.title     AS title2_1_1_,
       books1_.author_id      AS author_i2_0_0__,
       books1_.book_id        AS book_id1_2_0__
FROM   author manytomany0_
INNER JOIN book_author books1_
	ON manytomany0_.id = books1_.author_id
INNER JOIN book manytomany2_
	ON books1_.book_id = manytomany2_.id
WHERE  manytomany0_.full_name = 'Mark Armstrong'

SELECT books0_.author_id  AS author_i2_0_0_,
       books0_.book_id    AS book_id1_2_0_,
       manytomany1_.id    AS id1_1_1_,
       manytomany1_.title AS title2_1_1_
FROM   book_author books0_
INNER JOIN book manytomany1_
    ON books0_.book_id = manytomany1_.id
WHERE  books0_.author_id = 2

delete from Book_Author where book_id = 2

insert into Book_Author (book_id, author_id) values (2, 1)
insert into Book_Author (book_id, author_id) values (2, 2)

delete from Author where id = 3 

The many-to-many association generates way too many redundant SQL statements and often, they are very difficult to tune. Next, I’m going to demonstrate the many-to-many CascadeType.REMOVE hidden dangers.

The many-to-many CascadeType.REMOVE gotchas

The many-to-many CascadeType.ALL is another code smell, I often bump into while reviewing code. The CascadeType.REMOVE is automatically inherited when using CascadeType.ALL, but the entity removal is not only applied to the link table, but to the other side of the association as well.

Let’s change the Author entity books many-to-many association to use the CascadeType.ALL instead:

@ManyToMany(mappedBy = "authors", 
    cascade = CascadeType.ALL)
private List<Book> books = new ArrayList<>();

When deleting one Author:

doInTransaction(session -> {
    Author _Mark_Armstrong = 
        getByName(session, "Mark Armstrong");
    session.delete(_Mark_Armstrong);
    Author _John_Smith = 
        getByName(session, "John Smith");
    assertEquals(1, _John_Smith.books.size());
});

All books belonging to the deleted Author are getting deleted, even if other Authors we’re still associated to the deleted Books:

SELECT manytomany0_.id        AS id1_0_,
       manytomany0_.full_name AS full_nam2_0_
FROM   author manytomany0_
WHERE  manytomany0_.full_name = 'Mark Armstrong'  

SELECT books0_.author_id  AS author_i2_0_0_,
       books0_.book_id    AS book_id1_2_0_,
       manytomany1_.id    AS id1_1_1_,
       manytomany1_.title AS title2_1_1_
FROM   book_author books0_
INNER JOIN book manytomany1_ ON 
       books0_.book_id = manytomany1_.id
WHERE  books0_.author_id = 3  

delete from Book_Author where book_id=2
delete from Book where id=2
delete from Author where id=3

Most often, this behavior doesn’t match the business logic expectations, only being discovered upon the first entity removal.

We can push this issue even further, if we set the CascadeType.ALL to the Book entity side as well:

@ManyToMany(cascade = CascadeType.ALL)
@JoinTable(name = "Book_Author",
    joinColumns = {
        @JoinColumn(
            name = "book_id", 
            referencedColumnName = "id"
        )
    },
    inverseJoinColumns = {
        @JoinColumn(
            name = "author_id", 
            referencedColumnName = "id"
        )
    }
)

This time, not only the Books are being deleted, but Authors are deleted as well:

doInTransaction(session -> {
    Author _Mark_Armstrong = 
        getByName(session, "Mark Armstrong");
    session.delete(_Mark_Armstrong);
    Author _John_Smith = 
        getByName(session, "John Smith");
    assertNull(_John_Smith);
});

The Author removal triggers the deletion of all associated Books, which further triggers the removal of all associated Authors. This is a very dangerous operation, resulting in a massive entity deletion that’s rarely the expected behavior.

SELECT manytomany0_.id        AS id1_0_,
       manytomany0_.full_name AS full_nam2_0_
FROM   author manytomany0_
WHERE  manytomany0_.full_name = 'Mark Armstrong'  

SELECT books0_.author_id  AS author_i2_0_0_,
       books0_.book_id    AS book_id1_2_0_,
       manytomany1_.id    AS id1_1_1_,
       manytomany1_.title AS title2_1_1_
FROM   book_author books0_
INNER JOIN book manytomany1_
   ON books0_.book_id = manytomany1_.id
WHERE  books0_.author_id = 3  

SELECT authors0_.book_id      AS book_id1_1_0_,
       authors0_.author_id    AS author_i2_2_0_,
       manytomany1_.id        AS id1_0_1_,
       manytomany1_.full_name AS full_nam2_0_1_
FROM   book_author authors0_
INNER JOIN author manytomany1_
   ON authors0_.author_id = manytomany1_.id
WHERE  authors0_.book_id = 2  

SELECT books0_.author_id  AS author_i2_0_0_,
       books0_.book_id    AS book_id1_2_0_,
       manytomany1_.id    AS id1_1_1_,
       manytomany1_.title AS title2_1_1_
FROM   book_author books0_
INNER JOIN book manytomany1_
   ON books0_.book_id = manytomany1_.id
WHERE  books0_.author_id = 1 

SELECT authors0_.book_id      AS book_id1_1_0_,
       authors0_.author_id    AS author_i2_2_0_,
       manytomany1_.id        AS id1_0_1_,
       manytomany1_.full_name AS full_nam2_0_1_
FROM   book_author authors0_
INNER JOIN author manytomany1_
   ON authors0_.author_id = manytomany1_.id
WHERE  authors0_.book_id = 1  

SELECT books0_.author_id  AS author_i2_0_0_,
       books0_.book_id    AS book_id1_2_0_,
       manytomany1_.id    AS id1_1_1_,
       manytomany1_.title AS title2_1_1_
FROM   book_author books0_
INNER JOIN book manytomany1_
   ON books0_.book_id = manytomany1_.id
WHERE  books0_.author_id = 2  

delete from Book_Author where book_id=2
delete from Book_Author where book_id=1
delete from Author where id=2
delete from Book where id=1
delete from Author where id=1 
delete from Book where id=2
delete from Author where id=3

This use case is wrong in so many ways. There are a plethora of unnecessary SELECT statements and eventually we end up deleting all Authors and all their Books. That’s why CascadeType.ALL should raise your eyebrow, whenever you spot it on a many-to-many association.

When it comes to Hibernate mappings, you should always strive for simplicity. The Hibernate documentation confirms this assumption as well:

Practical test cases for real many-to-many associations are rare. Most of the time you need additional information stored in the link table. In this case, it is much better to use two one-to-many associations to an intermediate link class. In fact, most associations are one-to-many and many-to-one. For this reason, you should proceed cautiously when using any other association style.

If you enjoyed this article, I bet you are going to love my book as well.

Conclusion

Cascading is a handy ORM feature, but it’s not free of issues. You should only cascade from Parent entities to Children and not the other way around. You should always use only the casacde operations that are demanded by your business logic requirements, and not turn the CascadeType.ALL into a default ParentChild association entity state propagation configuration.

Code available on GitHub.

If you liked this article, you might want to subscribe to my newsletter too.

Advertisements

39 thoughts on “A beginner’s guide to JPA and Hibernate Cascade Types

  1. Nice article! I was a bit confused at the beginning when seeing all the select statements but then I saw you were trying to make a point! Apart from Cascade being dangerous in ManyToMany case as you mentioned, I wouldn’t use it at all because it might issue queries that I don’t want it to do. IMO the only real use cases for which cascades might be helpful is orphan removal.
    Would be interesting to see how the cascades behave when doing batch queries 😉

    1. Thanks. It’s always a good practice to check the generated SQL statements, because a bad mapping could have a huge impact on the application performance. For OneToOne and OneToMany, the PERSIST/MERGE and REMOVE Cascade Types are useful. For batch inserts, the cascade works the same.

  2. Great!!!
    I did not read the JPA documentatation, but the concept of “MERGE” is … well… not clear at all. If I have an entity A, that has a collection of entities B and the relationship beetwen them is not marked as “CascadeType.MERGE” in A, then when I make some update in a B child of A using A as a link (a.bCollection.get(0).setName = “Test”) that will not be propagated to the database (b will not be updated). Well, I’m supposing that happened inside a transaction, for sure.

    The question/comment is: MERGE has two concepts! One is related with a detached entity being atached (and updated in the database), and another is the concept of propagating updates of “child” entities.

    Do am I wrong? Is this a so complex concept?

    1. Merge is used for reattaching, for when you have a previously loaded entity and you want to make Hibernate start manage it. If both A and B entities are managed, then there’s no need for using a merge. The dirty checking mechanism will propagate changes on your behalf.
      If the child is a new entity and you set both sides of the association, then you shouldn’t use merge in this case either (persist will be much more suitable on the child entity).

  3. If I specify cascade types with Hibernate within my application, how should I make use of (do they even make sense?) foreign keys’ ON DELETE and ON UPDATE properties while not holding up/disturbing Hibernate’s implementation of cascades.

  4. Huh, that individual row deletion at the end… Doesn’t Hibernate do bulk deletions in such cases? That would heavily speed up the task, in those cases where the behaviour is actually desired…

    1. The individual row deletion makes sense only when being backed by some optimistic locking mechanism. But even then, a FK-level delete cascade might be a better alternative. The catch is that Hibernate is not aware of the underlying deleted child records, therefore caution is advised. Most often this should not be an issue since DELETE is the last operation to be executed during flush. However, if the flush order is manually overridden and there is some pending update on the underlying child records (that just got deleted), the update statements will fail since there is no longer any record associated on the DB side.

  5. In the section “Cascading the many-to-many persist operation”, the 2nd book has 3 authors. So shouldn’t the last insert to Book_Author be

    insert into Book_Author (book_id, author_id) values (2, 3)

    instead of

    insert into Book_Author (book_id, author_id) values (3, 1) ?

  6. Thanks for the helpful guide!

    I am having trouble with a many-to-one association. You mentioned “where the cascade being useful for the one-to-many side only”- does this mean I should not use cascading for my many-to-one, or should I use a different type?

    Specifically, I am getting a TransientObjectException when I try to flush the session. If I don’t flush, the code appears to function normally, but it has a strong code smell. If I try to use cascade=all, I get errors about a null value being inserted into a non-null field.

    1. Many-to-one associations don’t need to have a CascadeType. If you have a bidirectional association, you should have the cascade on the One-to-Many side. Otherwise, you should just persist every transient object manually.

  7. In the section “Cascading the one-to-one merge operation” I don’t see where in the code the id is set to 1. It’s missing something?

  8. I want to say one more Thank You! Thanks for your article, I was able to deal with some moments of cascade types. Thank you.
    By the way, found one small typo along the way, please correct: “We shouldn’t default to CascadeType.ALL, because the CascadeTpe.REMOVE…”

  9. Good day, Vlad.

    Let me ask a couple of questions. My questions relate to @ManyToMany code example.

    1. In entity class Author you have such method:
    public void removeBook(Book book) {
            books.remove(book);
            book.authors.remove(this);
    }
    

    I don’t understand how we can access field authors from another entity class Book since it has private access in Book, or you meant book.getAuthors().remove(this)?

    1. In dissociating one side of the many-to-many association part of example code looks like:
    doInTransaction(session -> {
        Author _Mark_Armstrong =
            getByName(session, "Mark Armstrong");
        _Mark_Armstrong.remove();
        session.delete(_Mark_Armstrong);
    });
    

    I don’t know what I’m doing wrong, but I get ConcurrentModificationException at line _Mark_Armstrong.remove(); In author.remove() method we are going in a loop calling removeBook() method for each Book in Authors bookList and trying delete book from this list we are looping through right now, that’s why I’m getting Exception, I think. But strangely that if I’m perform deleting from list with iterator’s remove() method it doesn’t save the situation (getting the same exception) although it should be. Or I’m wrong? Can you explain this?

    By the way, calling author.getBooks().clear(); does the job as expected and after clear() I can delete author without any damage to associated objects, because we delete author with empty books list. Such way of deleting object from list is acceptable?

    Or I completely lost? 🙂

    Thanks in advance.

      1. Thanks for the tip. I updated the example so that it won’t be any confusion in the future related to access modifiers.

      2. I explained this question in my book, High-Performance Java Persistence. Basically, you need to rely on the Iterator.remove if you want to modify the collection that you are currently looping.

      3. The most acceptable method is to disassociate all references, even if clear() works for the current version. Maybe in future, something will change in Hibernate implementation, and this behavior that you observe won’t be enforced anymore. Only the dissociation logic for both sides is guaranteed to work.

  10. Hello and thanks for the post, it is awesome

    However the Cascading the many-to-many persist operation example does not work for me

    If I follow your example, it indeed saves the Author and the Book but not the relationship between them in the linking table.

    If I do the opposite way: book.getAuthors().add(author) and save the book it saves the book, the author and the link between them relationship (which makes more sense as it is the owner side of the relation ship?)

    Is this a mistake ?

  11. Thanks for writing “Cascading only makes sense only for Parent – Child associations (the Parent entity state transition being cascaded to its Child entities). Cascading from Child to Parent is not very useful and usually, it’s a mapping code smell.”! I actually spent an hour and a half on stack overflow trying to figure out what I was doing wrong, until I read that paragraph. Thanks!

  12. Hi Vlad, Thank you very much for sharing this in a comprehensive manner. Although I find one scenario to be missing in the above article. When I have auto-generated entities where all the cascade options are already there if I need to insert a row only into the parent table I get errors related to the child entity (I am using Spring Repository save). The workaround I had to follow was to duplicate this entity, delete @OneToMany annotated method etc.. Is there any simpler way to insert into a parent table. Thanks.

    1. If you cannot insert a Parent because a Child entity throws a ConstraintViolationExcepion, then your entity mapping design is broken because a Parent is always independent of any Child existence, but the reverse is not true.

      1. Vlad, I really appreciate your quick reply. Actually I am not getting ConstraintViolationExcepion. I am getting NullPointerException for the child entity. In my JSON I have just the data for one parent row. I tried adding not null check for the child entity list but that does not help either. It would have been very useful if Spring Repository save API had a boolean argument to enable/disable cascading. Thanks.

  13. “You should only cascade from Parent entities to Children and not the other way around.”

    Yeah, that’s what I want to do. It is not so easy, though.

    It turns out that contrary to hibernate documentation, which does not reference a “none” cascade type, there are usages of “CascadeType.NONE” – eg. http://pwinckles.blogspot.com/2011/12/how-to-disable-hibernate-cascading.html.

    But then Stack overflow seems to contradict this:

    https://stackoverflow.com/questions/8048268/default-value-for-cascadetype-in-hibernate

    By default no operations are cascaded.

    But yay! That’s what I want. But No! That’s not what you get in the Grails wrapper for hibernate:

    https://docs.grails.org/latest/ref/Database%20Mapping/cascade.html

    “If the association doesn’t define an owner (a “belongs to” relationship) … then GORM uses a cascading policy of “save-update” by default.”

    Ugh! I guess it’s not a hibernate problem after all, but just a grails implementation of hibernate issue.

  14. Hi Vlad,
    Thanks for this article.
    I have question in section: “Cascading the one-to-one persist operation”
    Why in object PostDetails details = new PostDetails(); you are not setting Post, what I mean:
    details.setPost(post);
    Because without setting post it it’s throwing error :
    org.hibernate.id.IdentifierGenerationException: attempted to assign id from null one-to-one property

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s