Trong bài viết trước, mình đã giới thiệu với các bạn về annotation @ManyToMany để thể hiện mối quan hệ nhiều-nhiều giữa 2 bảng bất kỳ trong database, 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. Nhưng thực tế là chúng ta cần lưu thêm một số thông tin khác liên quan đến 2 bảng này, ví dụ như trong dự án thì developer có thể được giao để làm một task nào đó. Trong trường hợp này, chúng ta phải thể hiện bằng các entity trong JPA như thế nào? Trong bài viết này, chúng ta hãy cùng nhau tìm hiểu nhé các bạn!
Đầ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.6.1.Final</version> </dependency> |
MySQL Connector:
1 2 3 4 5 |
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.27</version> </dependency> |
Project Lombok:
1 2 3 4 5 6 |
<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.22</version> <scope>provided</scope> </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; |
Để thể hiện mối quan hệ nhiều-nhiều giữa 2 bảng trên, mình cũng định nghĩa một joinTable 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; |
Như mình ví dụ ở đầu bài viết, giờ mình muốn lưu trữ thêm thông tin developer sẽ làm gì trong dự án. Như các bạn thấy, ở đây chúng ta chỉ có thể lưu thông tin này trong joinTable mà thôi. Do đó, mình sẽ thêm một column trong bảng developer_project như sau:
1 2 3 4 5 6 7 |
CREATE TABLE `developer_project` ( `developer_id` int(11) NOT NULL, `project_id` int(11) NOT NULL, `task` varchar(255) DEFAULT NULL, FOREIGN KEY (`developer_id`) REFERENCES `developer` (`id`), FOREIGN KEY (`project_id`) REFERENCES `project` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; |
Để thể hiện những table này qua entity, chúng ta không thể dùng annotation @ManyToMany được nữa, lí do là bởi vì nếu các bạn sử dụng annotation @ManyToMany, thông tin của joinTable sẽ bị ẩn, và do đó chúng ta không thể hiện được thông tin của column task trong table developer_project.
Giải pháp cho vấn đề này, chúng ta có thể dùng annotation @OneToMany hai chiều. Cụ thể như thế nào, các bạn đọc tiếp nhé!
Để sử dụng annotation @OneToMany hai chiều, chúng ta cần làm những bước sau:
- Đầu tiên, các bạn cần xây dựng entity cho table developer_project.
Vì bảng developer_project có chứa một Composite Primary Key nên chúng ta sẽ định nghĩa một class với annotation @Embeddable như sau:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
package com.huongdanjava.jpamanymanyextracolumns; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import javax.persistence.Column; import javax.persistence.Embeddable; import java.io.Serializable; @Embeddable @Data @AllArgsConstructor @NoArgsConstructor public class DeveloperProjectId implements Serializable { @Column(name = "developer_id") private Integer developerId; @Column(name = "project_id") private Integer projectId; } |
Entity cho bảng developer_project sẽ có 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 22 23 24 25 |
package com.huongdanjava.jpamanymanyextracolumns; import lombok.Data; import javax.persistence.*; @Entity @Table(name = "developer_project") @Data public class DeveloperProject { @EmbeddedId private DeveloperProjectId developerProjectId; @ManyToOne @MapsId("developerId") private Developer developer; @ManyToOne @MapsId("projectId") private Project project; @Column private String task; } |
Trong entity này, mình đã sử dụng annotation @ManyToOne để thể hiện mối quan hệ nhiều-một giữa bảng developer_project với các bảng developer và project.
Ở đây, mình cũng đã sử dụng một annotation @MapsId. Annotation này được sử dụng trong entity có khai báo annotation @EmbeddedId để map với cột primary key được định nghĩa trong đối tượng Embeddable. Do đó, value của annotation @MapsId sẽ là tên biến trong đối tượng @Embeddable map với cột primary key.
- Bước tiếp theo là chúng ta sẽ định nghĩa entity cho 2 bảng developer và project.
Entity của 2 bảng này sẽ sử dụng annotation @OneToMany:
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 |
package com.huongdanjava.jpamanymanyextracolumns; import java.io.Serializable; import java.util.List; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.OneToMany; import javax.persistence.Table; import lombok.Data; @Table @Entity @Data public class Developer implements Serializable { @Id @GeneratedValue private Integer id; @Column private String name; @OneToMany(mappedBy = "developer") private List<DeveloperProject> projects; } |
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 |
package com.huongdanjava.jpamanymanyextracolumns; import java.io.Serializable; import java.util.List; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.OneToMany; import javax.persistence.Table; import lombok.Data; @Table @Entity @Data public class Project implements Serializable { @Id @GeneratedValue private Integer id; @Column private String name; @OneToMany(mappedBy = "project") private List<DeveloperProject> developers; } |
OK, vậy là chúng ta đã định nghĩa xong các entity rồi đó các bạn. Giờ thử chạy ví dụ xem nào.
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 22 |
<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.jpamanymanyextracolumns.Project</class> <class>com.huongdanjava.jpamanymanyextracolumns.Developer</class> <class>com.huongdanjava.jpamanymanyextracolumns.DeveloperProject</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.jpamanymanyextracolumns; 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: " + project.getName()); project.getDevelopers().forEach( (developer) -> System.out.println("Developer: "+ developer.getDeveloper().getName() + " | Task: " + developer.getTask()) ); em.close(); emf.close(); } } |
Kết quả sẽ là:
Việt
Dạ thầy cho em hỏi nếu trong Entity developer_project sinh thêm thuộc tính id để làm khóa ngoại cho Entity khác thì trường hợp này đúng không ạ
Khanh Nguyen
Sao bạn cần phải làm như vậy?
Minh Chiến
private List projects;
private List developers;
Minh Chiến
Anh bị nhầm đoạn mã mappedBy, phải nên giống bên dưới:
Class Developer:
@OneToMany(mappedBy = “developer”)
private List projects;
Class Project:
@OneToMany(mappedBy = “project”)
private List projects;
Kết quả: Developer: Khanh | Task: Code
Developer: Quan | Task: Test
Khanh Nguyen
Cảm ơn bạn, mình đã sửa rồi nhé!
lâm
anh ơi em bị lỗi này là sao ạ
Exception in thread “main” org.springframework.orm.jpa.JpaSystemException: Could not set field value [2] value by reflection : [class model.ABKey.akey] setter of model.ABKey.akey; nested exception is org.hibernate.PropertyAccessException: Could not set field value [2] value by reflection : [class model.ABKey.akey] setter of model.ABKey.akey
at org.springframework.orm.jpa.vendor.HibernateJpaDialect.convertHibernateAccessException(HibernateJpaDialect.java:351)
at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:253)
at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.translateExceptionIfPossible(AbstractEntityManagerFactoryBean.java:527)
at org.springframework.dao.support.ChainedPersistenceExceptionTranslator.translateExceptionIfPossible(ChainedPersistenceExceptionTranslator.java:61)
at org.springframework.dao.support.DataAccessUtils.translateIfNecessary(DataAccessUtils.java:242)
at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:153)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.data.jpa.repository.support.CrudMethodMetadataPostProcessor$CrudMethodMetadataPopulatingMethodInterceptor.invoke(CrudMethodMetadataPostProcessor.java:135)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:93)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.data.repository.core.support.SurroundingTransactionDetectorMethodInterceptor.invoke(SurroundingTransactionDetectorMethodInterceptor.java:61)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:212)
at com.sun.proxy.$Proxy49.save(Unknown Source)
at Main.Main.main(Main.java:23)