You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在多表关联时使用Java CriteriaBuilder的notLike方法

如何用CriteriaBuilder实现查找不包含指定食材的食谱

实体类定义

Recipe类

@jakarta.persistence.Entity 
public class Recipe implements Serializable {

private static final long serialVersionUID = 1L;

@jakarta.persistence.Id @jakarta.persistence.GeneratedValue
private Integer id;

private String name;

private String instructions;

private Boolean isVegetarian;

private String numberOfServings;

@jakarta.persistence.OneToMany(cascade = CascadeType.ALL)
@Valid
private List<@Valid Ingredient> includedIngredients = new ArrayList<>();

@jakarta.persistence.OneToMany(cascade = CascadeType.ALL)
@Valid
private List<@Valid Ingredient> excludedIngredients;

public Recipe() {
  super();
}

Ingredient类

@jakarta.persistence.Entity
public class Ingredient implements Serializable {

private static final long serialVersionUID = 1L;

@jakarta.persistence.Id @jakarta.persistence.GeneratedValue
private Integer id;

private String name;

private Boolean required;

public Ingredient() {
  super();
}

现有实现与问题

已成功用CriteriaBuilder实现包含指定食材的食谱查询,代码如下:

List<Predicate> ingredientsPredicates = new ArrayList<>();

for (Ingredient ingredient : fields.getIncludedIngredients()) {
    ingredientsPredicates.add(builder.like(builder.lower(recipe.get("includedIngredients").<String> get("name")), "%" + ingredient.getName().toLowerCase() + "%"));
}

allPredicates.add(builder.or(ingredientsPredicates.toArray(new Predicate[]{})));

但尝试实现不包含指定食材的查询时,直接使用notLike方法失败(该方法对Recipe的name字段有效,但对关联的includedIngredients列表无效),尝试的代码:

List<Predicate> ingredientsPredicates = new ArrayList<>();

for (Ingredient ingredient : fields.getExcludedIngredients()) {
    ingredientsPredicates.add(builder.notLike(builder.lower(recipe.get("includedIngredients").<String> get("name")), "%" + ingredient.getName().toLowerCase() + "%"));
}

allPredicates.add(builder.or(ingredientsPredicates.toArray(new Predicate[]{})));

解决方案

问题核心是:直接对集合字段用notLike会导致逻辑错误——只要集合中有一个元素不匹配条件,整个食谱就会被选中,而我们需要的是食谱不存在任何匹配指定名称的includedIngredients。

方法一:子查询+ID排除法

List<Predicate> excludePredicates = new ArrayList<>();

for (Ingredient excludeIngredient : fields.getExcludedIngredients()) {
    // 创建子查询:找出所有包含当前排除食材的Recipe ID
    Subquery<Integer> subquery = query.subquery(Integer.class);
    Root<Recipe> subRecipe = subquery.from(Recipe.class);
    Join<Recipe, Ingredient> subIngredient = subRecipe.join("includedIngredients");
    
    subquery.select(subRecipe.get("id"))
            .where(
                builder.like(
                    builder.lower(subIngredient.get("name")),
                    "%" + excludeIngredient.getName().toLowerCase() + "%"
                )
            );
    
    // 主查询条件:当前Recipe的ID不在子查询结果中
    excludePredicates.add(builder.not(builder.in(recipe.get("id")).value(subquery)));
}

// 所有排除条件必须同时满足(食谱不能包含任何指定的排除食材)
allPredicates.add(builder.and(excludePredicates.toArray(new Predicate[]{})));

方法二:notExists简洁实现(JPA 2.1+支持)

List<Predicate> excludePredicates = new ArrayList<>();

for (Ingredient excludeIngredient : fields.getExcludedIngredients()) {
    Subquery<Ingredient> subquery = query.subquery(Ingredient.class);
    Root<Recipe> subRecipe = subquery.from(Recipe.class);
    Join<Recipe, Ingredient> subIngredient = subRecipe.join("includedIngredients");
    
    subquery.select(subIngredient)
            .where(
                builder.equal(subRecipe.get("id"), recipe.get("id")),
                builder.like(
                    builder.lower(subIngredient.get("name")),
                    "%" + excludeIngredient.getName().toLowerCase() + "%"
                )
            );
    
    excludePredicates.add(builder.notExists(subquery));
}

allPredicates.add(builder.and(excludePredicates.toArray(new Predicate[]{})));

逻辑说明

  • 针对每个需排除的食材,先查询出所有包含该食材的食谱ID
  • 主查询筛选出ID不在上述结果中的食谱,确保食谱完全不包含该食材
  • 用and组合所有排除条件,保证食谱不包含任何指定的排除食材

内容的提问来源于stack exchange,提问作者humbleCoder

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.28 11:36:10