Khái niệm  quan hệ 1-N:  là quan hệ giữa hai tập thực thể trong đó mỗi thực thể của tập này có thể liên kết với nhiều thực thể của tập còn lại

Source code tham khảo:

https://github.com/TechMaster/SpringBoot28Days/tree/27a0de8d52f72c30632a1ec9820d1e912305a554/15-CustomRepository/relation

Đầu tiên ta xét ví dụ sau về quan hệ 1-N

mối quan hệ giữa bảng post và bảng comments, một bài viết có thể có nhiều nhận xét nhưng một nhận xét chỉ thuộc duy nhất một và chỉ một bài viết

Trong hibernate để mapping quan hệ 1-N ta có 3 cách làm như sau:

  • Cách 1: mapping 1 chiều (unidirectional) có tạo bảng phụ
  • Cách 2: mapping 1 chiều không tạo bảng phụ
  • Cách 3: mapping 2 chiều

Ta cùng đi tìm hiểu cách đầu tiên: Unidirectional có tạo bảng phụ

Cách này ta sẽ chỉ sử dụng @OneToMany bên phía Post Entity (entity cha)

@Entity
@Table(name = "posts")
public class Post {
    // ...
    @OneToMany
    List<Comment> comments = new ArrayList<>();
}

Comment Entity

@Entity
@Table(name = "comments")
public class Comment  {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Lob
    private String text;

    public Comment() {

    }

    public Comment(String text) {
        this.text = text;
    }
// equals and hashcode
}

Ta thực hiện save post và comment vào database như sau

public class Runner {

    @Override
    public void run(String... args) throws Exception {
        Comment comment = new Comment();
        comment.setText("Example about 1-N relationship with unidirectional mapping with one more table create");

        Post post = new Post();
        post.setTitle("it will create more table");

        Set<Comment> comments = new HashSet<>();
        comments.add(comment);

        post.setComments(comments);

        postRepo.save(post);
    }
}

Sau khi chạy ứng dụng ta thu được Log Query như sau:

insert into posts (created_at, updated_at, content, description, title) values (?, ?, ?, ?, ?)
insert into comments (created_at, updated_at, text) values (?, ?, ?)
insert into posts_comments (post_id, comments_id) values (?, ?)

Ta thấy trong database sẽ sinh ra thêm một bảng thứ 3: posts_comments.

Quan hệ giữa 3 bảng như sau:

Với cách làm này ta cần thêm bộ nhớ để lưu trữ thêm bảng thứ 3, bảng phụ thứ 3 này khóa chính của nó chứa 2 thuộc tính, việc này cũng làm tăng bộ nhớ để đánh index với khóa chính có 2 thuộc tính.

Nếu không muốn phát sinh thêm bảng phụ thứ 3 chúng ta sẽ mapping với 2 cách phía sau.

One-To-Many Unidirectional không tạo thêm bảng phụ

Ta dữ nguyên Comment Enity và thay đổi Post Entity như sau, thêm annotation @JoinColumn vào bảng Post (bảng con) như sau

    @OneToMany(cascade = CascadeType.ALL)
    @JoinColumn(name = "post_id")
    Set<Comment> comments;

Thực thi lại ứng dụng một lần nữa ta thu được Log Query mới như sau

insert into posts (created_at, updated_at, content, description, title) values (?, ?, ?, ?, ?)
insert into comments (created_at, updated_at, text) values (?, ?, ?)
update comments set post_id=? where id=?

Với cách làm trên sẽ phát sinh thêm một cột nữa bên phía entity con thay vì sinh ra bảng thứ 3, đó là cột post_id bên phía bảng comment.

Trong cách mapping này ta dùng thêm annotattion @JoinColumn, @JoinColumn sẽ được sử dụng ở cả 3 mối quan hệ 1-1, 1-N, N-N, nó sẽ xuất hiện ở bảng ‘con’ của mối quan hệ. Thông thường, entity con là chủ sở hữu của mối quan hệ và thực thể cha là bên nghịch đảo của mối quan hệ, annotation này chỉ cho JPA thấy trong bảng comments có một cột post_id là khóa ngoại sẽ xác định cho mối quan hệ này,

Chú ý :  Nhìn vào log query ta thấy trường post_id được update ở câu lệnh update cuối cùng chứ không phải câu lệnh insert phía trên. Đó là do thứ tự flush của hibernate, xử lý đối với entity sẽ được thực hiện trước, sau đó đến quan hệ giữa các entity.

Ta xét thêm một ví dụ nhỏ cho việc xử lý entity trước, xử lý quan hệ giữa các entity sau:

Ta thay đổi post entity như sau:

@OneToMany(fetch = FetchType.EAGER, orphanRemoval = true, cascade = CascadeType.PERSIST)
@JoinColumn(name = "post_id")
List<Comment> comments;

Sau đó ta sẽ remove comment đầu tiên của post rồi chạy lệnh save xuống database:

Post post = postRepo.findById(3l).get();

post.getComments().remove(0);

postRepo.save(post);

Log Query : Hibernate thực hiện update đối với entity comment trước sau đó xử lý quan hệ post và comment (xoá comment trong post ). Ta thấy hibernate sinh ra 2 câu sql thay vì 1 câu sql ‘delete from comments where id=?’

update comments set post_id=null where post_id=?
delete from comments where id=?

One-To-Many Bidirectional

Post Enity:

    @OneToMany(fetch = FetchType.EAGER, mappedBy = "post", cascade = CascadeType.ALL)
    List<Comment> comments = new ArrayList<>();

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

    public void removeComment(Comment comment) {
        comments.remove(comment);
        comment.setPost(null);
    }
  • Nếu bạn không chỉ định FetchType là LAZY thì @ManyToOne sẽ sử dụng fetch type EAGER là mặc định, bạn nên lưu ý performent khi sử dụng fetch type là EAGER
  • Chú ý 1: trong bảng post ta có 2 phương thức addComment và removeComment nhưng trong bảng comment lại không có phương thức addPost và removePost vì đứng từ comment mà thêm mới post hoặc xóa post là không hợp lý.
  • Chú ý 2: 2 phương thức addComment và removeComment sẽ chứa thêm logic nhằm đồng bộ dữ liệu cho đúng khi save hoặc remove comment (post chứa comment và comment chứa post). Nếu không có sự đồng bộ dữ liệu từ 2 phía, mặc dù khi lưu post vẫn sẽ tự động lưu comment do có sử dụng CasecadeType = PERSIST, tuy nhiên bản ghi comment với trường post_id sẽ có giá trị null (vì bên trong đối tượng comment này chưa có post) , về mặt logic thì dữ liệu như vậy là không hợp lý.

Comment Enity:

@ManyToOne(fetch = FetchType.LAZY)
private Post post;

Runner:

Post post = new Post("one-to-many bidirectional");
Comment comment = new Comment("cmt");
post.addComment(comment);

postRepo.save(post);

Log Query : Dùng Bidirectional số câu sql sinh ra sẽ ít hơn Unidirectional

insert into posts (created_at, updated_at, content, description, title) values (?, ?, ?, ?, ?)
insert into comments (created_at, updated_at, post_id, text) values (?, ?, ?, ?)