【问题标题】:Hibernate + JPA fetches child associations of parent's child when using JPQL 'JOIN FETCH'Hibernate + JPA 在使用 JPQL \'JOIN FETCH\' 时获取父子的子关联
【发布时间】:2022-11-12 02:31:41
【问题描述】:

假设我们有以下实体:

用户实体

@Getter
@Setter
@SuperBuilder(toBuilder = true)
@ToString
@RequiredArgsConstructor
@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id", nullable = false)
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "referred_by", referencedColumnName = "id")
    @ToString.Exclude
    private User referredBy;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "city_id")
    @ToString.Exclude
    private City city;

    private String name;

    // equals and hashcode implementation omitted for brevity
}

城市实体

@Getter
@Setter
@ToString
@SuperBuilder(toBuilder = true)
@RequiredArgsConstructor
@Entity
@Table(name = "cities")
public class City {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id", nullable = false)
    private Long id;

    private String name;

    // equals and hashcode implementation omitted for brevity
}

以及以下存储库:

用户存储库

@Repository
public interface UserRepository extends PagingAndSortingRepository<User, Long>, JpaSpecificationExecutor<User> {
    @Query(
            "select u from User u " +
            "left join fetch u.referredBy " +
            "left join fetch u.city " +
            "where u.id = :id"
    )
    Optional<User> loadDetailsById(@Param("id") Long id);
}

城市资料库

@Repository
public interface CityRepository extends JpaRepository<City, Long> {
}

所以问题是:当我在UserRepository 中使用loadDetailsById 方法时,我希望我能得到类似这样的结果:

User(
    id=2,
    name=John,
    city=City(id=1, name=Seattle),
    referredBy=User(id=1, name=Joe, city=HIBERNATE_PROXY, referredBy=HIBERNATE_PROXY)
)

考虑到事实User(name=John) 有引用者 User(name=Joe)。通过我现在的查询,我得到了类似的结果:

User(
    id=2,
    name=John,
    city=City(id=1, name=Seattle),
    referredBy=User(
                   id=1, 
                   name=Joe, 
                   city=City(id=2, name=New York), 
                   referredBy=User(id=3, ... and so on and so forth)
    )
)

所以问题是:JPQL join fetch 是否有可能急切地获取实体及其所有子代,并保持实体子代的子关联延迟加载(事实上,实体的一个子代是同一个实体)?

或者我应该改用EntityGraph 吗?

测试作为参考

@DataJpaTest
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class JoinFetchMweApplicationTests {
    private @Autowired UserRepository userRepository;

    private @Autowired CityRepository cityRepository;

    @BeforeAll
    void setup() {
        City city = City.builder().id(1L).name("Seattle").build();
        City city2 = City.builder().id(2L).name("New York").build();
        cityRepository.saveAll(Arrays.asList(city, city2));

        User referrer = User.builder()
                .id(1L)
                .name("Joe")
                .city(city2)
                .build();

        User referral = User.builder()
                .id(2L)
                .name("John")
                .city(city)
                .referredBy(referrer)
                .build();

        userRepository.saveAll(Arrays.asList(referrer, referral));
    }

    // Passed
    @Test
    void whenFindById_itReturnsUser() {
        Optional<User> userOptional = userRepository.findById(2L);

        assertThat(userOptional).isPresent();
        User user = userOptional.get();
        assertThat(user.getName()).isEqualTo("John");

        System.out.println(user);

        assertThat(user.getCity()).isInstanceOf(HibernateProxy.class);
        assertThat(user.getReferredBy()).isInstanceOf(HibernateProxy.class);
    }

    // Failed
    @Test
    void whenLoadDetailsById_itLoadsEagerly() {
        Optional<User> userOptional = userRepository.loadDetailsById(2L);

        assertThat(userOptional).isPresent();
        User user = userOptional.get();
        assertThat(user.getName()).isEqualTo("John");

        System.out.println(user);

        assertThat(user.getCity()).isNotInstanceOf(HibernateProxy.class);
        assertThat(user.getReferredBy()).isNotInstanceOf(HibernateProxy.class);
        
        assertThat(user.getReferredBy().getCity()).isInstanceOf(HibernateProxy.class); // Fails here: getCity is NOT instance of HibernateProxy
        assertThat(user.getReferredBy().getReferredBy()).isInstanceOf(HibernateProxy.class); // Fails here: getReferredBy is NOT instance of HibernateProxy

    }
}

以及为loadDetailsById 生成的 SQL

select 
    user0_.id as id1_1_0_, 
    user1_.id as id1_1_1_, 
    city2_.id as id1_0_2_, 
    user0_.city_id as city_id3_1_0_,
    user0_.name as name2_1_0_, 
    user0_.referred_by as referred4_1_0_, 
    user1_.city_id as city_id3_1_1_, 
    user1_.name as name2_1_1_, 
    user1_.referred_by as referred4_1_1_, 
    city2_.name as name2_0_2_ 
from users user0_ 
left outer join 
    users user1_ on user0_.referred_by=user1_.id 
left outer join 
    cities city2_ on user0_.city_id=city2_.id 
where user0_.id=?

【问题讨论】:

标签: java hibernate jpa spring-data-jpa jpql


【解决方案1】:

基本上:

User referrer = User.builder()
        .id(1L)
        .name("Joe")
        // turns out that Joe referred by John
        .city(city2)
        .build();

例如

User(name=John).referredBy -> User(name=Joe).referredBy -> User(name=John) because he was already LOADED in the beginning

这意味着什么?这意味着当我们使用join fetch我们急切地加载两个用户(意味着用户不再是 Hibernate 代理)并且只要 User John 被 User Joe 引用,User John 被引用,我们得到的结果是所有用户都已加载并且 JPQL 中没有错误(同样的结果也会出现在 EntityGraph 中)。

【讨论】:

    猜你喜欢
    • 2013-09-03
    • 2015-10-02
    • 2019-10-01
    • 2020-01-20
    • 1970-01-01
    • 1970-01-01
    • 2010-10-07
    • 2013-06-30
    • 1970-01-01
    相关资源
    最近更新 更多