You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多
文档控制台
免费开始使用

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

火山引擎 最新活动