Spring Data Overview

Spring Data Overview

Spring Data’s mission is to provide a familiar and consistent, Spring-based programming model for data access while still retaining the special traits of the underlying data store.


JPA

N+1 Query Problem

Link : Annotations Notes

Initializing a lazy relation via calling a method on a mapped relation causes an additional query. This should be avoided for performance reasons.

  • Solutions
    • SQL :
    • Eager Fetching
    • Entity Graph
      • Named Entity Graphs : a good solution if you will reuse the defined graph in code
      • Dynamic Entity Graphs : can be the better solution if you need to define a use case specific graph
    • DTO projection

NamedEntityGraph

Init
List<Menu> menus = List.of(
        Menu.builder().id(1).menuName("System Management").build(),
        Menu.builder().id(2).menuName("User Management").parentId(1).build(),
        Menu.builder().id(3).menuName("Role Management").parentId(1).build(),
        Menu.builder().id(4).menuName("Statistics Report").build(),
        Menu.builder().id(5).menuName("Statistics by Month").parentId(4).build()
);

menuRepository.saveAll(menus);
Entity
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "menu")
public class Menu {
    @Id
    private Integer id;
    private String menuName;
    private Integer parentId;
    @OneToMany
    @JoinColumn(name = "parentId")
    private List<Menu> childList;
}
Repository
1
2
3
4
@Repository
public interface  MenuRepository extends JpaRepository<Menu,Integer> {
    List<Menu> findAllByParentIdIsNull();
}
Queries Problem
1
2
3
4
5
6
7
8
@GetMapping("/menu")
public void test() {
    List<Menu> menuList = menuRepository.findAllByParentIdIsNull();
    System.out.println("Parent Id is null - Menu count = " + menuList.size());
    for (Menu m : menuList) {
        System.out.println("Menu name = " + m.getMenuName() + " - sub menu count = " + m.getChildList().size());
    }
}
output
Hibernate: select m1_0.id,m1_0.menu_name,m1_0.parent_id from menu m1_0 where m1_0.parent_id is null
Parent Id is null - Menu count = 2
Hibernate: select cl1_0.parent_id,cl1_0.id,cl1_0.menu_name from menu cl1_0 where cl1_0.parent_id=?
Menu name = System Management - sub menu count = 2
Hibernate: select cl1_0.parent_id,cl1_0.id,cl1_0.menu_name from menu cl1_0 where cl1_0.parent_id=?
Menu name = Statistics Report - sub menu count = 1
Entity
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@NamedEntityGraph(name = "menu.findAll", attributeNodes = {
        @NamedAttributeNode(value = "childList")
})
@Entity
@Table(name = "menu")
public class Menu {
    @Id
    private Integer id;
    private String menuName;
    private Integer parentId;
    @OneToMany
    @JoinColumn(name = "parentId")
    private List<Menu> childList;
}
Repository
1
2
3
4
5
@Repository
public interface  MenuRepository extends JpaRepository<Menu,Integer> {
    @EntityGraph(value = "menu.findAll", type = EntityGraph.EntityGraphType.FETCH)
    List<Menu> findAllByParentIdIsNull();
}
Solution-1
1
2
3
4
5
6
7
8
@GetMapping("/menu")
public void test() {
    List<Menu> menuList = menuRepository.findAllByParentIdIsNull();
    System.out.println("Parent Id is null - Menu count = " + menuList.size());
    for (Menu m : menuList) {
        System.out.println("Menu name = " + m.getMenuName() + " - sub menu count = " + m.getChildList().size());
    }
}
output
Hibernate: select m1_0.id,cl1_0.parent_id,cl1_0.id,cl1_0.menu_name,m1_0.menu_name,m1_0.parent_id from menu m1_0 left join menu cl1_0 on m1_0.id=cl1_0.parent_id where m1_0.parent_id is null
Parent Id is null - Menu count = 2
Menu name = System Management - sub menu count = 2
Menu name = Statistics Report - sub menu count = 1

JOIN FETCH

data.sql
insert into posts (id, title, content, author)
values (1, 'Post Title 1', 'Post content 1', 'Mr. J');
insert into posts (id, title, content, author)
values (2, 'Post Title 2', 'Post content 2', 'Mr. J');
insert into posts (id, title, content, author)
values (3, 'Post Title 3', 'Post content 3', 'Mr. J');
insert into posts (id, title, content, author)
values (4, 'Post Title 4', 'Post content 4', 'Mr. J');
insert into posts (id, title, content, author)
values (5, 'Post Title 5', 'Post content 5', 'Mr. J');

