嵌套关联查询触发EclipseLink-QueryException问题排查咨询
我之前也碰到过一模一样的问题,这个异常的本质是EclipseLink在处理左连接产生的null实体记录时,违反了JPA实体必须拥有非null主键的约束。
为什么会抛出这个异常?
你的查询以Person为根做了多层左连接,当出现以下情况时:
- 某个
Person没有关联的Address - 某个
Person没有关联的Job - 某个
Job没有关联的Address
结果集中会出现对应实体字段为null的行。但EclipseLink在尝试把这些行映射回实体对象时,会尝试创建一个对应实体的实例(比如Address或Job),但这些实例的主键是null——而JPA规范明确要求实体的主键不能为null,所以就触发了nullPrimaryKeyInBuildingObject异常。虽然你直接执行SQL能正常运行,但JPA的实体映射逻辑不允许这种情况。
修复方案
这里提供几种可行的单查询解决方案:
方案1:改用投影查询,返回具体字段而非完整实体
放弃返回完整的实体对象,而是选择需要的具体字段,这样即使某些字段为null,也不会触发实体构建的主键检查。之后你可以手动把这些字段映射到目标Map结构中:
CriteriaBuilder builder = em.getCriteriaBuilder(); CriteriaQuery<Object[]> query = builder.createQuery(Object[].class); Root<Person> personRoot = query.from(Person.class); Join<Person, Address> joinPersonAddress = personRoot.join(Person_.address, JoinType.LEFT); Join<Person, Job> joinPersonJob = personRoot.join(Person_.jobs, JoinType.LEFT); Join<Job, Address> joinJobAddress = joinPersonJob.join(Job_.addresses, JoinType.LEFT); // 选择需要的具体字段(根据你的实体实际字段调整) query.multiselect( personRoot.get(Person_.id), personRoot.get(Person_.name), joinPersonAddress.get(Address_.id), joinPersonAddress.get(Address_.city), joinPersonAddress.get(Address_.street), joinPersonJob.get(Job_.id), joinPersonJob.get(Job_.jobName), joinJobAddress.get(Address_.id), joinJobAddress.get(Address_.city), joinJobAddress.get(Address_.street) ); Query q = em.createQuery(query); List<Object[]> result = q.getResultList(); // 手动映射到Map<Address, Pair<Set<Person>, Set<Job>>> // 注意处理null字段,比如当Address ID为null时跳过,避免构建主键为null的实体
方案2:使用DTO构造函数接收结果
创建一个DTO类来封装查询结果,这样当左连接没有匹配记录时,对应的参数会直接传入null,而不会尝试构建主键为null的实体:
首先定义DTO:
public class AddressAssociationDTO { private Person person; private Address personAddress; private Job job; private Address jobAddress; public AddressAssociationDTO(Person person, Address personAddress, Job job, Address jobAddress) { this.person = person; this.personAddress = personAddress; this.job = job; this.jobAddress = jobAddress; } // Getters public Person getPerson() { return person; } public Address getPersonAddress() { return personAddress; } public Job getJob() { return job; } public Address getJobAddress() { return jobAddress; } }
然后修改Criteria查询:
CriteriaBuilder builder = em.getCriteriaBuilder(); CriteriaQuery<AddressAssociationDTO> query = builder.createQuery(AddressAssociationDTO.class); Root<Person> personRoot = query.from(Person.class); Join<Person, Address> joinPersonAddress = personRoot.join(Person_.address, JoinType.LEFT); Join<Person, Job> joinPersonJob = personRoot.join(Person_.jobs, JoinType.LEFT); Join<Job, Address> joinJobAddress = joinPersonJob.join(Job_.addresses, JoinType.LEFT); // 使用构造函数表达式创建DTO query.select(builder.construct(AddressAssociationDTO.class, personRoot, joinPersonAddress, joinPersonJob, joinJobAddress)); Query q = em.createQuery(query); List<AddressAssociationDTO> result = q.getResultList(); // 之后遍历result,将数据整理到目标Map结构中
这个方案的核心是:当左连接没有匹配时,对应的实体参数会是null,而不是尝试创建一个主键为null的实体实例,从而避开异常。
方案3:调整查询根为Address(更贴合你的业务目标)
因为你的最终目标是获取所有Address及其关联的Person和Job,直接以Address为查询根会更合理,这样结果集中的Address都是非null的,关联的Person或Job为null时也只会传入null引用,不会触发主键异常。不过由于你的关联都是单向的,需要用条件来关联:
CriteriaBuilder builder = em.getCriteriaBuilder(); CriteriaQuery<Object[]> query = builder.createQuery(Object[].class); Root<Address> addressRoot = query.from(Address.class); // 关联Person:通过Person的address字段匹配当前Address Root<Person> personRoot = query.from(Person.class); Predicate personMatch = builder.equal(personRoot.get(Person_.address), addressRoot); // 关联Job:通过Job的addresses集合包含当前Address Root<Job> jobRoot = query.from(Job.class); Predicate jobMatch = builder.isMember(addressRoot, jobRoot.get(Job_.addresses)); // 查询所有Address,以及关联的Person和Job(左连接逻辑) query.multiselect(addressRoot, personRoot, jobRoot) .where(builder.or(personMatch, jobMatch, builder.isNull(personRoot), builder.isNull(jobRoot))) .distinct(true); // 去重避免笛卡尔积 Query q = em.createQuery(query); List<Object[]> result = q.getResultList();
这个方案的好处是直接围绕你的业务目标构建查询,结果集中的Address始终有效,不会出现主键null的情况。
总结
最直接的修复方式是避免让JPA尝试构建主键为null的实体——要么返回字段/DTO,要么调整查询根确保核心实体(这里是Address)始终非null。你之前用两个查询的方案虽然可行,但单查询方案通过以上调整完全可以实现。
内容的提问来源于stack exchange,提问作者Filou

