Post

Spring) JPA Entity Options - Lazy Loading / Cascade / Orphan Entity Removal

Spring) JPA Entity Options - Lazy Loading / Cascade / Orphan Entity Removal

This post was migrated from Tistory. You can find the original here.

2023.07.13 - [spring] - Spring) JPA Entity Relationships - The Foreign Key Owner (23-07-11)

Continuing from the previous post.

JPA Entity Options

FetchType EAGER/LAZY (Lazy Loading)

1
2
3
4
5
6
7
8
9
10
11
public @interface ManyToOne{
...
    FetchType fetch() default FetchType.EAGER;
...
}

public @interface OneToMany{
...
    FetchType fetch() default FetchType.LAZY;
...
}

With EAGER, the referenced entity is fetched via a join at the same time as the initial select.
With LAZY, no join is done on the initial select; instead an additional select is issued and the reference is fetched only when it’s actually needed.

The default FetchType for ManyToOne is EAGER,
and the default FetchType for OneToMany is LAZY.
This is presumably because eagerly joining an unknown number of “many” rows by default could hurt performance, so LAZY makes more sense there.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Post extends Timestamped {
...

    @OneToMany(mappedBy = "post", cascade = CascadeType.REMOVE)
    private List<Comment> commentList = new ArrayList<>();

...
}

public class Comment extends Timestamped {
    ....
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "post_id")
    private Post post;

    @ManyToOne()
    @JoinColumn(name = "user_id")
    private User user;
}

Given the entity relationships above, calling findAll on Post produces the following SQL.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Hibernate: 
    /* <criteria> */ select
        ...
        p1_0.subject,
        p1_0.username 
    from
        post p1_0
Hibernate: 
    select
        ...
        c1_0.created_at,
        c1_0.modified_at,
        u1_0.id,
        u1_0.password,
        u1_0.username 
    from
        comment c1_0 
    left join
        users u1_0 
            on u1_0.id=c1_0.user_id 
    where
        c1_0.post_id=?

On Post, commentList defaults to LAZY. You can see that only Post is selected first, and an additional select for comments is issued only when commentList is actually accessed.

Now, Comment has both a user field and a post field. user uses the default EAGER, while post is explicitly set to LAZY.


You can see that Comment joins user immediately, while post is never used, so no extra select query is executed for it.

CascadeType PERSIST/REMOVE (Cascading Persistence)

Cascading persistence: a situation where an operation performed on a persistent entity propagates to its associated entities.

1
2
3
4
5
6
7
8
9
10
11
12
13
public class User {
    ...
    (1)
    @OneToMany(mappedBy = "user")
    (2)
    @OneToMany(mappedBy = "user", cascade = {CascadeType.PERSIST, CascadeType.REMOVE})
    private List<Food> foodList = new ArrayList<>();

    public void addFoodList(Food food) {
        this.foodList.add(food);
        food.setUser(this);// set the foreign key (the relationship)
        }
}
1
2
3
4
5
6
7
    User user = new User();
    user.setName("Robbie");
    ...
    user.addFoodList(food1);
    user.addFoodList(food2);

    userRepository.save(user);

With setting (1), saving via userRepo does not cascade down to saving via foodRepo.
With setting (2), CascadeType.PERSIST cascades the insert operation, so saving user also persists food1 and food2 through foodRepo.

1
2
    User user = userRepository.findByName("Robbie");
    userRepository.delete(user);

With setting (1), deleting via userRepo does not cascade down to deleting via foodRepo.
With setting (2), CascadeType.REMOVE cascades the delete operation, so deleting user also removes food1 and food2.

orphanRemoval (Removing Orphaned Entities)

1
2
3
4
5
6
7
8
9
10
11
12
    (@Transactional)
    User user = userRepository.findByName("Robbie");

    Food chicken = null;
    for (Food food : user.getFoodList()) {
        if(food.getName().equals("fried chicken")) {
            chicken = food;
        }
    }
    if(chicken != null) {
        user.getFoodList().remove(chicken);
    }

When removing an associated entity from a collection like this, CascadeType.REMOVE alone does not trigger a delete query.

1
2
3
4
5
6
7
public class User {
    ...
    (1)
    @OneToMany(mappedBy = "user", cascade = CascadeType.PERSIST, orphanRemoval = true)
    private List<Food> foodList = new ArrayList<>();
    ...
}

The orphanRemoval setting is what makes a partial removal of associated data actually result in a delete query.

cascade = REMOVE deletes all associated entities when the owning entity itself is deleted,
while orphanRemoval includes that same behavior and additionally issues a delete query whenever an associated entity is individually removed from the collection.

orphanRemoval is not an option available on ManyToOne.
It’s a feature for deleting the entity on the “one” side of a relationship from the opposite side, and since the “many” side holds the foreign key, it wouldn’t make sense for the “many” side to be able to kill off the “one” side — the “one” side is the reverse relation, so there’s no need for a partial-removal feature there.

A word of caution

orphanRemoval and CascadeType.REMOVE should be used carefully.
Before applying either option, make sure to check whether the related entity is referenced only by “you,” or whether someone else also references it.

This post is licensed under CC BY-NC 4.0 by the author.