The best way to map a @OneToMany relationship with JPA and Hibernate

(Last Updated On: January 4, 2018)
 

Introduction

In a relational database system, a one-to-many association between links two tables based on a Foreign Key column so that the child table record references the Primary Key of the parent table row.

As straightforward as it might be in a RDBMS, when it comes to JPA, the one-to-manydatabase association can be represented either through a @ManyToOne or a @OneToMany association since the OOP association can be either unidirectional or bidirectional.

The @ManyToOne annotation allows you to map the Foreign Key column in the child entity mapping so that the child has an entity object reference to its parent entity. This is the most natural way of mapping a database one-to-many database association, and, usually, the most efficient alternative too.

For convenience, to take advantage of the entity state transitions and the dirty checking mechanism, many developers choose to map the child entities as a collection in the parent object, and, for this purpose, JPA offers the @OneToManyannotation.

As I explained in my book, many times, you are better off replacing collections with a query, which is much more flexible in terms of fetching performance. However, there are times when mapping a collection is the right thing to do, and then you have two choices:

  • a unidirectional @OneToMany association
  • a bidirectional @OneToMany association

The bidirectional association requires the child entity mapping to provide a @ManyToOne annotation, which is responsible for controlling the association.

One the other hand, the unidirectional @OneToMany association is simpler since it’s just the parent-side which defines the relationship. In this article, I’m going to explain the caveats of @OneToMany associations, and how you can overcome them.

There are many ways to map the @OneToMany association. We can use a List or a Set. We can also define the @JoinColumn annotation too. So, let’s see how all these work.

 

Unidirectional @OneToMany

Consider we have the following mapping:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Entity(name = "Post")
@Table(name = "post")
public class Post {
 
    @Id
    @GeneratedValue
    private Long id;
 
    private String title;
 
    @OneToMany(
        cascade = CascadeType.ALL,
        orphanRemoval = true
    )
    private List<PostComment> comments = new ArrayList<>();
 
    //Constructors, getters and setters removed for brevity
}
 
@Entity(name = "PostComment")
@Table(name = "post_comment")
public class PostComment {
 
    @Id
    @GeneratedValue
    private Long id;
 
    private String review;
 
    //Constructors, getters and setters removed for brevity
}

Now, if we persist one Post and three PostComment(s):

1
2
3
4
5
6
7
8
9
10
11
12
13
Post post = new Post("First post");
 
post.getComments().add(
    new PostComment("My first review")
);
post.getComments().add(
    new PostComment("My second review")
);
post.getComments().add(
    new PostComment("My third review")
);
 
entityManager.persist(post);

Hibernate is going to execute the following SQL statements:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
insert into post (title, id)
values ('First post', 1)
 
insert into post_comment (review, id)
values ('My first review', 2)
 
insert into post_comment (review, id)
values ('My second review', 3)
 
insert into post_comment (review, id)
values ('My third review', 4)
 
insert into post_post_comment (Post_id, comments_id)
values (1, 2)
 
insert into post_post_comment (Post_id, comments_id)
values (1, 3)
 
insert into post_post_comment (Post_id, comments_id)
values (1, 4)

What is that! Why there are so many queries executed? And what’s the deal with that post_post_comment table anyway?

Well, by default, that’s how the unidirectional @OneToMany association works, and this is how it looks from a database perspective:

For a DBA, this looks more like a many-to-many database association than a one-to-many relationship, and it’s not very efficient either. Instead of two tables, we now have three tables, so we are using more storage than necessary. Instead of only one Foreign Key, we now have two of them. However, since we are most likely going to index these Foreign Keys, we are going to require twice as much memory to cache the index for this association. Not nice!

Unidirectional @OneToMany with @JoinColumn

To fix the aforementioned extra join table issue, we just need to add the @JoinColumn in the mix:

1
2
3
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "post_id")
private List<PostComment> comments = new ArrayList<>();

The @JoinColumn annotation helps Hibernate (the most famous JPA provider) to figure out that there is a post_id Foreign Key column in the post_comment table that defines this association.

With this annotation in place, when persisting the three PostComment entities, we get the following SQL output:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
insert into post (title, id)
values ('First post', 1)
 
insert into post_comment (review, id)
values ('My first review', 2)
 
insert into post_comment (review, id)
values ('My second review', 3)
 
insert into post_comment (review, id)
values ('My third review', 4)
 
update post_comment set post_id = 1 where id = 2
 
