获取与@transactional一起加入其关系实体
我有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;
}
}
例如,我正在测试Fetch加入,例如,通过团队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。另外,成功传递了两种测试方法。
我发现“非transactionalfetchjoin()”加载了10个成员对象的团队,但是“ TransactionalFetchJoin()”没有。
另外,我观察到2种测试方法为所有JPA方法(包括save()生成相同的JPQL/SQL查询。
尤其是,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()的情况下,Ohtype.descriptor.sql.basicextractor提取器提取器just team.ID和embers.id,而非TransactionalFetchJoin()提取整个团队的领域()和成员。
// 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]
为什么会发生这种差异?
谢谢。
I have 2 entities, Team and Member, which are related by 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;
}
}
I'm testing fetch joining, for example, find a team and their members by team id.
The test code is like this,
// 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
}
These two test methods have the same logic but the only difference is @Transactional or not. Also, two test methods are passed successfully.
I found that 'nonTransactionalFetchJoin()' loaded team with 10 member objects, but 'transactionalFetchJoin()' didn't.
Also, I observed that 2 test methods generate the same JPQL/SQL queries for all JPA methods, including save().
Especially, the findByIdWithAllMembers() method generates query like,
/* 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=?
The only difference is that, in case of transactionalFetchJoin(), o.h.type.descriptor.sql.BasicExtractor extracts just Team.id and Member.id, while nonTransactionalFetchJoin() extracts whole fields of Team and Member.
// 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]
Why does this difference occur?
Thanks.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(1)
欢迎来到被 JPA 一级缓存搞砸的开发者俱乐部。
JPA 保留第一级缓存中的实体。当您加载或持久化一个实体时,它将被添加到一级缓存中,直到事务/会话/持久化上下文结束。每当它从任何类型的加载操作中为您提供一个实体时,它都会检查该实体是否存在于缓存中并将其返回给您。
这意味着在带有
@Transactional
注释的示例中,您实际上并未加载实体。您实际上只需加载 ID,然后使用它们在缓存中查找该实体即可。但缓存中的实体不知道添加的团队成员。 =>您的团队没有任何成员。如果没有显式事务,事务只需对存储库进行一次调用。每次调用后,第一级缓存都会被丢弃,您的加载操作实际上会根据从数据库加载的数据创建一个新的 Team 实例。
您应该解决一些问题。
要么完全摆脱双向关系,使其成为单向关系。或者,如果确实想在代码中确保它使双方匹配。即,如果您设置了
Member.team
,则还应该将Member
添加到Team.members
。当然,删除成员也是同样的道理。这将避免在您的应用程序中看到不一致的数据。确保您在测试中经历正确的 JPA 生命周期。我首选的方法是使用
TransactionTemplate
将测试的不同步骤包装在单独的事务中。如果您想仔细检查急切加载是否正常工作,您应该将根实体(在本例中为团队)的加载放在单独的事务中,然后在关闭该事务后尝试访问事务外部的急切加载属性。这样,如果引用的实体未按预期立即加载,您将收到
LazyLoadingException
。Welcome to the club of developers screwed by the JPAs 1st level cache.
JPA holds on to entities in the 1st level cache. When you load or persist an entity it will be added to the 1st level cache until the end of the transaction/session/persistence context. And whenever it gives you an entity from any kind of load operation it will check if that entity exists in the cache and return that to you.
This means in the example with
@Transactional
annotation you don't actually load the entity. You effectively just load the ids and then use them to look up that entity in the cache. But the entity in the cache doesn't know about the added team members. => your team doesn't have any members.With out explicit transactions the transactions span just one call to the repository. After each call the 1st level cache gets discarded and your load operation actually creates a new
Team
instance from the data loaded from the database.There are a couple of things you should fix.
Either get rid of the bidirectional relationship all together and make it a one directional one only. Or if really want to have it ensure in your code to make both sides match. I.e if you set
Member.team
, you should als add theMember
toTeam.members
. Of course the same is true for removing members. This will avoid seeing inconsistent data in your application.Make sure you go through the proper JPA life cycle in your tests. My preferred approach to do this is to use
TransactionTemplate
to wrap the different steps of your test in separate transactions.If you want to double check that eager loading works properly you should put the loading of the root entity (team in this case) in a separate transaction and then after closing that transaction try to access the eager loaded properties outside the transaction. This way you'll get a
LazyLoadingException
if the referenced entities weren't loaded eagerly as intended.