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

如何通过Hibernate获取插入与删除的行数统计

解决大型实体层级结构删除的测试验证问题

需求场景

我正在实现删除多表中大型实体层级结构的方案,需要搭建测试套件完成以下步骤:

  • 用应用方法将大型实体树加载至数据库
  • 刷新实体并记录插入行数
  • 清空EntityManager
  • 仅加载实体根节点
  • 通过应用逻辑删除实体
  • 断言删除行数与插入行数相等

测试无需线程安全(用例按序执行),且无法使用内存数据库做集成测试。需要一种能统计事务开始以来所有插入、删除行数的机制,支持编程式操作、JPQL、Criteria API等场景。

当前方案的局限性

目前采用StatementInspector实现统计,但无法适配批量操作、@ElementCollection及JPQL场景,核心问题:

  • 仅统计SQL语句数量,而非受影响行数,批量操作一条SQL会影响多行,统计结果完全不准
  • @ElementCollection的插入/删除可能被Hibernate优化为批量操作,无法准确计数
  • JPQL批量操作的SQL识别逻辑不完善,计数错误

解决方案

一、准确统计所有插入/删除的受影响行数

放弃StatementInspector,改用Hibernate原生拦截机制或数据库系统表统计,覆盖所有操作场景:

方案1:数据库系统表统计(适配所有操作类型)

直接查询数据库自带的统计表,获取全量插入/删除行数,适合所有操作场景(包括批量、JPQL):

public class DbStatsCountVerifier {
    private final EntityManager em;
    private long initialInserts;
    private long initialDeletes;
    // 需替换为你的业务表列表
    private final List<String> targetTables = List.of("root_entity", "child_entity", "root_entity_elements");

    public DbStatsCountVerifier(EntityManager em) {
        this.em = em;
        resetCounters();
    }

    private void resetCounters() {
        // 以MySQL为例,查询information_schema统计
        initialInserts = getTotalRows("INSERT");
        initialDeletes = getTotalRows("DELETE");
    }

    private long getTotalRows(String operation) {
        String sql = String.format("SELECT SUM(ROWS_AFFECTED) FROM information_schema.PROCESSLIST WHERE INFO LIKE '%%%s%%' AND DB = DATABASE()", operation);
        return em.createNativeQuery(sql, Long.class).getSingleResult();
    }

    public void afterInsert() {
        em.flush();
        // 可在此缓存插入行数
    }

    public void assertCountsMatch() {
        em.flush();
        long insertDiff = getTotalRows("INSERT") - initialInserts;
        long deleteDiff = getTotalRows("DELETE") - initialDeletes;
        assert deleteDiff == insertDiff : "删除行数与插入行数不匹配";
    }
}

注:不同数据库的统计表语法不同,需根据实际使用的数据库(PostgreSQL/Oracle等)调整SQL。

方案2:Hibernate Interceptor拦截实体事件(精准统计ORM操作)

通过Hibernate的EmptyInterceptor拦截实体生命周期,覆盖普通实体、@ElementCollection的插入/删除:

public class EntityOperationInterceptor extends EmptyInterceptor {
    private long insertCount = 0;
    private long deleteCount = 0;

    @Override
    public boolean onSave(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types) {
        insertCount++;
        return super.onSave(entity, id, state, propertyNames, types);
    }

    @Override
    public void onDelete(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types) {
        deleteCount++;
        super.onDelete(entity, id, state, propertyNames, types);
    }

    // 处理@ElementCollection的集合修改
    @Override
    public boolean onCollectionUpdate(Object collection, Serializable key) {
        if (collection instanceof PersistentCollection pc) {
            if (!pc.wasInitialized()) pc.initialize();
            insertCount += pc.getAddedSize();
            deleteCount += pc.getRemovedSize();
        }
        return super.onCollectionUpdate(collection, key);
    }

    public long getInsertCount() {
        return insertCount;
    }

    public long getDeleteCount() {
        return deleteCount;
    }

    public void reset() {
        insertCount = 0;
        deleteCount = 0;
    }
}

注册拦截器(Spring Boot环境可通过配置类注入):

<property name="hibernate.session_factory.interceptor" value="your.package.EntityOperationInterceptor"/>

测试中使用:

// 初始化拦截器
EntityOperationInterceptor interceptor = em.unwrap(Session.class).getInterceptor();
interceptor.reset();

// 执行实体插入逻辑
em.flush();
long insertTotal = interceptor.getInsertCount();

// 执行删除逻辑
em.flush();
long deleteTotal = interceptor.getDeleteCount();

assert deleteTotal == insertTotal;

二、确保深度对象树被完整删除

从配置和测试验证两方面保障层级结构全量删除:

1. 实体映射配置级联删除

在实体类中配置级联规则,确保删除根节点时自动清理所有关联数据:

@Entity
public class RootEntity {
    // 级联删除子实体,同时启用孤儿移除
    @OneToMany(mappedBy = "root", cascade = CascadeType.REMOVE, orphanRemoval = true)
    private List<ChildEntity> children;

    // ElementCollection默认会级联删除集合表数据
    @ElementCollection
    private List<String> extraElements;
}

2. 测试验证所有关联表为空

删除操作完成后,直接查询所有相关业务表的行数,确保全部清零:

public void verifyHierarchyDeleted() {
    long rootCount = em.createQuery("SELECT COUNT(r) FROM RootEntity r", Long.class).getSingleResult();
    long childCount = em.createQuery("SELECT COUNT(c) FROM ChildEntity c", Long.class).getSingleResult();
    long elementCount = em.createNativeQuery("SELECT COUNT(*) FROM root_entity_extra_elements", Long.class).getSingleResult();

    assert rootCount == 0;
    assert childCount == 0;
    assert elementCount == 0;
}

内容的提问来源于stack exchange,提问作者Adrian Jałoszewski

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.07 23:45:34