为啥在本机查询 Hibernate 延迟加载的子实体中?

Posted

技术标签:

【中文标题】为啥在本机查询 Hibernate 延迟加载的子实体中?【英文标题】:Why in native query Hibernate lazy load's children entities?为什么在本机查询 Hibernate 延迟加载的子实体中? 【发布时间】:2020-06-02 07:55:03 【问题描述】:

我不明白,当我使用 JPQL 和 JOIN fetch 时,休眠应该执行一个查询来连接子实体,但是当我想使用本机查询并通过一个查询连接所有子实体时,休眠仍然会在其他查询中延迟加载子实体. 我正在使用 Spring Data 2。

我应该怎么做才能避免使用本机查询进行延迟加载n+1个查询?

例子:

@Query(value = "SELECT recipe.*, r_ing.*, ing.* FROM recipe recipe join " +
        " on recipe.id = r.recipe_id " +
        " LEFT JOIN recipe_ingredients r_ing on r.recipe_id = r_ing.recipe_id " +
        " LEFT JOIN ingredient ing on r_ing.ingredient_id = ing.id where ing.names in (:ingredientsNames)",
        countQuery = "SELECT count(*) FROM recipe recipe join " +
                " on recipe.id = r.recipe_id " +
                " LEFT JOIN recipe_ingredients r_ing on r.recipe_id = r_ing.recipe_id " +
                " LEFT JOIN ingredient ing on r_ing.ingredient_id = ing.id where ing.names in (:ingredientsNames)",
        nativeQuery = true
)
Page<Recipe> findAllByIngredientsNames(List<String> ingredientsNames, Pageable page);

实体:

@Entity
public class Recipe 
    @OneToMany(mappedBy = "recipe", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<RecipeIngredients> ingredients;

@Entity
public class RecipeIngredients implements Serializable 

    @EmbeddedId
    private RecipeIngredientsId recipeIngredientsId;

    @ManyToOne(fetch = FetchType.LAZY)
    @MapsId("recipeId")
    private Recipe recipe;

    @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST, CascadeType.MERGE)
    @MapsId("ingredientId")
        private Ingredient ingredient;


@Entity
public class Ingredient 

    @NaturalId
    @Column(unique = true)
    private String name;

【问题讨论】:

请添加您的控制器/服务代码,如果您使用任何 dto 进行响应,请也添加它。此外,请尝试使用最少的代码来重现您的问题。 【参考方案1】:

对于原生查询,Hibernate 不知道如何映射高级数据。在您的情况下,您有一个获取Recipe 实体的请求,其中实体映射器知道如何从SELECT * FROM recipe 中提取结果。但是ingredients属性是反向映射,它是作为惰性初始化集合实现的,后面有查询。这就是 JPA 和 Spring 数据为您所做的,但它们还不够聪明,无法自动理解并进一步映射以急切地将查询结果映射到集合属性。

另外,我猜您在查询结果中看到了多个相同的Recipe 实体。

如果出于某种原因您真的想处理原生查询,那么请正确使用它们:原生查询的结果通常不是 JPA 管理的实体,而是投影。

因此,为您在本机查询中的行创建一个特定的投影:

public class FullRecipeProjection 
    private final Integer recipeId; 
    private final Integer recipeIngredientsId;
    private final Integer ingredientId
    private final Integer ingredientName 

    /* Full-arg-constructor */
    public FullRecipeProjection (Integer recipeId, Integer recipeIngredientsId, Integer ingredientId, String ingredientName) ...


然后您可以创建查询:

@Query(value = "SELECT new FullRecipeProjection(recipe.recipeId, r_ing.recipeIngredientsId, ing.ingredientId, ing.IngredientName) FROM recipe recipe join " +
        " on recipe.id = r.recipe_id " +
        " LEFT JOIN recipe_ingredients r_ing on r.recipe_id = r_ing.recipe_id " +
        " LEFT JOIN ingredient ing on r_ing.ingredient_id = ing.id where ing.names in (:ingredientsNames)",
        countQuery = "SELECT count(*) FROM recipe recipe join " +
                " on recipe.id = r.recipe_id " +
                " LEFT JOIN recipe_ingredients r_ing on r.recipe_id = r_ing.recipe_id " +
                " LEFT JOIN ingredient ing on r_ing.ingredient_id = ing.id where ing.names in (:ingredientsNames)",
        nativeQuery = true
)
List<FullRecipeProjection> findAllByIngredientsNames(List<String> ingredientsNames);

然后您可以将FullRecipeProjection 的集合转换为您的Recipe 的类似对象:

public class FullRecipe 
    private final Integer recipeId;
    private final Set<IngredientProjection> ingredients;
    public FullRecipe(Integer recipeId, Set<IngredientProjection> ingredients) ...


public class IngredientProjection 
    private final Integer ingredientId;
    private final String ingredientName;
    public IngredientProjection(Integer ingredientId, String ingredientName) ...

然后你可以像这样得到你想要的:

final List<FullRecipeProjection> data = repository.findAllByIngredientsNames(ingredientsNames);

final List<FullRecipe> results = data
    .stream()
    // extracting distinct identities of recipes, you have fetched  
    .map(FullRecipeProjection::recipeId)
    .distinct()
    // now we have unique key for the data and can map it
    .map(it -> 
         new FullRecipe(
             it, 
             // extracting all ingredients, which  were fetched in rows with references to recipe.
             data
                 .stream()
                 .filter(o -> o.recipeId.equals(it))
                 .map(ing -> new IngredientProjection(ing.ingredientId, ing.ingredientName))
                 .collect(Collectors.toSet())
    .collect(Collectors.toList()) ; 

很长的路要走。但这就是它的工作原理。当你使用 JPQL 查询时,这个漫长的处理是由 Hibernate 完成的。

并且注意:分页对于这种数据提取变得很麻烦:以您指定的方式,您将分页不是最终结果,而是FullRecipeProjection,这可能导致不完整的Recipe fetch,并且肯定是在错误分页的数据中(它可能只包含 1 个FullRecipe,它可能没有完全加载!)。

【讨论】:

以上是关于为啥在本机查询 Hibernate 延迟加载的子实体中?的主要内容,如果未能解决你的问题,请参考以下文章

hibernate的延迟加载

Hibernate-延迟加载和立即加载

Hibernate检索策略之延迟加载和立即加载

Hibernate延迟加载

Hibernate延迟加载

Hibernate延迟加载