Try   HackMD

@JoinTable @ManyToMany

tags: JPA

@JoinTable

Specifies the mapping of associations. It is applied to the owning side of an association.
Join table 通常應用於映射:
多對多
單向的一對多
雙向的多對一/一對多
單向的多對一
單向及雙向的一對一

單向 雙向
一對多
多對一
一對一
多對多

A join table is typically used in the mapping of many-to-many and unidirectional one-to-many associations. It may also be used to map bidirectional many-to-one/one-to-many associations, unidirectional many-to-one relationships, and one-to-one associations (both bidirectional and unidirectional).

When a join table is used in mapping a relationship with an embeddable class on the owning side of the relationship, the containing entity rather than the embeddable class is considered the owner of the relationship.

If the JoinTable annotation is missing, the default values of the annotation elements apply. The name of the join table is assumed to be the table names of the associated primary tables concatenated together (owning side first) using an underscore.

Many-To-Many

  • 基礎的多對多範例:學生-課程

一個學生可以擁有多種課程
一個課程也可以有多個學生
如此一來就形成了多對多的關係

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

  • 在表單圖上,我會稱 student 學生、course 課程為主要的表單
    course_rating 可以稱為中介表或是聯接表
  • 單純的多對多,中介表course_rating裡不需要rating這個欄位就可以使用,rating的用途後面會再提到

在 JPA 中實作

我們需在兩個 Entity 中各自加入一個 Collection 並且以 @ManytoMany annotation 設置關聯

@Entity
class Student {
package com.example.manytomany.entity;

import java.io.Serializable;
import java.util.Set;
import javax.persistence.*;

/**
 * 學生
 *
 * @author 歐炫
 */
@Entity
@SuppressWarnings("PersistenceUnitPresent")
@Table(catalog = "manytomany", schema = "public", name = "student")
public class Student implements Serializable {

	private static final long serialVersionUID = 5681823641952133327L;

	@Id
	@Basic(optional = false)
	@Column(nullable = false)
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;

	@Column(name = "full_name")
	private String fullName;

	@JoinTable(name = "course_rating", joinColumns = @JoinColumn(name = "student_id"), inverseJoinColumns = @JoinColumn(name = "course_id"))
	@ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
	private Set<Course> courses;

	public Student() {
	}

	protected Student(Long id) {
		this.id = id;
	}

	public Long getId() {
		return id;
	}

	public void setId(Long id) {
		this.id = id;
	}

	public String getFullName() {
		return fullName;
	}

	public void setFullName(String fullName) {
		this.fullName = fullName;
	}

	public Set<Course> getCourses() {
		return courses;
	}

	public void setCourses(Set<Course> courses) {
		this.courses = courses;
	}

	@Override
	public int hashCode() {
		int hash = 0;
		hash += (id != null ? id.hashCode() : 0);
		return hash;
	}

	@Override
	public boolean equals(Object object) {
		if (!(object instanceof Student)) {
			return false;
		}
		Student other = (Student) object;
		return !((this.id == null && other.id != null) || (this.id != null && !this.id.equals(other.id)));
	}

	@Override
	public String toString() {
		return "com.example.manytomany.entity.Student[ id=" + id + " ]";
	}
}

}
package com.example.manytomany.entity;

import com.fasterxml.jackson.annotation.JsonIgnore;
import java.io.Serializable;
import java.util.Set;
import javax.persistence.Basic;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.ManyToMany;
import javax.persistence.Table;
import org.hibernate.annotations.Cascade;

