1. 概述

本文将介绍 JPA 支持的几种连接类型(Join Types),并结合 JPQL 示例来说明它们的使用场景和注意事项。

如果你已经对 JPA 有基本了解,那这篇文章会帮助你更好地掌握在实际开发中如何使用连接来查询关联数据。

2. 示例数据模型

我们以一个简单的员工管理系统为例,包含三个实体:EmployeeDepartmentPhone

Employee 实体

@Entity
public class Employee {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    private String name;
    private int age;

    @ManyToOne
    private Department department;

    @OneToMany(mappedBy = "employee")
    private List<Phone> phones;

    // getters and setters...
}

Department 实体

@Entity
public class Department {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;

    private String name;

    @OneToMany(mappedBy = "department")
    private List<Employee> employees;

    // getters and setters...
}

Phone 实体

@Entity
public class Phone {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    private String number;

    @ManyToOne
    private Employee employee;

    // getters and setters...
}

3. 内连接(Inner Join)

内连接只返回满足连接条件的记录。

3.1 隐式内连接(Implicit Inner Join)

当我们通过路径表达式访问关联对象时,JPA 会自动创建隐式连接:

@Test
public void whenPathExpressionIsUsedForSingleValuedAssociation_thenCreatesImplicitInnerJoin() {
    TypedQuery<Department> query
      = entityManager.createQuery("SELECT e.department FROM Employee e", Department.class);
    List<Department> resultList = query.getResultList();
}

✅ 这里 JPA 会自动为 e.department 创建一个内连接。

3.2 显式内连接(Explicit Inner Join)

也可以使用 JOIN 显式指定连接:

@Test
public void whenJoinKeywordIsUsed_thenCreatesExplicitInnerJoin() {
    TypedQuery<Department> query
      = entityManager.createQuery("SELECT d FROM Employee e JOIN e.department d", Department.class);
    List<Department> resultList = query.getResultList();
}

也可以加上 INNER 关键字,效果一样:

@Test
public void whenInnerJoinKeywordIsUsed_thenCreatesExplicitInnerJoin() {
    TypedQuery<Department> query
      = entityManager.createQuery("SELECT d FROM Employee e INNER JOIN e.department d", Department.class);
    List<Department> resultList = query.getResultList();
}

⚠️ 注意:隐式连接只在路径表达式中自动创建。如果你不使用路径表达式,就需要显式写 JOIN

3.3 集合类型的连接(Collection-Valued Associations)

当连接的是集合类型的关联时(如 e.phones),隐式连接不能直接用于 SELECTWHERE 子句。

例如,下面的语句会返回 Collection 类型的列表,而不是 Phone 实体:

@Test
public void whenCollectionValuedAssociationIsSpecifiedInSelect_ThenReturnsCollections() {
    TypedQuery<Collection> query 
      = entityManager.createQuery("SELECT e.phones FROM Employee e", Collection.class);
    List<Collection> resultList = query.getResultList();
}

要操作集合中的元素,必须使用显式连接并为集合中的元素指定别名:

@Test
public void whenCollectionValuedAssociationIsJoined_ThenCanSelect() {
    TypedQuery<Phone> query 
      = entityManager.createQuery("SELECT ph FROM Employee e JOIN e.phones ph WHERE ph.number LIKE '1%'", Phone.class);
    List<Phone> resultList = query.getResultList();
}

4. 外连接(Outer Join)

外连接会返回满足条件的记录,以及左表(LEFT JOIN)或右表(RIGHT JOIN)中未匹配的记录。

JPA 只支持左外连接(LEFT JOIN),没有原生的 RIGHT JOIN。但你可以通过交换 FROM 子句中的实体顺序来模拟。

示例:

@Test
public void whenLeftKeywordIsSpecified_thenCreatesOuterJoinAndIncludesNonMatched() {
    TypedQuery<Department> query 
      = entityManager.createQuery("SELECT DISTINCT d FROM Department d LEFT JOIN d.employees e", Department.class);
    List<Department> resultList = query.getResultList();
}

