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

Criteria API实现GraphQL按需字段查询失败,全字段被加载

问题:Criteria API配合GraphQL指定字段查询时,实体所有字段仍被加载

我尝试通过Criteria API只获取GraphQL查询请求的指定字段,但调试或打印MediaModel实例时,发现类的所有字段都被填充了——哪怕只请求id、name、about这几个字段,切换到EAGER加载策略结果也一样。

相关代码实现

工具类代码

public abstract class Utils {
    public static Map<String, List<String>> getRequestedFields(DataFetchingEnvironment environment) {
        return environment.getSelectionSet().getFields().stream()
                .collect(Collectors.toMap(
                        SelectedField::getName,
                        field -> field.getSelectionSet().getFields().stream()
                                .map(SelectedField::getName)
                                .collect(Collectors.toList())));
    }
}

该工具类可将GraphQL请求的字段及子字段转换为Map,示例:
查询语句:

query Media {
    tvserie(name: "Stranger Things") {
       id
       title
       genres {
         name
       }
    }
}

转换后的结构:

{
  "media": [
    "id",
    "title",
    "genres"
  ],
  "genres": [
    "name"
  ]
}

自定义Repository代码

@Repository
public class MediaRepositoryCustomImpl implements MediaRepositoryCustom {
    @PersistenceContext
    private EntityManager entityManager;

    @Override
    public List<MediaModel> findMediaByNameOrAlternativeTitleAndMediaCategory(
            String name,
            String alternativeTitle,
            Integer category,
            Map<String, List<String>> requestedFields) {
        CriteriaBuilder cb = entityManager.getCriteriaBuilder();
        CriteriaQuery<MediaModel> query = cb.createQuery(MediaModel.class);
        Root<MediaModel> mediaRoot = query.from(MediaModel.class);
        Join<MediaModel, AlternativeTitleModel> altTitlesJoin = mediaRoot.join("alternativeTitles", JoinType.LEFT);

        query.select(mediaRoot);

        requestedFields.forEach((field, subFields) -> {
            if (Utils.isJoinableField(mediaRoot, field)) {
                Join<Object, Object> join = mediaRoot.join(field, JoinType.LEFT);
                subFields.forEach(subField -> join.fetch(subField, JoinType.LEFT));
            } else {
                mediaRoot.get(field);
            }
        });

        List<Predicate> predicates = new ArrayList<>();
        if (category != null) {
            predicates.add(cb.equal(mediaRoot.get("mediaCategory").get("id"), category));
        }

        List<Predicate> namePredicates = new ArrayList<>();
        if (name != null && !name.isEmpty()) {
            predicates.add(cb.like(mediaRoot.get("name"), "%" + name + "%"));
        }
        if (alternativeTitle != null) {
            predicates.add(cb.like(altTitlesJoin.get("name"), "%" + alternativeTitle + "%"));
        }

        if (!namePredicates.isEmpty()) {
            Predicate orPredicate = cb.or(namePredicates.toArray(new Predicate[0]));
            predicates.add(orPredicate);
        }

        if (!predicates.isEmpty()) {
            query.where(cb.and(predicates.toArray(new Predicate[0])));
        }

        return entityManager.createQuery(query).getResultList();
    }
}

MediaModel实体类代码

@Entity
@Table(name = "medias", indexes = {@Index(name = "idx_name_media", columnList = "name_media", unique = true)})
@Getter
@Setter
public class MediaModel implements Serializable {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id_media")
    private Integer id;

    @Column(name = "name_media")
    private String name;

    @Column(name = "episode_count")
    private Integer totalEpisodes;

    @Column(name = "episode_length_in_minutes")
    private Integer episodeLength;

    @Column(name = "about", length = 10000)
    private String about;

    @Column(name = "url_cover")
    private String cover;

    @Column(name = "url_banner")
    private String banner;

    @JoinColumn(name = "id_category", nullable = false)
    @ManyToOne
    @NotNull
    private MediaCategoryModel mediaCategory;

    @OneToMany(mappedBy = "media", fetch = FetchType.LAZY, orphanRemoval = true)
    @NotNull
    private List<ExternalReferenceModel> externalReference;

    @ManyToMany(fetch = FetchType.LAZY)
    @JoinTable(
        name = "medias_has_companies",
        joinColumns = @JoinColumn(name = "medias_id_media"),
        inverseJoinColumns = @JoinColumn(name = "companies_id_company"))
    private List<CompanyModel> company;

    // ...
}

