1. 简介

本文将快速介绍如何使用 @ManyToMany 注解在 Hibernate 中定义多对多关系。对于有经验的开发者来说,这是处理复杂实体关联的必备技能,直接上干货。

2. 典型示例

先看一个简单的实体关系图(ER图),展示了 employeeproject 两个实体间的多对多关联:

多对多关系ER图

在这个场景中:

  • ✅ 一个员工可以参与多个项目
  • ✅ 一个项目可以有多个员工参与
  • ❌ 这就形成了典型的多对多关系

数据库设计需要三个表:

  1. employee 表(主键 employee_id
  2. project 表(主键 project_id
  3. 关联表 employee_project(包含两个外键)

3. 数据库设置

假设已创建名为 spring_hibernate_many_to_many 的数据库。执行以下 SQL 创建表结构:

CREATE TABLE `employee` (
  `employee_id` int(11) NOT NULL AUTO_INCREMENT,
  `first_name` varchar(50) DEFAULT NULL,
  `last_name` varchar(50) DEFAULT NULL,
  PRIMARY KEY (`employee_id`)
) ENGINE=InnoDB AUTO_INCREMENT=17 DEFAULT CHARSET=utf8;

CREATE TABLE `project` (
  `project_id` int(11) NOT NULL AUTO_INCREMENT,
  `title` varchar(50) DEFAULT NULL,
  PRIMARY KEY (`project_id`)
) ENGINE=InnoDB AUTO_INCREMENT=18 DEFAULT CHARSET=utf8;

CREATE TABLE `employee_project` (
  `employee_id` int(11) NOT NULL,
  `project_id` int(11) NOT NULL,
  PRIMARY KEY (`employee_id`,`project_id`),
  KEY `project_id` (`project_id`),
  CONSTRAINT `employee_project_ibfk_1` 
   FOREIGN KEY (`employee_id`) REFERENCES `employee` (`employee_id`),
  CONSTRAINT `employee_project_ibfk_2` 
   FOREIGN KEY (`project_id`) REFERENCES `project` (`project_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

⚠️ 注意:关联表必须使用复合主键(employee_id + project_id)确保关系唯一性。

4. 模型类

使用 JPA 注解创建 EmployeeProject 实体类:

Employee.java

@Entity
@Table(name = "Employee")
public class Employee { 
    // ...
 
    @ManyToMany(cascade = { CascadeType.ALL })
    @JoinTable(
        name = "Employee_Project", 
        joinColumns = { @JoinColumn(name = "employee_id") }, 
        inverseJoinColumns = { @JoinColumn(name = "project_id") }
    )
    Set<Project> projects = new HashSet<>();
   
    // standard constructor/getters/setters
}

Project.java

@Entity
@Table(name = "Project")
public class Project {    
    // ...  
 
    @ManyToMany(mappedBy = "projects")
    private Set<Employee> employees = new HashSet<>();
    
    // standard constructors/getters/setters   
}

关键注解解析

  1. @ManyToMany

    • 标记多对多关系
    • 双向关联:两端都需配置
    • 级联操作:Employee 端配置 CascadeType.ALL
  2. **@JoinTable**(关系维护端)

    • 指定关联表名称
    • joinColumns:当前实体在关联表中的外键
    • inverseJoinColumns:对方实体的外键
  3. **mappedBy**(被维护端)

    • 指定由 Employeeprojects 字段维护关系
    • 避免重复生成关联表

⚠️ 踩坑提示:双向关联必须正确设置 mappedBy,否则会导致数据不一致问题。

5. 执行测试

通过 JUnit 测试验证多对多关系:

public class HibernateManyToManyAnnotationMainIntegrationTest {
    private static SessionFactory sessionFactory;
    private Session session;

    //...

    @Test
        public void givenSession_whenRead_thenReturnsMtoMdata() {
        prepareData();
               @SuppressWarnings("unchecked")
        List<Employee> employeeList = session.createQuery("FROM Employee").list();
            @SuppressWarnings("unchecked")
        List<Project> projectList = session.createQuery("FROM Project").list();
            assertNotNull(employeeList);
            assertNotNull(projectList);
            assertEquals(2, employeeList.size());
            assertEquals(2, projectList.size());
        
            for(Employee employee : employeeList) {
               assertNotNull(employee.getProjects());
               assertEquals(2, employee.getProjects().size());
            }
            for(Project project : projectList) {
               assertNotNull(project.getEmployees());
               assertEquals(2, project.getEmployees().size());
            }
        }

    private void prepareData() {
        String[] employeeData = { "Peter Oven", "Allan Norman" };
        String[] projectData = { "IT Project", "Networking Project" };
        Set<Project> projects = new HashSet<Project>();

        for (String proj : projectData) {
        projects.add(new Project(proj));
        }

        for (String emp : employeeData) {
        Employee employee = new Employee(emp.split(" ")[0], emp.split(" ")[1]);
        employee.setProjects(projects);
            
            for (Project proj : projects) {
            proj.getEmployees().add(employee);
        }
            
        session.persist(employee);
        }
    }
    
    //...
}

测试验证点:

  • ✅ 创建 2 个员工和 2 个项目
  • ✅ 每个员工关联所有项目
  • ✅ 每个项目包含所有员工
  • ✅ 双向关联数据一致性

6. 总结

Hibernate 的 @ManyToMany 注解提供了一种简洁高效的多对多关系映射方案,相比传统的 XML 配置方式更加直观。关键点总结:

  1. 核心注解

    • @ManyToMany:声明多对多关系
    • @JoinTable:定义关联表结构
    • mappedBy:指定关系维护方
  2. 最佳实践

    • 使用 Set 而非 List 避免重复数据
    • 双向关联必须正确配置 mappedBy
    • 合理使用级联操作
  3. 常见陷阱

    • ❌ 忘记初始化集合(new HashSet<>()
    • ❌ 双向关联只配置一端
    • ❌ 关联表主键配置错误

完整代码示例可在 GitHub 获取。实际开发中,建议结合 Spring Data JPA 进一步简化操作。


原始标题:Hibernate Many to Many Annotation Tutorial | Baeldung