✅ 此查询会返回所有部门,包括那些没有员工的部门。

5. WHERE 子句中的连接

5.1 带条件的连接

可以在 WHERE 子句中手动指定连接条件,适用于没有外键约束的情况:

@Test
public void whenEntitiesAreListedInFromAndMatchedInWhere_ThenCreatesJoin() {
    TypedQuery<Department> query 
      = entityManager.createQuery("SELECT d FROM Employee e, Department d WHERE e.department = d", Department.class);
    List<Department> resultList = query.getResultList();
}

5.2 无条件连接(笛卡尔积)

如果在 FROM 子句中列出多个实体但不加 WHERE 条件,会生成笛卡尔积:

@Test
public void whenEntitiesAreListedInFrom_ThenCreatesCartesianProduct() {
    TypedQuery<Department> query
      = entityManager.createQuery("SELECT d FROM Employee e, Department d", Department.class);
    List<Department> resultList = query.getResultList();
}

⚠️ 踩坑提醒:这种写法性能极差,应避免使用,除非你明确知道自己在做什么。

6. 多重连接(Multiple Joins)

你可以在一个查询中连接多个实体:

@Test
public void whenMultipleEntitiesAreListedWithJoin_ThenCreatesMultipleJoins() {
    TypedQuery<Phone> query
      = entityManager.createQuery(
          "SELECT ph FROM Employee e " +
          "JOIN e.department d " +
          "JOIN e.phones ph " +
          "WHERE d.name IS NOT NULL", Phone.class);
    List<Phone> resultList = query.getResultList();
}

✅ 这种方式可以一次获取多个层级的关联数据,但要注意性能和内存消耗。

7. Fetch Join(抓取连接)

Fetch Join 的作用是强制立即加载延迟加载的关联字段,避免 N+1 查询问题。

示例:立即加载员工列表

@Test
public void whenFetchKeywordIsSpecified_ThenCreatesFetchJoin() {
    TypedQuery<Department> query 
      = entityManager.createQuery("SELECT d FROM Department d JOIN FETCH d.employees", Department.class);
    List<Department> resultList = query.getResultList();
}

✅ 此查询会一次性加载 Department 和其关联的 employees,避免后续访问时触发懒加载。

左外 Fetch Join

也可以结合 LEFT JOIN FETCH 来加载左表中未匹配的记录:

@Test
public void whenLeftAndFetchKeywordsAreSpecified_ThenCreatesOuterFetchJoin() {
    TypedQuery<Department> query 
      = entityManager.createQuery("SELECT d FROM Department d LEFT JOIN FETCH d.employees", Department.class);
    List<Department> resultList = query.getResultList();
}

⚠️ 注意内存消耗:Fetch Join 会加载大量数据到内存,需权衡性能与内存占用。

8. 总结

本文介绍了 JPA 中常见的连接类型:

类型 关键字 说明
内连接 JOIN / INNER JOIN 只返回满足条件的记录
隐式连接 通过路径表达式自动创建
显式连接 JOIN 用于集合关联或非路径表达式
外连接 LEFT JOIN 返回左表所有记录,包括未匹配的
WHERE 中的连接 手动在 WHERE 中指定连接条件
笛卡尔积 不推荐使用
Fetch Join JOIN FETCH 强制立即加载关联数据
左外 Fetch Join LEFT JOIN FETCH 加载左表所有记录并立即加载关联

✅ 总结建议:

  • 使用路径表达式前确认是否懒加载,避免 N+1 问题。
  • 集合类型的关联必须显式 JOIN
  • 使用 FETCH 时注意内存占用。
  • 避免无条件的多表查询,防止笛卡尔积。

完整示例代码可在 GitHub 获取。


原始标题:JPA Join Types | Baeldung