iT邦幫忙

2023 iThome 鐵人賽

DAY 11
0
Software Development

救救我啊我救我!CRUD 工程師的惡補日記系列 第 11

【Spring Boot】使用 JPA 建立多對多關係

  • 分享至 

  • xImage
  •  

在昨天的文章,我們設計出兩張資料表的一對多關係,而本文會以學生選課為情境,引進「中間表」的概念,實作出較複雜的「多對多關係」。


一、多對多關係

在完成資料庫的正規化後,我們可以思考 table 之間是否有類似下面的關係。

  • 每位學生會選修多個課程;每個課程會被多位學生選修(學生表 對 課程表)
  • 每張訂單會有多個商品;每個商品會被加入到多張訂單中(訂單表 對 商品表)

A 可以對應到多個 B;而 B 也可對應到多個 A,這就是「多對多」的關係。

從資料表來看,這兩張 table 乍看之下是沒有關聯的,比方說學生歸學生、課程歸課程。不過,「選修」這個使用情境讓這兩種看似獨立的資料,產生了某種關係。

「購物網站」是程式作品常見的練習主題,那麼就以此為例繼續說明。關於「下單」這個功能,會牽涉兩張表,也就是商品和訂單。前者儲存了商品資訊,後者儲存了訂單的買家、總金額和送貨地址等。

然而商品表和訂單表本身是無關的,是情境讓它們有關。因此勢必要有第三張表,用來紀錄訂單購買了什麼商品。構想大致上如下圖:
https://ithelp.ithome.com.tw/upload/images/20230908/20131107Nmm0ViY1or.jpg

如圖,「ordered_product」紀錄了訂購的商品,並將其他表透過兩個「一對多」關係做間接關聯。在本文,我們稱它為「中間表」。

二、Model 類別介紹

首先來看看本文要使用的兩個 model 類別,也就是 table 的設計。

第一個是學生資料(StudentPO)。

@Entity
@Table(name = "student")
public class StudentPO {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "student_id")
    private long id;

    private String name;

    public static StudentPO of(String name) {
        var student = new StudentPO();
        student.name = name;

        return student;
    }
    
    // hashCode, equals ...

    // getter, setter ...
}

第二個是課程資料(CoursePO)。

@Entity
@Table(name = "course")
public class CoursePO {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "course_id")
    private long id;

    private String name;

    private int point;

    public static CoursePO of(String name, int point) {
        var course = new CoursePO();
        course.name = name;
        course.point = point;

        return course;
    }

    // hashCode, equals ...

    // getter, setter ...
}

這兩者的關聯,如同第一節所提到的學生選修課程的例子。構想大致上如下圖:
https://ithelp.ithome.com.tw/upload/images/20230908/20131107el5Q48zu5q.jpg

其中「selected_course」為中間表。

三、建立關聯

本節會將程式中「student」與「course」這兩張表,利用「中間表」建立起多對多關係。我們先配置其中一張表,再配置另一張。由於兩表都是「多」的那方,因此順序不拘。

筆者會以「課程已經開設好了,學生可以來選課」的角度來實作。

(一)配置第一方的表

由於一個課程可以被學生們加選,因此在 CoursePO 添加 StudentPO 的集合欄位。此處使用 Set 是為了避免發生重複資料。

@Entity
@Table(name = "course")
public class CoursePO {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "course_id")
    private long id;
    
    // ...

    @ManyToMany(targetEntity = StudentPO.class, fetch = FetchType.EAGER)
    @JoinTable(
            // 中間表名稱
            name = "selected_course",
            // 本類別與中間表的關聯
            joinColumns = @JoinColumn(name = "course", referencedColumnName = "course_id"),
            // 對方類別與中間表的關聯
            inverseJoinColumns = @JoinColumn(name = "student", referencedColumnName = "student_id")
    )
    private Set<StudentPO> students;

    // ...
}

接著還需要使用一些標記(annotation),設定上會比一對多關係複雜些。

@ManyToMany 標記代表多對多關係(many contact to many student)。targetEntity 參數代表發生關聯的 model 類別。至於 fetch 參數已在昨天文章說明。

