Spring boot : Quan hệ nhiều - nhiều (many to many) trong JPA

Bài viết này được dịch bài : Trần Anh Tuấn - Học viện lớp java07
Email liên hệ : tanhtuan093@gmail.com
Bài viết gốc : https://www.baeldung.com/jpa-many-to-many

1.Tổng quan

Trong hướng dẫn này, chúng ta sẽ thấy nhiều cách để xử lý các mối quan hệ many to many bằng JPA.

Chúng tôi sẽ sử dụng mô hình sinh viên, khóa học và các mối quan hệ khác nhau giữa họ.

Để đơn giản, trong các code ví dụ, chúng ta sẽ chỉ hiển thị các thuộc tính và cấu hình JPA có liên quan đến các mối quan hệ many to many.

2. Many to many cơ bản

2.1 Mô hình hóa mối quan hệ many to many

Mối quan hệ là sự kết nối giữa hai loại thực thể. Trong trường hợp mối quan hệ nhiều-nhiều, cả hai bên có thể liên quan đến nhiều trường hợp của bên kia.

Lưu ý rằng các loại thực thể có thể có mối quan hệ với chính chúng. Hãy nghĩ về ví dụ về mô hình cây gia đình: Mỗi nút là một người, vì vậy nếu chúng ta nói về mối quan hệ cha-con, cả hai người tham gia sẽ là một người.

Tuy nhiên, nó không tạo ra sự khác biệt cho dù chúng ta nói về mối quan hệ giữa các loại thực thể đơn lẻ hay nhiều loại thực thể. Vì sẽ dễ dàng hơn khi nghĩ về mối quan hệ giữa hai loại thực thể khác nhau, chúng tôi sẽ sử dụng điều đó để minh họa các trường hợp của chúng tôi.

Hãy lấy ví dụ về việc sinh viên đánh dấu các khóa học mà họ thích.

Một sinh viên có thể thích nhiều khóa học và nhiều sinh viên có thể thích cùng một khóa học:
simple-er

Như chúng ta đã biết, trong RDBMS, chúng ta có thể tạo mối quan hệ với các khóa ngoại. Vì cả hai bên đều có thể tham chiếu bên kia, chúng ta cần tạo một bảng riêng để chứa các khóa ngoại (foreign keys):
simple-model-updated

Một bảng như vậy được gọi là một bảng nối. Trong một bảng nối, tổ hợp các khóa ngoại sẽ là khóa chính tổng hợp của nó.

2.2 Thực hiện trong JPA

Dễ dàng lập mô hình mối quan hệ nhiều-nhiều với POJO. Chúng ta nên bao gồm một Collection trong cả hai class, trong đó chứa các phần tử của các class khác.

Sau đó, chúng ta cần đánh dấu class bằng @Entity và khóa chính bằng @Id để biến chúng thành các thực thể JPA thích hợp.

Ngoài ra, chúng ta nên cấu hình kiểu quan hệ. Vì vậy, chúng ta đánh dấu các collection bằng chú thích @ManyToMany:

@Entity
class Student {

    @Id
    Long id;

    @ManyToMany
    Set<Course> likedCourses;

    // additional properties
    // standard constructors, getters, and setters
}

@Entity
class Course {

    @Id
    Long id;

    @ManyToMany
    Set<Student> likes;

    // additional properties
    // standard constructors, getters, and setters
}

Ngoài ra, chúng ta phải cấu hình cách lập mô hình mối quan hệ trong RDBMS.

Phía chủ sở hữu là nơi chúng ta định cấu hình mối quan hệ. Chúng ta sẽ sử dụng class Student.

Chúng ta có thể làm điều này với @JoinTable trong class Student. Chúng ta cung cấp tên của bảng tham gia (course_like) cũng như các khóa ngoại có @JoinColumn. Thuộc tính joinColumn sẽ kết nối với phía chủ sở hữu của mối quan hệ và inverseJoinColumn với phía bên kia:

@ManyToMany
@JoinTable(
  name = "course_like", 
  joinColumns = @JoinColumn(name = "student_id"), 
  inverseJoinColumns = @JoinColumn(name = "course_id"))
