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
注解
✅ 或者直接通过 @EntityGraph
的 attributePaths
参数定义“临时”实体图(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