Spring Boot/JPA+QueryDSL设置@OneToOne关联为null后报“Unable to locate entity descriptor”异常求助
你猜的完全没错!这个问题的核心就是QueryDSL的批量更新直接操作了数据库,但没同步Hibernate的持久化上下文,导致缓存的实体状态和数据库实际状态打架,触发了找不到实体描述符的错误。我之前处理JPA批量操作时也踩过这个坑,给你几个实用的解决方案:
先说说问题根源
JPAUpdateClause是绕过Hibernate持久化上下文的批量SQL操作,执行完后数据库里的外键已经被设为null,但你从envelope里拿到的EnvelopeSignerEntity实例还在一级缓存里保留着对EnvelopeSignerCertificateEntity的引用。后续调用certificateRepository.deleteById()时,Hibernate处理缓存状态和数据库状态的冲突,就抛出了“Unable to locate entity descriptor”异常。
另外注意下你代码里的小细节:虽然用Supplier每次调用get()都会生成新的Stream,但提前把需要处理的Signer收集成List会更高效,也避免重复过滤的操作。
方案一:同步持久化上下文状态(推荐,平衡性能和规范)
先把需要处理的Signer收集成List,执行批量更新后,手动把缓存里的实体引用设为null,让持久化上下文和数据库状态保持一致:
修改核心方法:
@Transactional public void deleteCertificatesFromEnvelope(Envelope envelope) { if ((envelope.status == EnvelopeStatus.COMPLETED || envelope.status == EnvelopeStatus.CANCELED) && Objects.nonNull(envelope.envelopeSigner) && !envelope.envelopeSigner.isEmpty()) { // 提前收集所有带证书的Signer到List,避免重复处理Stream List<EnvelopeSigner> signersWithCert = envelope.envelopeSigner.stream() .filter(signer -> Objects.nonNull(signer.envelopeSignerCertificate)) .collect(Collectors.toList()); if (signersWithCert.isEmpty()) return; // 1. 解除Signer和Certificate的关联 removeCertificateFromSigner(signersWithCert); // 2. 同步缓存:手动把Signer里的证书引用设为null signersWithCert.forEach(signer -> signer.envelopeSignerCertificate = null); // 3. 删除Certificate removeCertificates(signersWithCert); } } // 接收List代替Stream,避免重复操作 private void removeCertificateFromSigner(List<EnvelopeSigner> signersWithCert) { List<UUID> signerIds = signersWithCert.stream() .map(signer -> signer.id) .collect(Collectors.toList()); signerCustomRepository.deleteAllSignerCertificate(signerIds); } // 同样接收List,用批量删除代替循环 private void removeCertificates(List<EnvelopeSigner> signersWithCert) { List<UUID> certIds = signersWithCert.stream() .map(signer -> signer.envelopeSignerCertificate.id) .collect(Collectors.toList()); if (!certIds.isEmpty()) { certificateRepository.deleteAllById(certIds); // 批量删除比循环更高效 } }
方案二:用JPA实体操作代替批量SQL(最规范,适合数据量小的场景)
如果你的Signer数量不多,完全可以用JPA的实体状态管理来解除关联,不需要QueryDSL的批量更新——因为@Transactional方法里的实体是被Hibernate管理的,直接修改状态会自动同步到数据库,还不会有上下文不一致的问题:
// 替换removeCertificateFromSigner方法 private void removeCertificateFromSigner(List<EnvelopeSigner> signersWithCert) { // 直接修改管理状态的实体,Hibernate会在事务提交时自动同步到数据库 signersWithCert.forEach(signer -> signer.envelopeSignerCertificate = null); }
这种方式完全遵循JPA的设计思想,不需要手动处理上下文同步,缺点是如果Signer数量很大,会生成多条Update语句,性能不如批量SQL。
方案三:只刷新相关实体,不clear整个上下文(适合必须用批量SQL的场景)
如果因为性能原因必须用QueryDSL的批量更新,那可以在执行完后,手动刷新涉及到的Signer实体,而不是用entityManager.clear()(clear会 detach所有实体,可能影响其他操作):
修改你的deleteAllSignerCertificate方法:
public void deleteAllSignerCertificate(List<UUID> signers) { QEnvelopeSignerEntity entity = QEnvelopeSignerEntity.envelopeSignerEntity; new JPAUpdateClause(entityManager, entity) .setNull(entity.envelopeSignerCertificate) .where(entity.id.in(signers)) .execute(); // 只刷新需要处理的Signer实体,同步持久化上下文 for (UUID signerId : signers) { EnvelopeSignerEntity signer = entityManager.find(EnvelopeSignerEntity.class, signerId); if (signer != null) { entityManager.refresh(signer); } } }
额外提醒
- 检查你的@OneToOne关联配置:
cascade = CascadeType.ALL是对的,因为你需要解除关联后单独删除Certificate,不会触发级联删除;如果配置了CascadeType.REMOVE要注意,可能会有意外的级联操作。 - 你已经验证过实体注解和扫描配置,这部分没问题,不用再折腾。
内容来源于stack exchange