/**
​* 課程
​*
​* @author 歐炫
​*/
@Entity
@SuppressWarnings("PersistenceUnitPresent")
@Table(catalog = "manytomany", schema = "public", name = "course")
public class Course implements Serializable {

​   private static final long serialVersionUID = 4959301416563126328L;

​   @Id
​   @Basic(optional = false)
​   @Column(nullable = false)
​   @GeneratedValue(strategy = GenerationType.IDENTITY)
​   private Long id;

​   @Column(name = "display_name")
​   private String displayName;

​   //@Cascade(org.hibernate.annotations.CascadeType.ALL)
​   @JsonIgnore
​   @ManyToMany(fetch = FetchType.EAGER, mappedBy = "courses")
​   private Set<Student> students;

​   public Course() {
​   }

​   protected Course(Long id) {
​   	this.id = id;
​   }

​   public Long getId() {
​   	return id;
​   }

​   public void setId(Long id) {
​   	this.id = id;
​   }

​   public String getDisplayName() {
​   	return displayName;
​   }

​   public void setDisplayName(String displayName) {
​   	this.displayName = displayName;
​   }

​   public Set<Student> getStudents() {
​   	return students;
​   }

​   public void setStudents(Set<Student> students) {
​   	this.students = students;
​   }

​   @Override
​   public int hashCode() {
​   	int hash = 0;
​   	hash += (id != null ? id.hashCode() : 0);
​   	return hash;
​   }

​   @Override
​   public boolean equals(Object object) {
​   	if (!(object instanceof Course)) {
​   		return false;
​   	}
​   	Course other = (Course) object;
​   	return !((this.id == null && other.id != null) || (this.id != null && !this.id.equals(other.id)));
​   }

​   @Override
​   public String toString() {
​   	return "com.example.manytomany.entity.Course[ id=" + id + " ]";
​   }
}

在擁有者方(student.java),注意到這段

@JoinTable(name = "course_rating", joinColumns = @JoinColumn(name = "student_id"), inverseJoinColumns = @JoinColumn(name = "course_id"))
@ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
	private Set<Course> courses;
  • @JoinTable (name = "<中介表的名稱>",
  • joinColumns @JoinColumn(name = "<中介表關聯至 student(擁有者方)的欄位名稱>")
  • inverseJoinColumns @JoinColumn(name = "<中介表關聯至 course(目標方)的欄位名稱>")

而在目標方(course.java),我們只需提供映射關係的字段名稱。

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

Using a Composite Key in JPA (複合鍵)

以學生-課程為例,學生要對課程做評分,沒辦法儲存在學生的表中,因為一個學生可以有多門課程,反過來說要存在課程中也不是一個好方法。

這時我們就可以在關係表(中介表)中加入屬性(欄位),跟上面的基礎多對多模型幾乎相同,只是多加了一個屬性 rating

Creating a Composite Key in JPA

由於我們將數據庫屬性映射到 JPA 中的 class 字段,因此我們需要為該仲介表創建一個新的 Entity。

不過因為我們的 primary key 是一個組合鍵,我們必須另外新增一個 class,來存放鍵的不同部分。

@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
}
  • 我們必須用@Embeddable標記它
  • 它必須實現java.io.Serializable
  • 我們需要提供hashcode()和equals()方法的實現
  • 這些字段本身都不可以是實體

Using a Composite Key in JPA

使用此複合類,我們可以對中介表建立一個 entity class 模型

@Entity
class CourseRating {
 
    @EmbeddedId
    CourseRatingKey id;
 
    @ManyToOne
    @MapsId("student_id")
    @JoinColumn(name = "student_id")
    Student student;
 
    @ManyToOne
    @MapsId("course_id")
    @JoinColumn(name = "course_id")
    Course course;
 
    int rating;
     
    // standard constructors, getters, and setters
}

此代碼與常規 entity 實現非常相似。但是,我們有一些主要區別:

  • 我們使用 @EmbeddedId 標記主鍵,它是 CourseRatingKey 類的實例
  • 我們用@MapsId標記了學生和課程的 field
  • @MapsId意味著我們將這些字段綁定到鍵的一部分,它們是多對一關係的外鍵。我們需要它,因為如上所述,在組合鍵中我們沒有實體。

↑以上就完成了 entity的配置↑

接下來在 student 跟 course 的 repository層中各寫一個 method

public Student findOneById(Long id);
public Course findOneById(Long id);

再接著,我們就可以在 controller 層中去呼叫 repository 的 method

	@GetMapping(path = "/course/{id}.json", produces = "application/json;charset=UTF-8")
	ResponseEntity<Course> course(@PathVariable("id") Long id) {
		Optional<Course> optional = courseRepository.findById(id);
		if (optional.isPresent()) {
			return ResponseEntity.ok(optional.get());
		}
		return ResponseEntity.notFound().build();
	}
	@GetMapping(path = "/student/{id}.json", produces = "application/json;charset=UTF-8")
	ResponseEntity<Student> student(@PathVariable("id") Long id) {
		Optional<Student> optional = studentRepository.findById(id);
		if (optional.isPresent()) {
			return ResponseEntity.ok(optional.get());
		}
		return ResponseEntity.notFound().build();
	}