Set<Course> likedCourses;

Lưu ý rằng không cần sử dụng @JoinTable hoặc thậm chí @JoinColumn. JPA sẽ tạo tên bảng và cột cho chúng ta. Tuy nhiên, chiến lược mà JPA sử dụng không phải lúc nào cũng phù hợp với các quy ước đặt tên mà chúng tôi sử dụng. Vì vậy, chúng ta cần khả năng cấu hình tên bảng và cột.

Về phía mục tiêu, chúng ta chỉ phải cung cấp tên của trường, ánh xạ mối quan hệ…

Do đó, chúng tôi đặt thuộc tính mappedBy của chú thích @ManyToMany trong lớp Khóa học:

@ManyToMany(mappedBy = "likedCourses")
Set<Student> likes;

Hãy nhớ rằng vì mối quan hệ nhiều-nhiều không có phía chủ sở hữu trong cơ sở dữ liệu, chúng ta có thể định cấu hình bảng tham gia trong class Course và tham chiếu nó từ class Student .

3. Many to many bằng cách sử dụng Composite Key

3.1. Mô hình hóa các thuộc tính mối quan hệ

Giả sử chúng ta muốn để sinh viên đánh giá các khóa học. Một sinh viên có thể đánh giá bất kỳ số lượng khóa học nào và bất kỳ số lượng sinh viên nào cũng có thể đánh giá cùng một khóa học. Do đó, nó cũng là một mối quan hệ nhiều-nhiều.

Điều làm cho ví dụ này phức tạp hơn một chút là có nhiều thứ liên quan đến mối quan hệ xếp hạng hơn là thực tế là nó tồn tại. Chúng ta cần lưu trữ điểm đánh giá mà sinh viên đã cho trong khóa học.

Chúng ta có thể lưu trữ thông tin này ở đâu? Chúng ta không thể đưa nó vào thực thể Student vì một sinh viên có thể đưa ra các xếp hạng khác nhau cho các khóa học khác nhau. Tương tự, lưu trữ nó trong thực thể Course cũng không phải là một giải pháp tốt.

Đây là một tình huống khi bản thân mối quan hệ có một thuộc tính.

Sử dụng ví dụ này, việc đính kèm một thuộc tính vào một quan hệ trông giống như sau trong một sơ đồ ER:
relation-attibute-er

Chúng ta có thể mô hình hóa nó theo cách gần giống như mối quan hệ nhiều-nhiều đơn giản. Sự khác biệt duy nhất là chúng tôi đính kèm một thuộc tính mới vào bảng tham gia:
relation-attribute-model-updated

3.2 Tạo composite key(khóa tổng hợp) trong JPA

Việc thực hiện một mối quan hệ nhiều-nhiều đơn giản khá đơn giản. Vấn đề duy nhất là chúng ta không thể thêm một thuộc tính vào một mối quan hệ theo cách đó vì chúng ta đã kết nối các thực thể một cách trực tiếp. Do đó, chúng ta không có cách nào để thêm một thuộc tính vào chính mối quan hệ.

Vì chúng ta ánh xạ các thuộc tính DB với các trường lớp trong JPA, chúng ta cần tạo một lớp thực thể mới cho mối quan hệ.

Tất nhiên, mọi thực thể JPA đều cần một khóa chính. Vì khóa chính của chúng ta là khóa tổng hợp, chúng ta phải tạo một lớp mới sẽ chứa các phần khác nhau của khóa:

@Embeddable
class CourseRatingKey implements Serializable {

    @Column(name = "student_id")
    Long studentId;

    @Column(name = "course_id")
    Long courseId;

    // standard constructors, getters, and setters
    // hashcode and equals implementation
}

Lưu ý rằng một lớp khóa tổng hợp phải đáp ứng một số yêu cầu chính:

  • Chúng ta phải đánh dấu nó bằng @Embeddable.
  • Nó phải triển khai java.io.Serializable.
  • Chúng tôi cần cung cấp triển khai các phương thức hashcode() và equals().
  • Không có trường nào có thể là một thực thể.

