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

Hibernate自引用实体查询报错,请求排查代码问题

问题排查:Querydsl查询自关联ManyToMany时SQL语法错误

场景与代码

现有两张数据表medicaments_group和medicaments_group_join,实体类MedicamentGroup继承自GenericDictionary,通过@ManyToMany实现自关联,定义了childrens(子分组)和childrenOf(父分组)两个属性:

@Entity 
@Table(name = "medicaments_group") 
@Getter @Setter 
public class MedicamentGroup extends GenericDictionary {

    @Id
    private Long id;

    private boolean groupMain;

    @ManyToMany
    @JoinTable(
            name = "medicaments_group_join",
            joinColumns = @JoinColumn(name = "medicament_group_id"),
            inverseJoinColumns = @JoinColumn(name = "medicament_join_id")
    )
    private List<MedicamentGroup> childrens = new ArrayList<>();

    @ManyToMany
    @JoinTable(
            name = "medicaments_group_join",
            joinColumns = @JoinColumn(name = "medicament_join_id"),
            inverseJoinColumns = @JoinColumn(name = "medicament_group_id")
    )
    private List<MedicamentGroup> childrenOf; 
}

为获取包含子实体列表的MedicamentGroup列表,编写了以下Querydsl查询方法:

@Override
public List<MedicamentGroup> getGroupsAndItsChildren() {
    List<Tuple> query = Objects.requireNonNull(getQuerydsl())
            .createQuery()
            .from(medicamentGroup)
            .select(id, shortName, childrens)
            .where(groupMain.isFalse())
            .fetch();

    return query.stream().map(tuple -> {
        MedicamentGroup group = new MedicamentGroup();
        group.setId(tuple.get(id));
        group.setShortName(tuple.get(shortName));
        group.setChildrens(tuple.get(childrens));
        return group;
    }).toList();
}

调用该方法时触发SQL语法错误,报错信息如下:

Hibernate: select medicament0_.id as col_0_0_, medicament0_.short_name as col_1_0_, . as col_2_0_, medicament2_.id as id1_20_, medicament2_.active as active2_20_, medicament2_.name as name3_20_, medicament2_.short_name as short_na4_20_, medicament2_.group_main as group_ma5_20_ from medicaments_group medicament0_ inner join medicaments_group_join childrens1_ on medicament0_.id=childrens1_.medicament_group_id inner join medicaments_group medicament2_ on childrens1_.medicament_join_id=medicament2_.id where medicament0_.group_main=?
2023-04-03 20:12:23.563 WARN 46674 --- [nio-8089-exec-3] o.h.engine.jdbc.spi.SqlExceptionHelper : SQL Error: 0, SQLState: 42601
2023-04-03 20:12:23.563 ERROR 46674 --- [nio-8089-exec-3] o.h.engine.jdbc.spi.SqlExceptionHelper : ERROR: syntax error at or near "." Position: 74
2023-04-03 20:12:23.579 ERROR 46674 --- [nio-8089-exec-3] p.k.c.w.f.ErrorResponseFactoryImpl : Error response
org.springframework.dao.InvalidDataAccessResourceUsageException: could not extract ResultSet; SQL [n/a]; nested exception is org.hibernate.exception.SQLGrammarException: could not extract ResultSet ...

错误原因

问题出在Querydsl的select(id, shortName, childrens)语句中:childrens是@ManyToMany关联的集合属性,Querydsl无法直接将集合作为单独的SELECT列进行查询,导致生成的SQL出现了无效的.占位符(对应报错中的as col_2_0_, .),最终触发语法错误。

解决方法

方法1:使用Fetch Join直接加载关联实体

通过Fetch Join一次性加载主实体和关联的子实体,避免手动映射Tuple,同时解决N+1查询问题:

@Override
public List<MedicamentGroup> getGroupsAndItsChildren() {
    return Objects.requireNonNull(getQuerydsl())
            .createQuery()
            .from(medicamentGroup)
            .leftJoin(medicamentGroup.childrens, childrens)
            .fetchJoin() // 启用Fetch Join加载子实体
            .where(medicamentGroup.groupMain.isFalse())
            .distinct() // 避免主实体重复(关联查询会产生笛卡尔积)
            .fetch();
}

方法2:使用DTO投影(自定义返回字段场景)

如果不需要完整的MedicamentGroup实体,可以定义DTO类,通过构造函数投影包含子实体信息:

先定义DTO:

public record GroupWithChildrenDTO(Long id, String shortName, List<MedicamentGroup> children) {}

再修改查询:

@Override
public List<GroupWithChildrenDTO> getGroupsAndItsChildren() {
    return Objects.requireNonNull(getQuerydsl())
            .createQuery()
            .select(Projections.constructor(GroupWithChildrenDTO.class,
                    medicamentGroup.id,
                    medicamentGroup.shortName,
                    JPAExpressions.selectFrom(childrens)
                            .where(childrens.childrenOf.any().id.eq(medicamentGroup.id))))
            .from(medicamentGroup)
            .where(medicamentGroup.groupMain.isFalse())
            .fetch();
}

方法3:调整原查询逻辑(不推荐,存在N+1性能问题)

如果坚持使用Tuple映射,需单独查询子实体,但这种方式会触发N+1查询,性能较差:

@Override
public List<MedicamentGroup> getGroupsAndItsChildren() {
    // 先查询主分组
    List<MedicamentGroup> mainGroups = Objects.requireNonNull(getQuerydsl())
            .createQuery()
            .from(medicamentGroup)
            .where(medicamentGroup.groupMain.isFalse())
            .fetch();
    
    // 为每个主分组查询子实体
    mainGroups.forEach(group -> {
        List<MedicamentGroup> children = Objects.requireNonNull(getQuerydsl())
                .createQuery()
                .from(childrens)
                .where(childrens.childrenOf.any().id.eq(group.getId()))
                .fetch();
        group.setChildrens(children);
    });
    
    return mainGroups;
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.25 23:35:14