資料庫的部分

所以當我們到頁面上查詢學生 id=1 的時候,就應該要顯示出他擁有的兩門課程 國語 跟 英文

詳細的程式碼可參考github

過程中碰到的問題(待解決):

一開始執行程式時會出現 stackOverFlowError 的錯誤

這是因為一個雙向多對多的關係,在使用 fetch = FetchType.EAGER 的時候,會形成無限遞迴,呼叫A程式的 Collection<Course>,而B程式中又會呼叫A的 Collection<Student>,此時我們可以在其中一個 entity 中的 Collection 加入一個 @JsonIgnore ,不過這樣一來,就只有查詢學生的時候可以得到課程,而課程沒辦法查到學生了。

Student.java

@JoinTable(name = "course_rating", joinColumns = @JoinColumn(name = "student_id"), inverseJoinColumns = @JoinColumn(name = "course_id"))
@ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
private Collection<Course> courseCollection;

Course.java

@Fetch(value = FetchMode.SUBSELECT)
@ManyToMany(mappedBy = "courseCollection", fetch = FetchType.EAGER)
private Collection<Student> studentCollection;

@Fetch定義資料庫關聯

  • FetchMode 定義如何獲取關聯資料,參數如下:
    • JOIN: 使用JOIN關聯資料表,可以載入 related entities, collections 或 joins
    • SELECT:使用SELECT關聯資料表,可以載入 related entities, collections 或 joins
    • SUBSELECT:使用子查詢來關聯資料表,只適用 collections

Hibernate throws MultipleBagFetchException - cannot simultaneously fetch multiple bags


多對多的資料寫入 創建C 修改U

@PostMapping(path = "/student/{id}.json", produces = "application/json;charset=UTF-8")
	@SuppressWarnings("UnusedAssignment")
	ResponseEntity<Student> student(@PathVariable Long id, @RequestParam(defaultValue = "") String fullName, @RequestParam(name = "courses") Set<Course> courses) {
		Student student = null;
		Optional<Student> optional = studentRepository.findById(id);
		if (!optional.isPresent()) {
			return ResponseEntity.notFound().build();
		}
		student = optional.get();

		if (null == fullName || fullName.isEmpty()) {
			fullName = Long.toHexString(new GregorianCalendar().getTimeInMillis());
		}
		student.setFullName(fullName);

		student.setCourses(courses);

		return ResponseEntity.ok(studentRepository.saveAndFlush(student));
	}
  • method 的參數中,我們直接帶入了@RequestParam(name = "courses") Set<Course> courses,這樣一來我們在 setCourses時就直接 student.setCourses(courses); 直接帶入即可

  • if (null == fullName || fullName.isEmpty()) { fullName = Long.toHexString(new GregorianCalendar().getTimeInMillis()); } 這邊在做的只是沒有給予 fullName 參數的值時,會隨機產生一個值

  • 最後直接回傳 studentRepository.saveAndFlush(student)

demomo結果:

如果對一個學生增加一個課程是沒有問題的
但如果是一個學生增加數個課程會出現這樣的錯誤

java.lang.IllegalStateException: Multiple representations of the same entity

這時將 Course.java 裡
@ManyToMany 括號中的 cascade = CascadeType.ALL 拿掉就沒問題了,主要是因為 CasadeTyepe.merge 這個屬性所造成
解決辦法參考來源

多對多的資料新增與修改

@PostMapping(path = "/course/{id}.json", produces = "application/json;charset=UTF-8")
	@SuppressWarnings("UnusedAssignment")
	ResponseEntity<Course> course(@PathVariable("id") Long id, @RequestParam(name = "students") Set<Student> students) {
		Course course = null;
		Optional<Course> optional = courseRepository.findById(id);
		if (!optional.isPresent()) {
			return ResponseEntity.notFound().build();
		}
		course = optional.get();

		course.setStudents(students);

		return ResponseEntity.ok(courseRepository.saveAndFlush(course));
	}