3.3 Sử dụng composite key trong JPA

Sử dụng lớp khóa tổng hợp này, chúng ta có thể tạo lớp thực thể, lớp này lập mô hình bảng tham gia:

@Entity
class CourseRating {

    @EmbeddedId
    CourseRatingKey id;

    @ManyToOne
    @MapsId("studentId")
    @JoinColumn(name = "student_id")
    Student student;

    @ManyToOne
    @MapsId("courseId")
    @JoinColumn(name = "course_id")
    Course course;

    int rating;
    
    // standard constructors, getters, and setters
}

Mã này rất giống với một triển khai thực thể thông thường. Tuy nhiên, chúng ta có một số điểm khác biệt chính:

  • Chúng ta đã sử dụng @EmbeddedId để đánh dấu khóa chính, là một phiên bản của lớp CourseRatingKey.
  • Chúng ta đã đánh dấu các trường sinh viên và khóa học bằng @MapsId.

@MapsId có nghĩa là chúng ta liên kết các trường đó với một phần của khóa và chúng là khóa ngoại của mối quan hệ nhiều-một. Chúng ta cần nó bởi vì, như chúng tôi đã đề cập, chúng ta không thể có các thực thể trong khóa tổng hợp.

Sau đó, chúng ta có thể định cấu hình các tham chiếu nghịch đảo trong các thực thể Student và Course như trước:

class Student {

    // ...

    @OneToMany(mappedBy = "student")
    Set<CourseRating> ratings;

    // ...
}

class Course {

    // ...

    @OneToMany(mappedBy = "course")
    Set<CourseRating> ratings;

    // ...
}

Lưu ý rằng có một cách thay thế để sử dụng các khóa tổng hợp: annotation @IdClass.

3.4. Các đặc điểm khác

Chúng ta đã định cấu hình các mối quan hệ với các lớp Student và Course dưới dạng @ManyToOne. Chúng ta có thể làm điều này bởi vì với thực thể mới, chúng ta đã phân tách một cách cấu trúc mối quan hệ nhiều-nhiều thành hai mối quan hệ nhiều-một.

Tại sao chúng ta có thể làm điều này? Nếu chúng ta kiểm tra kỹ các bảng trong trường hợp trước, chúng ta có thể thấy rằng nó chứa hai mối quan hệ nhiều-một. Nói cách khác, không có bất kỳ mối quan hệ nhiều-nhiều nào trong RDBMS. Chúng ta gọi các cấu trúc chúng ta tạo với các bảng nối các mối quan hệ nhiều-nhiều vì đó là những gì chúng ta mô hình hóa.

Bên cạnh đó, sẽ rõ ràng hơn nếu chúng ta nói về mối quan hệ nhiều-nhiều vì đó là ý định của chúng ta. Trong khi đó, một bảng tham gia chỉ là một chi tiết triển khai; chúng ta không thực sự quan tâm đến nó.

Hơn nữa, giải pháp này có một tính năng bổ sung mà chúng ta chưa đề cập đến. Giải pháp nhiều-nhiều đơn giản tạo ra mối quan hệ giữa hai thực thể. Do đó, chúng ta không thể mở rộng mối quan hệ ra nhiều thực thể hơn. Nhưng chúng ta không có giới hạn này trong giải pháp này: chúng ta có thể mô hình hóa các mối quan hệ giữa bất kỳ số loại thực thể nào.

Ví dụ: khi nhiều giáo viên có thể dạy một khóa học, sinh viên có thể đánh giá cách một giáo viên cụ thể dạy một khóa học cụ thể. Theo cách đó, xếp hạng sẽ là mối quan hệ giữa ba thực thể: sinh viên, khóa học và giáo viên.

4. Many to many với một thực thể mới

4.1. Mô hình hóa các thuộc tính mối quan hệ

