Quarkus中EntityManager调用SP返回重复数据的调试与排查
问题分析与解决方案
一、数据重复的核心原因
你遇到的重复问题本质是JPA实体缓存机制的限制:
- 你的
PostpaidAccount类标注了@Entity,并将V_NUMERO_CUENTA设为@Id主键。JPA的设计原则是主键唯一标识一个实体,当存储过程返回多条主键相同但其他字段不同的记录时,EntityManager会认为这些是同一个实体对象。第一次加载后,后续同主键的记录会直接复用缓存中的对象,不会更新其他字段,最终列表里出现重复的对象。 - 这种场景下,你的返回结果并不符合JPA实体的定义(同主键多记录),因此用
@Entity类映射存储过程结果是不合适的。
二、修复方案
方案1:改用DTO类替代JPA实体(推荐)
创建一个普通的DTO类,移除所有JPA注解,让存储过程结果直接映射到这个DTO上,避开JPA的缓存干扰:
package com.tmve.account.beans; import lombok.Getter; import lombok.ToString; import java.sql.Date; import java.io.Serializable; @Getter @ToString public class PostpaidAccountDTO implements Serializable { private static final long serialVersionUID = 1L; public static final String NAME_QUERY_BUSCAR_CUENTAS_ROLES_X_DOC_IDE = "BuscarCuentasRolesxDocId"; Long account; Long billingAccountNumber; Long billingAccountNumberValidators; String accountType; String accountStatus; String accountStatusDescription; Integer productoId; String productName; Date customerSince; String accountHolder; String relationshipType; int platformId; String identifier; String marketName; }
修改存储过程注解的resultClasses为这个DTO:
@NamedStoredProcedureQueries({ @NamedStoredProcedureQuery( name = PostpaidAccountDTO.NAME_QUERY_BUSCAR_CUENTAS_ROLES_X_DOC_IDE, procedureName = "PERS.PKG_COMP_CUENTAS.BUSCAR_CUENTAS_ROLES_X_DOC_ID", resultClasses = {PostpaidAccountDTO.class}, parameters = { @StoredProcedureParameter(mode = ParameterMode.IN, type = String.class), @StoredProcedureParameter(mode = ParameterMode.IN, type = Integer.class), @StoredProcedureParameter(mode = ParameterMode.REF_CURSOR, type = void.class), }) })
最后更新仓库类的返回类型为List<PostpaidAccountDTO>即可。
方案2:手动映射原始结果集(临时方案)
如果必须使用原实体类,可以绕过JPA的自动映射,手动解析结果集:
@Override public List<PostpaidAccount> getPostpaidAccount(String documentType, String documentNumber) { StoredProcedureQuery query = entityManager.createNamedStoredProcedureQuery("BuscarCuentasRolesxDocId"); query.setParameter(1, documentNumber); query.setParameter(2, Integer.parseInt(documentType)); query.execute(); List<Object[]> rawResults = query.getResultList(); List<PostpaidAccount> result = new ArrayList<>(); for (Object[] row : rawResults) { PostpaidAccount account = new PostpaidAccount(); // 按存储过程返回字段顺序手动映射 account.account = (Long) row[0]; account.billingAccountNumber = (Long) row[1]; account.billingAccountNumberValidators = (Long) row[2]; // ...映射其他字段 result.add(account); } return result; }
三、IntelliJ IDEA调试数据库返回结果的方法
1. 开启SQL日志查看原始执行数据
在Quarkus的application.properties中添加日志配置,打印JPA执行的SQL和结果详情:
# 打印执行的SQL语句 quarkus.log.category."org.hibernate.SQL".level=DEBUG # 打印SQL参数和结果值 quarkus.log.category."org.hibernate.type.descriptor.sql".level=TRACE
通过控制台日志可以直接看到存储过程的调用参数,以及Hibernate解析的结果数据。
2. 用IntelliJ数据库工具直接执行存储过程
- 打开IntelliJ的Database工具窗口,连接目标数据库。
- 找到存储过程
PERS.PKG_COMP_CUENTAS.BUSCAR_CUENTAS_ROLES_X_DOC_ID,右键选择Run,输入测试参数后执行,对比返回结果和代码获取的结果是否一致。
3. 调试时查看原始结果集
在代码中storedProcedureQuery.execute();行设置断点,调试时:
- 查看
storedProcedureQuery对象的内部结构,找到resultSet相关属性,直接查看数据库返回的原始数据。 - 临时将结果转为
List<Object[]>形式,查看每个元素的字段值,确认是否与数据库返回一致:
// 调试临时代码,用完可删除 List<Object[]> rawData = storedProcedureQuery.getResultList(); // 在调试窗口查看rawData的内容
4. 用原生JDBC调用对比结果
绕过JPA,用原生JDBC调用存储过程,验证返回结果是否正确,排除JPA的影响:
@Inject DataSource dataSource; public List<Map<String, Object>> callSPDirectly(String documentNumber, int documentType) throws SQLException { List<Map<String, Object>> results = new ArrayList<>(); try (Connection conn = dataSource.getConnection()) { String sql = "{call PERS.PKG_COMP_CUENTAS.BUSCAR_CUENTAS_ROLES_X_DOC_ID(?, ?, ?)}"; try (CallableStatement stmt = conn.prepareCall(sql)) { stmt.setString(1, documentNumber); stmt.setInt(2, documentType); stmt.registerOutParameter(3, OracleTypes.CURSOR); // 根据数据库类型调整 stmt.execute(); try (ResultSet rs = (ResultSet) stmt.getObject(3)) { ResultSetMetaData metaData = rs.getMetaData(); while (rs.next()) { Map<String, Object> row = new HashMap<>(); for (int i = 1; i <= metaData.getColumnCount(); i++) { row.put(metaData.getColumnName(i), rs.getObject(i)); } results.add(row); } } } } return results; }
内容的提问来源于stack exchange,提问作者Cesar Justo




