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; // ... }
问题根源
- 投影逻辑错误:
query.select(mediaRoot)会直接选择整个MediaModel实体,Hibernate默认会加载实体的所有基本字段(即使关联是LAZY,基本字段都是即时加载的)。而代码中mediaRoot.get(field)只是获取字段路径,并没有对查询的投影做任何限制。 - 关联字段强制加载:实体类中的
@NotNull注解会让Hibernate强制加载关联字段(比如mediaCategory),即使GraphQL没请求该字段。 - 懒加载触发陷阱:调试或打印实体时,若不小心调用了未请求字段的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
相关产品推荐
相关产品推荐