Giả sử chúng ta muốn cho sinh viên đăng ký các khóa học. Ngoài ra, chúng ta cần lưu trữ điểm khi học viên đăng ký một khóa học cụ thể. Trên hết, chúng ta muốn lưu trữ điểm cô ấy nhận được trong khóa học.

Trong một thế giới lý tưởng, chúng ta có thể giải quyết vấn đề này bằng giải pháp trước đó, nơi chúng ta có một thực thể có khóa tổng hợp. Tuy nhiên, thế giới khác xa lý tưởng, và sinh viên không phải lúc nào cũng hoàn thành khóa học trong lần thử đầu tiên.

Trong trường hợp này, có nhiều kết nối giữa các cặp student-course giống nhau hoặc nhiều hàng, với các cặp student_id-course_id giống nhau. Chúng ta không thể lập mô hình nó bằng bất kỳ giải pháp nào trước đây vì tất cả các khóa chính phải là duy nhất. Vì vậy, chúng ta cần sử dụng một khóa chính riêng biệt.

Do đó, chúng ta có thể giới thiệu một thực thể, thực thể này sẽ chứa các thuộc tính của đăng ký:
relation-entity-er

Trong trường hợp này, thực thể Registration đại diện cho mối quan hệ giữa hai thực thể còn lại.

Vì nó là một thực thể, nó sẽ có khóa chính của riêng nó.

Trong giải pháp trước, hãy nhớ rằng chúng ta có một khóa chính tổng hợp mà chúng tôi đã tạo từ hai khóa ngoại.

Bây giờ hai khóa ngoại sẽ không còn là một phần của khóa chính:
relation-entity-model-updated

4.2. Thực hiện trong JPA

Vì course_registration trở thành một bảng thông thường, chúng ta có thể tạo một thực thể JPA cũ đơn giản mô hình hóa nó:

@Entity
class CourseRegistration {

    @Id
    Long id;

    @ManyToOne
    @JoinColumn(name = "student_id")
    Student student;

    @ManyToOne
    @JoinColumn(name = "course_id")
    Course course;

    LocalDateTime registeredAt;

    int grade;
    
    // additional properties
    // standard constructors, getters, and setters
}

Chúng ta cũng cần định cấu hình các mối quan hệ trong các lớp Student và Course :

class Student {

    // ...

    @OneToMany(mappedBy = "student")
    Set<CourseRegistration> registrations;

    // ...
}

class Course {

    // ...

    @OneToMany(mappedBy = "course")
    Set<CourseRegistration> registrations;

    // ...
}

Một lần nữa, chúng ta đã định cấu hình mối quan hệ trước đó, vì vậy chúng ta chỉ cần cho JPA biết nơi nó có thể tìm thấy cấu hình đó.

Chúng ta cũng có thể sử dụng giải pháp này để giải quyết vấn đề trước đây về xếp hạng các khóa học của sinh viên. Tuy nhiên, thật kỳ lạ khi tạo một khóa chính chuyên dụng trừ khi chúng ta phải làm vậy.

Hơn nữa, từ góc độ RDBMS, nó không có nhiều ý nghĩa vì việc kết hợp hai khóa ngoại lại tạo thành một khóa tổng hợp hoàn hảo. Bên cạnh đó, khóa tổng hợp đó có một ý nghĩa rõ ràng: chúng ta kết nối những thực thể nào trong mối quan hệ.

Nếu không, sự lựa chọn giữa hai cách triển khai này thường chỉ đơn giản là sở thích cá nhân.

5. Kết luận

Trong bài viết này, chúng ta đã biết mối quan hệ nhiều-nhiều là gì và làm thế nào chúng ta có thể mô hình hóa nó trong RDBMS bằng JPA.

Chúng tôi đã thấy ba cách để mô hình hóa nó trong JPA. Cả ba đều có những ưu và nhược điểm khác nhau khi nói đến các khía cạnh sau:

  • code rõ ràng
  • DB rõ ràng
  • khả năng gán các thuộc tính cho mối quan hệ
  • chúng ta có thể liên kết bao nhiêu thực thể với mối quan hệ
  • hỗ trợ nhiều kết nối giữa các thực thể giống nhau