@JoinTable 標記是用來配置「中間表」。name 參數代表中間表名稱。joinColumnsinverseJoinColumns 參數,則分別設置自身與對方的 model 類別要如何與中間表關聯。

@JoinColumn 標記定義了 model 表要用哪個欄位當外鍵。name 參數為中間表的欄位名;referencedColumnName 為 model 表的外鍵欄位名。

(二)配置第二方的表

基於昨天介紹的「雙向關聯」,我們想要在查詢出一門課程後,一併獲取學生清單。而且也會想在查詢出一位學生後,一併獲取選課清單。因此接下來在另一張表進行配置。

@Entity
@Table(name = "student")
public class StudentPO {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "student_id")
    private long id;

    // ...

    @ManyToMany(mappedBy = "students", fetch = FetchType.EAGER)
    private Set<CoursePO> courses = new HashSet<>();

    // ...
}

同樣是使用 @ManyToMany 標記,但只要提供 mappedBy 參數即可。此處傳入的「students」指的是 CoursePOstudents 欄位名,代表與本欄位互相關聯。

四、測試

下面的測試程式,是先分別建立課程與學生資料。接著將學生加入到課程中,模擬出選課的動作後,儲存課程。最後分別從兩個面向查詢:

  • 查詢學生,一併獲取選修課程清單
  • 查詢課程,一併獲取修課學生

在測試情境中,有「英文」、「計概」和「會計」這三門課程。Vincent 加選了英文和計概,而 Ivy 加選了英文和會計。

@RunWith(SpringRunner.class)
@SpringBootTest
public class ApplicationTests {

	@Autowired
	private StudentRepository studentRepository;

	@Autowired
	private CourseRepository courseRepository;
    
    @Before
    public void clearDB() {
        studentRepository.deleteAll();
        courseRepository.deleteAll();
    }

	@Test
	public void testManyToManyRelation() {
		// create courses
		CoursePO course1 = CoursePO.of("英文", 3);
		CoursePO course2 = CoursePO.of("計算機概論", 4);
		CoursePO course3 = CoursePO.of("會計學", 3);
		Set<CoursePO> courses = Set.of(course1, course2, course3);
		courseRepository.saveAll(courses);

		// create students
		StudentPO student1 = StudentPO.of("Vincent");
		StudentPO student2 = StudentPO.of("Ivy");
		studentRepository.saveAll(Set.of(student1, student2));

		// select courses
		course1.setStudents(Set.of(student1, student2));
		course2.setStudents(Set.of(student1));
		course3.setStudents(Set.of(student2));
		courseRepository.saveAll(courses);

		// query student and get related courses
		StudentPO dbStudent1 = studentRepository.findById(student1.getId()).orElseThrow();
		assertEquals(Set.of(course1, course2), dbStudent1.getCourses());

		StudentPO dbStudent2 = studentRepository.findById(student2.getId()).orElseThrow();
		assertEquals(Set.of(course1, course3), dbStudent2.getCourses());

		// query course and get related students
		CoursePO dbCourse1 = courseRepository.findById(course1.getId()).orElseThrow();
		assertEquals(Set.of(student1, student2), dbCourse1.getStudents());

		CoursePO dbCourse2 = courseRepository.findById(course2.getId()).orElseThrow();
		assertEquals(Set.of(student1), dbCourse2.getStudents());

		CoursePO dbCourse3 = courseRepository.findById(course3.getId()).orElseThrow();
		assertEquals(Set.of(student2), dbCourse3.getStudents());
	}
}

Ref:Spring Data JPA 12 多表操作 多對多 081 多對多:映射配置


今日文章到此結束!
最後推廣一下自己的部落格,我是「新手工程師的程式教室」的作者,請多指教/images/emoticon/emoticon41.gif


上一篇
【Spring Boot】使用 JPA 建立一對多關係
下一篇
【Spring Boot】使用 Java Mail 發送純文字郵件與附件
系列文
救救我啊我救我!CRUD 工程師的惡補日記50
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言