update post_comment set post_id = 1 where id =  3
 
update post_comment set post_id = 1 where id =  4

A little bit better, but what’s the purpose of those three update statements?

If you take a look at Hibernate flush order, you’ll see that the persist action is executed before the collection elements are handled. This way, Hibernate inserts the child records first without the Foreign Key since the child entity does not store this information. During collection handling phase, the Foreign Key column is updated accordingly.

The same logic applies to collection state modifications, so when removing the firsts entry from the child collection:

1
post.getComments().remove(0);

Hibernate executes two statements instead of one:

1
2
3
update post_comment set post_id = null where post_id = 1 and id = 2
 
delete from post_comment where id=2

Again, the parent entity state change is executed first, which triggers the child entity update. Afterward, when the collection is processed, the orphan removal action will execute the child row delete statement.

경축! 아무것도 안하여 에스천사게임즈가 새로운 모습으로 재오픈 하였습니다.
어린이용이며, 설치가 필요없는 브라우저 게임입니다.
https://s1004games.com

So, is a java.util.Set any different?

No, it’s not. The same statements are executed if you use the @JoinColumn annotation on a unidirectional @OneToMany Set association.

Bidirectional @OneToMany

The best way to map a @OneToMany association is to rely on the @ManyToOne side to propagate all entity state changes:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
@Entity(name = "Post")
@Table(name = "post")
public class Post {
 
    @Id
    @GeneratedValue
    private Long id;
 
    private String title;
 
    @OneToMany(
        mappedBy = "post",
        cascade = CascadeType.ALL,
        orphanRemoval = true
    )
    private List<PostComment> comments = new ArrayList<>();
 
    //Constructors, getters and setters removed for brevity
 
    public void addComment(PostComment comment) {
        comments.add(comment);
        comment.setPost(this);
    }
 
    public void removeComment(PostComment comment) {
        comments.remove(comment);
        comment.setPost(null);
    }
}
 
@Entity(name = "PostComment")
@Table(name = "post_comment")
public class PostComment {
 
    @Id
    @GeneratedValue
    private Long id;
 
    private String review;
 
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "post_id")
    private Post post;
 
    //Constructors, getters and setters removed for brevity
 
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof PostComment )) return false;
        return id != null && id.equals(((PostComment) o).id);
    }
    @Override
    public int hashCode() {
        return 31;
    }
}

There are several things to note on the aforementioned mapping:

  • The @ManyToOne association uses FetchType.LAZY because, otherwise, we’d fall back to EAGER fetching which is bad for performance.
  • The parent entity, Post, features two utility methods (e.g. addComment and removeComment) which are used to synchronize both sides of the bidirectional association. You should always provide these methods whenever you are working with a bidirectional association as, otherwise, you risk very subtle state propagation issues.
  • The child entity, PostComment, implement the equals and hashCode methods. Since we cannot rely on a natural identifier for equality checks, we need to use the entity identifier instead. However, you need to do it properly so that equality is consistent across all entity state transitions. Because we rely on equality for the removeComment, it’s good practice to override equals and hashCode for the child entity in a bidirectional association.

If we persist three PostComment(s):

1
2
3
4
5
6
7
8
9
10
11
12
13
Post post = new Post("First post");
 
post.addComment(
    new PostComment("My first review")
);
post.addComment(
    new PostComment("My second review")
);
post.addComment(
    new PostComment("My third review")
);
 
entityManager.persist(post);

Hibernate generates just one SQL statement for each persisted PostComment entity:

1
2
3
4
5
6
7
8
9
10
11
insert into post (title, id)
values ('First post', 1)
 
insert into post_comment (post_id, review, id)
values (1, 'My first review', 2)
 
insert into post_comment (post_id, review, id)
values (1, 'My second review', 3)
 
insert into post_comment (post_id, review, id)
values (1, 'My third review', 4)

If we remove a PostComment:

1
2
3
4
Post post = entityManager.find( Post.class, 1L );
PostComment comment1 = post.getComments().get( 0 );
 
post.removeComment(comment1);

There’s only one delete SQL statement that gets executed:

1
delete from post_comment where id = 2

So, the bidirectional @OneToMany association is the best way to map a one-to-manydatabase relationship when we really need the collection on the parent side of the association.

Just @ManyToOne

Just because you have the option of using the @OneToMany annotation, it does not mean this should be the default option for every one-to-many database relationship. The problem with collections is that we can only use them when the number of child records is rather limited.