insert into comments (id, content, author, post_id)
values (1, 'Comment on Post-1 ', 'Mr. J', 1);
insert into comments (id, content, author, post_id)
values (2, 'Comment on Post-1 ', 'Mr. J', 1);
insert into comments (id, content, author, post_id)
values (3, 'Comment on Post-2 ', 'Mr. J', 2);
insert into comments (id, content, author, post_id)
values (4, 'Comment on Post-3 ', 'Mr. J', 3);
insert into comments (id, content, author, post_id)
values (5, 'Comment on Post-4 ', 'Mr. J', 4);
insert into comments (id, content, author, post_id)
values (6, 'Comment on Post-4 ', 'Mr. J', 4);
insert into comments (id, content, author, post_id)
values (7, 'Comment on Post-4 ', 'Mr. J', 4);
insert into comments (id, content, author, post_id)
values (8, 'Comment on Post-5 ', 'Mr. J', 5);
Entity
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "posts")
public class Post {
    @Id
    private Integer id;
    private String title;
    private String content;
    private String author;
    @OneToMany(fetch = FetchType.LAZY, mappedBy = "post", cascade = CascadeType.ALL)
    private List<Comment> comments;
}
Entity
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "comments")
public class Comment {
    @Id
    private Integer id;
    private String content;
    private String author;
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "post_id")
    private Post post;
}
Repository
1
2
3
4
5
6
7
8
public interface CommentRepository extends JpaRepository<Comment, Integer> {
    // Origin - N+1
    List<Comment> findAll();

    // Join Fetch
    @Query("select c from Comment c join fetch c.post")
    List<Comment> findAllCustomQuery();
}
Queries Problem
1
2
3
4
5
6
7
8
@GetMapping("/comment")
  public void getComments() {
  	List<Comment> comments = commentRepository.findAll();
  	log.info("Comment count = {}", comments.size());
  	for (Comment comment : comments) {
  		log.info("Comment [{}] from Post [{}]", comment.getContent(), comment.getPost().getTitle());
  	}
  }
output
Hibernate: select c1_0.id,c1_0.author,c1_0.content,c1_0.post_id from comments c1_0
... Comment count = 8
Hibernate: select p1_0.id,p1_0.author,p1_0.content,p1_0.title from posts p1_0 where p1_0.id=?
... Comment [Comment on Post-1 ] from Post [Post Title 1]
... Comment [Comment on Post-1 ] from Post [Post Title 1]
Hibernate: select p1_0.id,p1_0.author,p1_0.content,p1_0.title from posts p1_0 where p1_0.id=?
2024-03-27T12:26:15.217+08:00 ... Comment [Comment on Post-2 ] from Post [Post Title 2]
Hibernate: select p1_0.id,p1_0.author,p1_0.content,p1_0.title from posts p1_0 where p1_0.id=?
... Comment [Comment on Post-3 ] from Post [Post Title 3]
Hibernate: select p1_0.id,p1_0.author,p1_0.content,p1_0.title from posts p1_0 where p1_0.id=?
... Comment [Comment on Post-4 ] from Post [Post Title 4]
... Comment [Comment on Post-4 ] from Post [Post Title 4]
... Comment [Comment on Post-4 ] from Post [Post Title 4]
Hibernate: select p1_0.id,p1_0.author,p1_0.content,p1_0.title from posts p1_0 where p1_0.id=?
... Comment [Comment on Post-5 ] from Post [Post Title 5]
Solution-1
1
2
3
4
5
6
7
8
@GetMapping("/comment/custom/query")
  public void getCommentsByCustom() {
  	List<Comment> comments = commentRepository.findAllCustomQuery();
  	log.info("Comment count = {}", comments.size());
  	for (Comment comment : comments) {
  		log.info("Comment [{}] from Post [{}]", comment.getContent(), comment.getPost().getTitle());
  	}
  }
output
Hibernate: select c1_0.id,c1_0.author,c1_0.content,p1_0.id,p1_0.author,p1_0.content,p1_0.title from comments c1_0 join posts p1_0 on p1_0.id=c1_0.post_id
...Comment count = 8
...Comment [Comment on Post-1 ] from Post [Post Title 1]
...Comment [Comment on Post-1 ] from Post [Post Title 1]
...Comment [Comment on Post-2 ] from Post [Post Title 2]
...Comment [Comment on Post-3 ] from Post [Post Title 3]
...Comment [Comment on Post-4 ] from Post [Post Title 4]
...Comment [Comment on Post-4 ] from Post [Post Title 4]
...Comment [Comment on Post-4 ] from Post [Post Title 4]
...Comment [Comment on Post-5 ] from Post [Post Title 5]

Redis

MongoDB