1. 概述

简单来说,实体图(Entity Graphs)是 JPA 2.1 中描述查询的另一种方式,我们可以使用它们来构建性能更优的查询语句。

在本教程中,我们将通过一个简单的例子来学习如何在 Spring Data JPA 中使用实体图。

2. 实体定义

首先,我们定义一个名为 Item 的实体,它包含多个特征(Characteristic):

@Entity
public class Item {

    @Id
    private Long id;
    private String name;
    
    @OneToMany(mappedBy = "item")
    private List<Characteristic> characteristics = new ArrayList<>();

    // getters and setters
}

接下来是 Characteristic 实体的定义:

@Entity
public class Characteristic {

    @Id
    private Long id;
    private String type;
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn
    private Item item;

    //Getters and Setters
}

如上所示,Item 实体中的 characteristics 字段和 Characteristic 实体中的 item 字段都配置为懒加载(FetchType.LAZY)。
我们的目标是:在运行时强制它们以 eager 方式加载。

3. 实体图的使用

在 Spring Data JPA 中,我们可以通过以下两种方式定义实体图:

✅ 使用 @NamedEntityGraph + @EntityGraph 注解
✅ 或者直接通过 @EntityGraphattributePaths 参数定义“临时”实体图(ad-hoc)

下面我们分别来看这两种方式。

3.1. 使用 @NamedEntityGraph

我们可以在 Item 实体上使用 JPA 提供的 @NamedEntityGraph 注解:

@Entity
@NamedEntityGraph(name = "Item.characteristics",
    attributeNodes = @NamedAttributeNode("characteristics")
)
public class Item {
    //...
}

然后在 Repository 方法上使用 @EntityGraph 来引用该实体图:

public interface ItemRepository extends JpaRepository<Item, Long> {

    @EntityGraph(value = "Item.characteristics")
    Item findByName(String name);
}

如上代码所示,我们在 @EntityGraph 中引用了之前定义的实体图名称。
默认情况下,type 参数为 EntityGraphType.FETCH,这意味着 Spring Data JPA 会将指定字段设置为 eager 加载,其他字段则仍为 lazy。

因此,即使我们在 @OneToMany 中设置了懒加载,该查询仍会 eager 加载 characteristics 字段。

⚠️ 注意:
如果字段本身定义为 FetchType.EAGER,那么即使使用实体图也无法将其改为 lazy 加载。这是设计上的限制,因为后续操作可能需要这些字段的数据。

3.2. 不使用 @NamedEntityGraph

如果我们不想在实体上定义命名实体图,也可以直接在 Repository 方法中使用 attributePaths 来定义 ad-hoc 实体图。

例如,我们希望 Characteristic 查询时 eager 加载其父 Item

public interface CharacteristicsRepository 
  extends JpaRepository<Characteristic, Long> {
    
    @EntityGraph(attributePaths = {"item"})
    Characteristic findByType(String type);    
}

这样,即使我们在实体中定义的是懒加载,这个查询仍会 eager 加载 item 字段。

这种方式更灵活,适合不需要复用的场景。

4. 测试验证

我们编写一个测试类来验证实体图是否生效:

@DataJpaTest
@RunWith(SpringRunner.class)
@Sql(scripts = "/entitygraph-data.sql")
public class EntityGraphIntegrationTest {
   
    @Autowired
    private ItemRepository itemRepo;
    
    @Autowired
    private CharacteristicsRepository characteristicsRepo;
    
    @Test
    public void givenEntityGraph_whenCalled_shouldRetrunDefinedFields() {
        Item item = itemRepo.findByName("Table");
        assertThat(item.getId()).isEqualTo(1L);
    }
    
    @Test
    public void givenAdhocEntityGraph_whenCalled_shouldRetrunDefinedFields() {
        Characteristic characteristic = characteristicsRepo.findByType("Rigid");
        assertThat(characteristic.getId()).isEqualTo(1L);
    }
}

生成的 SQL 对比

当使用 @EntityGraph 时,Hibernate 生成的 SQL 会包含关联表的 join:

select 
    item0_.id as id1_10_0_,
    characteri1_.id as id1_4_1_,
    item0_.name as name2_10_0_,
    characteri1_.item_id as item_id3_4_1_,
    characteri1_.type as type2_4_1_,
    characteri1_.item_id as item_id3_4_0__,
    characteri1_.id as id1_4_0__
from 
    item item0_ 
left outer join 
    characteristic characteri1_ 
on 
    item0_.id=characteri1_.item_id 
where 
    item0_.name=?

如果不加 @EntityGraph,生成的 SQL 只会查询主表字段:

select 
    item0_.id as id1_10_,
    item0_.name as name2_10_ 
from 
    item item0_ 
where 
    item0_.name=?

同样,对于 CharacteristicsRepository 的查询,使用 @EntityGraph 后也会 join item 表:

select 
    characteri0_.id as id1_4_0_,
    item1_.id as id1_10_1_,
    characteri0_.item_id as item_id3_4_0_,
    characteri0_.type as type2_4_0_,
    item1_.name as name2_10_1_ 
from 
    characteristic characteri0_ 
left outer join 
    item item1_ 
on 
    characteri0_.item_id=item1_.id 
where 
    characteri0_.type=?

而未使用实体图时,只查询 characteristic 表字段:

select 
    characteri0_.id as id1_4_,
    characteri0_.item_id as item_id3_4_,
    characteri0_.type as type2_4_ 
from 
    characteristic characteri0_ 
where 
    characteri0_.type=?

5. 总结

通过本教程,我们学习了如何在 Spring Data JPA 中使用实体图来控制关联字段的加载策略。

✅ 优点:

  • 灵活控制 eager / lazy 加载行为
  • 避免 N+1 查询问题
  • 可以在 Repository 层直接定义,无需修改实体类

✅ 使用建议:

  • 对于需要复用的实体图,使用 @NamedEntityGraph
  • 对于单次使用的场景,使用 @EntityGraph(attributePaths = {...}) 更简洁

GitHub 示例源码:Spring Data JPA Query Examples


原始标题:Spring Data JPA and Named Entity Graphs | Baeldung