Therefore, in reality, @OneToMany is practical only when many means few. Maybe @OneToFew would have been a more suggestive name for this annotation.

As I explained in this StackOverflow answer, you cannot limit the size of a @OneToMany collection like it would be the case if you used query-level pagination.

Therefore, most of the time, the @ManyToOne annotation on the child side is everything you need. But then, how do you get the child entities associated with a Post entity?

Well, all you need is just a single JPQL query:

1
2
3
4
5
6
List<PostComment> comments = entityManager.createQuery(
    "select pc " +
    "from PostComment pc " +
    "where pc.post.id = :postId", PostComment.class)
.setParameter( "postId", 1L )
.getResultList();

Which translates to a straightforward SQL query:

1
2
3
4
5
select pc.id AS id1_1_,
       pc.post_id AS post_id3_1_,
       pc.review AS review2_1_
from   post_comment pc
where  pc.post_id = 1

Even if the collection is not managed anymore, it’s rather trivial to just add/remove child entities whenever necessary. As for updating child objects, the dirty checking mechanism works just fine even if you don’t use a managed collection. What’s nice about using a query is that you can paginate it any way you like so that, if the number of child entities grows with time, the application performance is not going to be affected.

 

Conclusion

As you will see in a future article, bidirectional collections are way better than unidirectional ones because they rely on the @ManyToOne association, which is always efficient in terms of generated SQL statements.

But then, even if they are very convenient, you don’t always have to use collections. The @ManyToOne association is the most natural and also efficient way of mapping a one-to-many database relationship.

 

 

[출처] https://vladmihalcea.com/the-best-way-to-map-a-onetomany-association-with-jpa-and-hibernate/

 

 

 

본 웹사이트는 광고를 포함하고 있습니다.
광고 클릭에서 발생하는 수익금은 모두 웹사이트 서버의 유지 및 관리, 그리고 기술 콘텐츠 향상을 위해 쓰여집니다.
번호 제목 글쓴이 날짜 조회 수
38 Spring Batch 간단 정리 Sprong Batch 기초를 알아보자 file 졸리운_곰 2020.10.13 198
37 Spring Batch - ItemReader, ItemWriter, ItemProcessor 졸리운_곰 2020.10.13 370
36 spring batch» Spring Batch Tasklet Example file 졸리운_곰 2020.10.12 198
35 Spring Batch Example 2 - 간단한 Job만들기 file 졸리운_곰 2020.10.12 151
34 Spring batch Job 설정과 실행하기 file 졸리운_곰 2020.10.12 206
33 [Spring Batch] Configuring and Running a Job file 졸리운_곰 2020.10.12 121
32 [스프링] 스프링 배치 기본 개념 file 졸리운_곰 2019.12.24 244
31 Spring Batch 개념 정리 file 졸리운_곰 2019.12.24 259
30 스프링 배치 개발가이드 졸리운_곰 2019.12.24 321
29 Quartz + Spring Batch 조합! Quartz Scheduler 와 Spring Batch 의 궁합과 조합을 정리. file 졸리운_곰 2019.12.24 882
28 Spring Batch 간단 정리 file 졸리운_곰 2019.12.24 422
27 spring batch 소개 file 졸리운_곰 2019.12.24 210
26 Spring Batch - 소개 file 졸리운_곰 2019.12.24 207
25 Spring Batch Multithreading Example file 졸리운_곰 2019.01.31 285
24 Spring batch를 Parallel로 돌려보자 졸리운_곰 2019.01.31 399
23 Spring MVC 주요 애노테이션(Annotation)정리 file 졸리운_곰 2018.12.29 269
22 스프링/스프링부트 애노테이션(Annotation) 정리 졸리운_곰 2018.12.29 385
21 Spring Framework - annotation 정리 #1 졸리운_곰 2018.12.29 321
20 [Spring] Annotation 정리 졸리운_곰 2018.12.29 272
19 Maven 기초 사용법 졸리운_곰 2018.04.15 322
대표 김성준 주소 : 경기 용인 분당수지 U타워 등록번호 : 142-07-27414
통신판매업 신고 : 제2012-용인수지-0185호 출판업 신고 : 수지구청 제 123호 개인정보보호최고책임자 : 김성준 sjkim70@stechstar.com
대표전화 : 010-4589-2193 [fax] 02-6280-1294 COPYRIGHT(C) stechstar.com ALL RIGHTS RESERVED