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 实例。

有几件事你应该解决。

  1. 要么一起摆脱双向关系,使之成为单向关系。或者,如果真的想让它确保在您的代码中使双方匹配。也就是说,如果您设置 Member.team,您还应该将 Member 添加到 Team.members。当然移除成员也是一样。这将避免在您的应用程序中看到不一致的数据。

  2. 确保您在测试中经历了正确的 JPA 生命周期。我的首选方法是使用 TransactionTemplate 将测试的不同步骤包装在单独的事务中。

  3. 如果您想仔细检查预先加载是否正常工作,您应该将根实体(在本例中为团队)的加载放在一个单独的事务中,然后在关闭该事务后尝试访问事务外急切加载的属性。这样,如果引用的实体未按预期急切加载,您将获得 LazyLoadingException