问题根源

  1. 投影逻辑错误:query.select(mediaRoot)会直接选择整个MediaModel实体,Hibernate默认会加载实体的所有基本字段(即使关联是LAZY,基本字段都是即时加载的)。而代码中mediaRoot.get(field)只是获取字段路径,并没有对查询的投影做任何限制。
  2. 关联字段强制加载:实体类中的@NotNull注解会让Hibernate强制加载关联字段(比如mediaCategory),即使GraphQL没请求该字段。
  3. 懒加载触发陷阱:调试或打印实体时,若不小心调用了未请求字段的getter方法,Hibernate会自动触发LAZY加载,导致字段被填充。

解决方案

要实现只加载指定字段,需要用投影查询明确选择请求的字段,而非整个实体,同时正确处理关联字段和懒加载逻辑。

步骤1:修改查询逻辑,使用Tuple投影接收结果

Tuple是JPA提供的动态结果容器,可以灵活接收任意字段组合,避免依赖实体构造器的限制:

@Repository
public class MediaRepositoryCustomImpl implements MediaRepositoryCustom {
    @PersistenceContext
    private EntityManager entityManager;

    @Override
    public List<MediaModel> findMediaByNameOrAlternativeTitleAndMediaCategory(
            String name,
            String alternativeTitle,
            Integer category,
            Map<String, List<String>> requestedFields) {
        CriteriaBuilder cb = entityManager.getCriteriaBuilder();
        // 改用TupleQuery接收动态字段
        CriteriaQuery<Tuple> tupleQuery = cb.createTupleQuery();
        Root<MediaModel> mediaRoot = tupleQuery.from(MediaModel.class);
        Join<MediaModel, AlternativeTitleModel> altTitlesJoin = mediaRoot.join("alternativeTitles", JoinType.LEFT);

        // 收集需要查询的字段(含关联子字段)
        List<Selection<?>> selections = new ArrayList<>();
        requestedFields.forEach((field, subFields) -> {
            if (Utils.isJoinableField(mediaRoot, field)) {
                Join<Object, Object> join = mediaRoot.join(field, JoinType.LEFT);
                subFields.forEach(subField -> {
                    // 给关联字段起别名,方便后续映射
                    selections.add(join.get(subField).alias(field + "_" + subField));
                });
            } else {
                selections.add(mediaRoot.get(field).alias(field));
            }
        });
        tupleQuery.select(cb.tuple(selections.toArray(new Selection[0])));

        // 添加查询条件
        List<Predicate> predicates = new ArrayList<>();
        if (category != null) {
            predicates.add(cb.equal(mediaRoot.get("mediaCategory").get("id"), category));
        }

        List<Predicate> namePredicates = new ArrayList<>();
        if (name != null && !name.isEmpty()) {
            namePredicates.add(cb.like(mediaRoot.get("name"), "%" + name + "%"));
        }
        if (alternativeTitle != null) {
            namePredicates.add(cb.like(altTitlesJoin.get("name"), "%" + alternativeTitle + "%"));
        }

        if (!namePredicates.isEmpty()) {
            predicates.add(cb.or(namePredicates.toArray(new Predicate[0])));
        }

        if (!predicates.isEmpty()) {
            tupleQuery.where(cb.and(predicates.toArray(new Predicate[0])));
        }

        // 执行查询并将Tuple映射为MediaModel
        List<Tuple> tuples = entityManager.createQuery(tupleQuery).getResultList();
        return tuples.stream().map(tuple -> {
            MediaModel media = new MediaModel();
            requestedFields.forEach((field, subFields) -> {
                if (Utils.isJoinableField(mediaRoot, field)) {
                    // 处理关联字段,例如mediaCategory.name
                    String alias = field + "_" + subFields.get(0);
                    Object value = tuple.get(alias);
                    if ("mediaCategory".equals(field)) {
                        MediaCategoryModel categoryModel = new MediaCategoryModel();
                        categoryModel.setName((String) value);
                        media.setMediaCategory(categoryModel);
                    }
                    // 其他关联字段按此逻辑处理
                } else {
                    // 处理基本字段
                    Object value = tuple.get(field);
                    switch (field) {
                        case "id":
                            media.setId((Integer) value);
                            break;
                        case "name":
                            media.setName((String) value);
                            break;
                        case "about":
                            media.setAbout((String) value);
                            break;
                        // 其他基本字段依次映射
                    }
                }
            });
            return media;
        }).collect(Collectors.toList());
    }
}

步骤2:避免懒加载被触发

  • 调试时不要调用未请求字段的getter方法,否则Hibernate会自动去数据库加载该字段。
  • 若不需要强制加载关联字段,可移除实体类中关联字段的@NotNull注解,或调整为允许null。

关键注意点

  • 必须明确指定查询字段:不能用select(mediaRoot)选择整个实体,要通过cb.tuple()或构造器投影选择具体字段。
  • 关联字段需单独处理:要手动join关联表并选择子字段,而非依赖Hibernate的自动加载。
  • 调试时注意访问范围:只查看GraphQL请求过的字段,避免触发懒加载。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 15:02:31