Fetch join with @Transactional 不加载它们的关系实体
Fetch join with @Transactional doesnt load their relational entities
我有 2 个实体,团队和成员,它们通过 1:N 关联。
// Team.java
@Getter
@NoArgsConstructor
@Entity
public class Team {
@Id
@Column(name = "TEAM_ID")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToMany(mappedBy = "team", fetch = FetchType.LAZY)
private List<Member> members = new ArrayList<>();
}
// Member.java
@Getter
@NoArgsConstructor
@Entity
public class Member {
@Id
@Column(name = "MEMBER_ID")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
public Member(String name) {
this.name = name;
}
public Member(String name, Team team) {
this.name = name;
this.team = team;
}
}
我正在测试获取加入,例如,通过团队 ID 查找团队及其成员。
测试代码是这样的,
// TeamRepository.java
public interface TeamRepository extends JpaRepository<Team, Long> {
@Query(value =
"select distinct t from Team t " +
"join fetch t.members " +
"where t.id = :id")
Optional<Team> findByIdWithAllMembers(Long id);
}
// Test.java
@Transactional
@Test
void transactionalFetchJoin() {
System.out.println("save team");
Team team = new Team();
Team saved = teamRepository.save(team);
System.out.println("save members");
for (int i = 0; i < 10; i++) {
Member member = new Member("name" + String.valueOf(i), team);
memberRepository.save(member);
}
System.out.println("teamRepository.findByIdWithAllMembers(saved.getId())");
Team t = teamRepository.findByIdWithAllMembers(saved.getId())
.orElseThrow(() -> new RuntimeException("ㅠㅠ"));
assertThat(t.getMembers().size()).isEqualTo(0); // <-- no members are loaded
}
@Test
void nonTransactionalFetchJoin() {
System.out.println("save team");
Team team = new Team();
Team saved = teamRepository.save(team);
System.out.println("save members");
for (int i = 0; i < 10; i++) {
Member member = new Member("name" + String.valueOf(i), team);
memberRepository.save(member);
}
System.out.println("teamRepository.findByIdWithAllMembers(saved.getId())");
Team t = teamRepository.findByIdWithAllMembers(saved.getId())
.orElseThrow(() -> new RuntimeException("ㅠㅠ"));
assertThat(t.getMembers().size()).isEqualTo(10); // <-- 10 members are loaded
}
这两个测试方法的逻辑是一样的,唯一的区别就是有没有@Transactional。还有,两个测试方法都顺利通过
我发现 'nonTransactionalFetchJoin()' 加载了 10 个成员对象的团队,但 'transactionalFetchJoin()' 没有。
此外,我观察到 2 个测试方法为所有 JPA 方法生成相同的 JPQL/SQL 查询,包括 save()。
特别是,findByIdWithAllMembers() 方法生成的查询类似于,
/* select
distinct t
from
Team t
join
fetch t.members
where
t.id = :id */ select
distinct team0_.team_id as team_id1_1_0_,
members1_.member_id as member_i1_0_1_,
members1_.name as name2_0_1_,
members1_.team_id as team_id3_0_1_,
members1_.team_id as team_id3_0_0__,
members1_.member_id as member_i1_0_0__
from
team team0_
inner join
member members1_
on team0_.team_id=members1_.team_id
where
team0_.team_id=?
唯一的区别是,在 transactionalFetchJoin() 的情况下,o.h.type.descriptor.sql.BasicExtractor 仅提取 Team.id 和 Member.id,而 nonTransactionalFetchJoin() 提取团队和成员的整个字段。
// transactionalFetchJoin
2022-03-31 13:39:19.842 TRACE 4725 --- [ Test worker] o.h.type.descriptor.sql.BasicExtractor : extracted value ([team_id1_1_0_] : [BIGINT]) - [1]
2022-03-31 13:39:19.842 TRACE 4725 --- [ Test worker] o.h.type.descriptor.sql.BasicExtractor : extracted value ([member_i1_0_1_] : [BIGINT]) - [1]
// nonTransactionalFetchJoin
2022-03-31 13:39:19.933 TRACE 4725 --- [ Test worker] o.h.type.descriptor.sql.BasicExtractor : extracted value ([team_id1_1_0_] : [BIGINT]) - [2]
2022-03-31 13:39:19.934 TRACE 4725 --- [ Test worker] o.h.type.descriptor.sql.BasicExtractor : extracted value ([member_i1_0_1_] : [BIGINT]) - [21]
2022-03-31 13:39:19.935 TRACE 4725 --- [ Test worker] o.h.type.descriptor.sql.BasicExtractor : extracted value ([name2_0_1_] : [VARCHAR]) - [name0]
为什么会出现这种差异?
谢谢。
欢迎来到被 JPA 一级缓存搞砸的开发人员俱乐部。
JPA 保留一级缓存中的实体。当您加载或保留一个实体时,它将被添加到一级缓存,直到 transaction/session/persistence 上下文结束。每当它从任何类型的加载操作中为您提供一个实体时,它都会检查该实体是否存在于缓存中并return给您。
这意味着在带有 @Transactional
注释的示例中,您实际上并未加载实体。您实际上只需加载 id,然后使用它们在缓存中查找该实体。但是缓存中的实体不知道添加的团队成员。 => 你的团队没有任何成员。
如果没有显式事务,事务只会对存储库进行一次调用。每次调用后,一级缓存都会被丢弃,您的加载操作实际上会根据从数据库加载的数据创建一个新的 Team
实例。
有几件事你应该解决。
要么一起摆脱双向关系,使之成为单向关系。或者,如果真的想让它确保在您的代码中使双方匹配。也就是说,如果您设置 Member.team
,您还应该将 Member
添加到 Team.members
。当然移除成员也是一样。这将避免在您的应用程序中看到不一致的数据。
确保您在测试中经历了正确的 JPA 生命周期。我的首选方法是使用 TransactionTemplate
将测试的不同步骤包装在单独的事务中。
如果您想仔细检查预先加载是否正常工作,您应该将根实体(在本例中为团队)的加载放在一个单独的事务中,然后在关闭该事务后尝试访问事务外急切加载的属性。这样,如果引用的实体未按预期急切加载,您将获得 LazyLoadingException
。
我有 2 个实体,团队和成员,它们通过 1:N 关联。
// Team.java
@Getter
@NoArgsConstructor
@Entity
public class Team {
@Id
@Column(name = "TEAM_ID")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToMany(mappedBy = "team", fetch = FetchType.LAZY)
private List<Member> members = new ArrayList<>();
}
// Member.java
@Getter
@NoArgsConstructor
@Entity
public class Member {
@Id
@Column(name = "MEMBER_ID")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
public Member(String name) {
this.name = name;
}
public Member(String name, Team team) {
this.name = name;
this.team = team;
}
}
我正在测试获取加入,例如,通过团队 ID 查找团队及其成员。 测试代码是这样的,
// TeamRepository.java
public interface TeamRepository extends JpaRepository<Team, Long> {
@Query(value =
"select distinct t from Team t " +
"join fetch t.members " +
"where t.id = :id")
Optional<Team> findByIdWithAllMembers(Long id);
}
// Test.java
@Transactional
@Test
void transactionalFetchJoin() {
System.out.println("save team");
Team team = new Team();
Team saved = teamRepository.save(team);
System.out.println("save members");
for (int i = 0; i < 10; i++) {
Member member = new Member("name" + String.valueOf(i), team);
memberRepository.save(member);
}
System.out.println("teamRepository.findByIdWithAllMembers(saved.getId())");
Team t = teamRepository.findByIdWithAllMembers(saved.getId())
.orElseThrow(() -> new RuntimeException("ㅠㅠ"));
assertThat(t.getMembers().size()).isEqualTo(0); // <-- no members are loaded
}
@Test
void nonTransactionalFetchJoin() {
System.out.println("save team");
Team team = new Team();
Team saved = teamRepository.save(team);
System.out.println("save members");
for (int i = 0; i < 10; i++) {
Member member = new Member("name" + String.valueOf(i), team);
memberRepository.save(member);
}
System.out.println("teamRepository.findByIdWithAllMembers(saved.getId())");
Team t = teamRepository.findByIdWithAllMembers(saved.getId())
.orElseThrow(() -> new RuntimeException("ㅠㅠ"));
assertThat(t.getMembers().size()).isEqualTo(10); // <-- 10 members are loaded
}
这两个测试方法的逻辑是一样的,唯一的区别就是有没有@Transactional。还有,两个测试方法都顺利通过
我发现 'nonTransactionalFetchJoin()' 加载了 10 个成员对象的团队,但 'transactionalFetchJoin()' 没有。
此外,我观察到 2 个测试方法为所有 JPA 方法生成相同的 JPQL/SQL 查询,包括 save()。
特别是,findByIdWithAllMembers() 方法生成的查询类似于,
/* select
distinct t
from
Team t
join
fetch t.members
where
t.id = :id */ select
distinct team0_.team_id as team_id1_1_0_,
members1_.member_id as member_i1_0_1_,
members1_.name as name2_0_1_,
members1_.team_id as team_id3_0_1_,
members1_.team_id as team_id3_0_0__,
members1_.member_id as member_i1_0_0__
from
team team0_
inner join
member members1_
on team0_.team_id=members1_.team_id
where
team0_.team_id=?
唯一的区别是,在 transactionalFetchJoin() 的情况下,o.h.type.descriptor.sql.BasicExtractor 仅提取 Team.id 和 Member.id,而 nonTransactionalFetchJoin() 提取团队和成员的整个字段。
// transactionalFetchJoin
2022-03-31 13:39:19.842 TRACE 4725 --- [ Test worker] o.h.type.descriptor.sql.BasicExtractor : extracted value ([team_id1_1_0_] : [BIGINT]) - [1]
2022-03-31 13:39:19.842 TRACE 4725 --- [ Test worker] o.h.type.descriptor.sql.BasicExtractor : extracted value ([member_i1_0_1_] : [BIGINT]) - [1]
// nonTransactionalFetchJoin
2022-03-31 13:39:19.933 TRACE 4725 --- [ Test worker] o.h.type.descriptor.sql.BasicExtractor : extracted value ([team_id1_1_0_] : [BIGINT]) - [2]
2022-03-31 13:39:19.934 TRACE 4725 --- [ Test worker] o.h.type.descriptor.sql.BasicExtractor : extracted value ([member_i1_0_1_] : [BIGINT]) - [21]
2022-03-31 13:39:19.935 TRACE 4725 --- [ Test worker] o.h.type.descriptor.sql.BasicExtractor : extracted value ([name2_0_1_] : [VARCHAR]) - [name0]
为什么会出现这种差异?
谢谢。
欢迎来到被 JPA 一级缓存搞砸的开发人员俱乐部。
JPA 保留一级缓存中的实体。当您加载或保留一个实体时,它将被添加到一级缓存,直到 transaction/session/persistence 上下文结束。每当它从任何类型的加载操作中为您提供一个实体时,它都会检查该实体是否存在于缓存中并return给您。
这意味着在带有 @Transactional
注释的示例中,您实际上并未加载实体。您实际上只需加载 id,然后使用它们在缓存中查找该实体。但是缓存中的实体不知道添加的团队成员。 => 你的团队没有任何成员。
如果没有显式事务,事务只会对存储库进行一次调用。每次调用后,一级缓存都会被丢弃,您的加载操作实际上会根据从数据库加载的数据创建一个新的 Team
实例。
有几件事你应该解决。
要么一起摆脱双向关系,使之成为单向关系。或者,如果真的想让它确保在您的代码中使双方匹配。也就是说,如果您设置
Member.team
,您还应该将Member
添加到Team.members
。当然移除成员也是一样。这将避免在您的应用程序中看到不一致的数据。确保您在测试中经历了正确的 JPA 生命周期。我的首选方法是使用
TransactionTemplate
将测试的不同步骤包装在单独的事务中。如果您想仔细检查预先加载是否正常工作,您应该将根实体(在本例中为团队)的加载放在一个单独的事务中,然后在关闭该事务后尝试访问事务外急切加载的属性。这样,如果引用的实体未按预期急切加载,您将获得
LazyLoadingException
。