Trong bài viết này, chúng ta sẽ cùng nhau tìm hiểu một annotation cuối cùng trong JPA, ngoài @ManyToOne, @OneToOne, @OneToMany, để thể hiện mối quan hệ giữa hai bảng bất kỳ trong database. Đó chính là annotation @ManyToMany! Trong trường hợp 2 bảng bất kỳ có quan hệ nhiều nhiều, ví dụ như một developer có thể làm trong nhiều dự án khác nhau và một dự án thì lại có nhiều developer, thì các bạn có thể sử dụng annotation @ManyToMany để thể hiện mối quan hệ đó. Hãy cùng mình tìm hiểu cách sử dụng annotation này trong bài viết này các bạn nhé!
Đầu tiên, mình sẽ tạo một Maven project để làm ví dụ:
Mình sẽ sử dụng Hibernate làm implementation của JPA nên sẽ thêm Hibernate dependency như sau:
1 2 3 4 5 |
<dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-core</artifactId> <version>5.2.11.Final</version> </dependency> |
MySQL Connector:
1 2 3 4 5 |
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>6.0.6</version> </dependency> |
Giả sử bây giờ mình định nghĩa cấu trúc của 2 bảng developer và project có nội dung như sau:
1 2 3 4 5 6 7 8 9 10 11 |
CREATE TABLE `developer` ( `id` int(11) NOT NULL, `name` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE `project` ( `id` int(11) NOT NULL, `name` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`), ) ENGINE=InnoDB DEFAULT CHARSET=utf8; |
Như các bạn thấy, mình đã định nghĩa bảng developer với 2 cột là id và name trong đó cột id là primary key. Còn trong bảng project, mình cũng định nghĩa 2 cột id và name với primary key cũng là id.
Ở đây, chúng ta không thể định nghĩa các cột foreign key từ bảng này sang bảng kia bởi vì chúng sẽ không thể hiện được mối quan hệ nhiều nhiều giữa chúng. Ví dụ như: nếu mình định nghĩa một column project_id trong bảng developer thì mình chỉ định nghĩa được một project mà developer đó thuộc về, không thể định nghĩa nhiều project được. Ngược lại đối với bảng project cũng thế.
Một giải pháp để giải quyết vấn đề này chúng ta có thể làm đó là định nghĩa một bảng nữa để thể hiện mối quan hệ nhiều nhiều giữa bảng developer và project. Bảng này sẽ chứa id của developer tương ứng với id của project, còn được gọi là joinTable, cụ thể như sau:
1 2 3 4 5 6 |
CREATE TABLE `developer_project` ( `developer_id` int(11) NOT NULL, `project_id` int(11) NOT NULL, FOREIGN KEY (`developer_id`) REFERENCES `developer` (`id`), FOREIGN KEY (`project_id`) REFERENCES `project` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; |
Bây giờ thì chúng ta có thể định nghĩa một developer có thể thuộc về nhiều project và một project có thể chứa nhiều developer rồi!
Entity của 2 bảng developer và project lúc này có thể định nghĩa với annotation @ManyToMany như sau:
Entity Developer:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
package com.huongdanjava.jpamanytomany; import javax.persistence.*; import java.util.Collection; @Entity @Table public class Developer { @Id @GeneratedValue private Integer id; @Column private String name; @ManyToMany @JoinTable(name = "developer_project", joinColumns = @JoinColumn(name = "developer_id"), inverseJoinColumns = @JoinColumn(name = "project_id")) private Collection<Project> projects; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Collection<Project> getProjects() { return projects; } public void setProjects(Collection<Project> projects) { this.projects = projects; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Developer developer = (Developer) o; if (!id.equals(developer.id)) return false; return name.equals(developer.name); } @Override public int hashCode() { int result = id.hashCode(); result = 31 * result + name.hashCode(); return result; } } |
Entity Project:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
package com.huongdanjava.jpamanytomany; import javax.persistence.*; import java.util.Collection; @Entity @Table public class Project { @Id @GeneratedValue private Integer id; @Column private String name; @ManyToMany(mappedBy = "projects") private Collection<Developer> developers; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Collection<Developer> getDevelopers() { return developers; } public void setDevelopers(Collection<Developer> developers) { this.developers = developers; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Project project = (Project) o; if (!id.equals(project.id)) return false; return name.equals(project.name); } @Override public int hashCode() { int result = id.hashCode(); result = 31 * result + name.hashCode(); return result; } } |
Chúng ta sẽ đặt annotation @ManyToMany cùng với khai báo một biến Collection trong cả hai entity Developer và Project và để thể hiện mối quan hệ nhiều nhiều, chúng ta sẽ sử dụng một annotation khác tên là @JoinTable trong một trong hai entity này. Các bạn có thể đặt annotation @JoinTable ở đâu cũng được, nếu annotation này nằm trong entity @Developer thì trong entity Project các bạn phải khai báo thêm thuộc tính mappedBy trong annotation @ManyToMany và ngược lại.
Trong annotation @JoinTable mà mình định nghĩa trong entity Developer, ở đây chúng ta có 3 thuộc tính chúng ta cần phải khai báo:
- name: một là tên của joinTable (là bảng developer_project đó các bạn),
- joinColumns: hai là tên column trong bảng joinTable mà bảng developer sẽ foreign key tới,
- inverseJoinColumns: ba là tên column trong bảng joinTable mà bảng project sẽ foreign key tới.
OK, vậy là chúng ta đã định nghĩa xong entity Developer và entity Project với annotation @ManyToMany để thể hiện mối quan hệ nhiều nhiều giữa 2 bảng: developer và project. Bây giờ hãy thử làm ví dụ để xem nó làm việc như thế nào nhé các bạn!
Mình sẽ thêm tập tin cấu hình cho JPA, persistence.xml, nằm trong thư mục /src/main/resources/META-INF với nội dung như sau:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
<persistence version="2.1" xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence_2_1.xsd"> <persistence-unit name="jpaexample" transaction-type="RESOURCE_LOCAL"> <class>com.huongdanjava.jpamanytomany.Developer</class> <class>com.huongdanjava.jpamanytomany.Project</class> <exclude-unlisted-classes>true</exclude-unlisted-classes> <properties> <property name="javax.persistence.jdbc.driver" value="com.mysql.jdbc.Driver" /> <property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/jpaexample" /> <property name="javax.persistence.jdbc.user" value="root" /> <property name="javax.persistence.jdbc.password" value="123456" /> <property name="hibernate.format_sql" value="true" /> <property name="hibernate.use_sql_comments" value="true" /> </properties> </persistence-unit> </persistence> |
Giả sử trong database mình đang có những dữ liệu sau:
thì khi chạy ví dụ sau:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
package com.huongdanjava.jpamanytomany; import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; import javax.persistence.EntityTransaction; import javax.persistence.Persistence; public class Application { public static void main(String[] args) { EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpaexample"); EntityManager em = emf.createEntityManager(); EntityTransaction transaction = em.getTransaction(); Project project = em.find(Project.class, 1); System.out.println("Project name: " + project.getName()); project.getDevelopers().forEach( (developer) -> System.out.println(developer.getName()) ); em.close(); emf.close(); } } |
Kết quả sẽ như sau:
Anh
cho em hỏi nếu lớp developer_project có thêm 1 thuộc tính khác thì phải làm sao ạ?
Hưng
Cho mình hỏi chỗ Override cái hàm Hashcode và Equals đó có ảnh hưởng gì ko vậy?
Khanh Nguyen
Có ảnh hưởng bạn nhé, dùng để identify entity trả về đó bạn.
Võ Huy Hiếu
cho em hỏi nếu lớp developer_project có thêm 1 thuộc tính khác thì phải làm sao ạ?
Khanh Nguyen
Em generate lại hàm equals() và hashcode() nhé
Tuấn
Cám ơn vì bài viết cảu a .
cho em hỏi về việc “phải khai báo thêm thuộc tính mappedBy”
Khi nào thì mình khai báo mappedBy còn khi nào thì ko cần hả a .vì e có thấy vài ví dụ ko cần khai báo nó vẫn chạy bình thường.
Minh Tâm
Chào a Khanh.
Em cảm ơn anh về loạt bài rất dễ hiểu ạ.E có làm theo hướng dẫn của a về manytomany và sử dụng thêm phần JPAQL + RESTful. Nhưng khi e ResponseEntity(List, HttpStatus.OK); về Json thì dữ liệu trả về sai.
A có thể viết bài hướng dẫn về manytomany + JPAQL + RESTful không ạ?
Cảm ơn a.
Khanh Nguyen
Chào bạn,
Bạn có thể tạo New Issue trên trang GitHub tại https://github.com/huongdanjavacom/huongdanjava.com/issues để nhắc Khanh nhớ nhé!
Ngọc Sơn
Chào anh Khanh Nguyen, e có làm theo a e dùng Spring MVC và sử dụng Hibernate nhưng ko hiểu tại sao nó cứ báo lỗi failed to lazily initialize a collection of role mong a giải đáp dùm e. Thanks a.
Khanh Nguyen
Em gửi cho anh nguyên cái exception nhé.
Ngọc Sơn
29-Oct-2017 10:16:28.845 WARN [http-nio-8080-exec-1] org.hibernate.engine.jdbc.spi.SqlExceptionHelper.logExceptions SQL Error: 1054, SQLState: 42S22
29-Oct-2017 10:16:28.848 ERROR [http-nio-8080-exec-1] org.hibernate.engine.jdbc.spi.SqlExceptionHelper.logExceptions Unknown column ‘pros0_.project_proID’ in ‘field list’
lỗi này nè a nó ko vô đc database
Khanh Nguyen
Không có column “Unknown column ‘pros0_.project_proID’ in ‘field list’” trong database đó em!
Ngọc Sơn
ok a e sửa đc rồi mà ko hiểu tại sao e bỏ đi @ManyToMany(mappedBy = “projects”)
private Collection developers; thì nó lại chạy đc 😀
Trần Văn Minh
Em cảm ơn anh, anh viết rất dễ hiểu ạ
Giả sử như trong bảng developer_project yêu cầu thêm 1 cột “Nhiệm vụ” để thể hiện nhiệm vụ của devloper trong project đó thì mình thiết kế sao a.
Em giả sử thôi vì thường database thiết kế bảng n-n sẽ sinh ra cột phụ, chứ không đơn giản thì có 2 cột vừa làm khóa chính vừa làm khóa ngoại như thế này
Khanh Nguyen
Hi bạn,
Khanh đang plan để viết hướng dẫn cho phần này.
Bạn theo dõi Hướng Dẫn Java để cập nhập nhé!
Trần Văn Minh
huhu,e đang làm cần làm project phần này a gợi ý giúp e trước em nghiên cứu thêm được không ạ?
em nghĩ là tạo 1 entity mới developer_project, sau đó dùng manytoone và onetomany để nối giữa entity này với 2 entity developer_project, làm vậy được không anh? Cấu trúc 3 entity bây giờ sẽ là
public class Project {
…
@OneToMany(mappedBy = “project”)
private Collection dev_pro;
}
public class Developer{
…
@OneToMany(mappedBy = “developer”)
private Collection dev_pro;
}
public class Developer_Project {
…
@ManytoOne(mappedBy = “developer”)
@JoinColumn(name = “developer_id”
private Developer developer;
@ManytoOne(mappedBy = “project”)
@JoinColumn(name = “project_id”
private Project project;
@Column
private String nhiemvu;
}
Khanh Nguyen
Xem hướng dẫn ở đây nhé em: https://huongdanjava.com/vi/moi-quan-he-nhieu-nhieu-voi-extra-columns-trong-jpa.html
Trần Văn Minh
Dạ em cám ơn anh, em đọc rồi ạ.