XBatis 1.10.7 修复多列 DISTINCT 计数优化:升级前先验证生成 SQL

2026-08-01 26 预计阅读时间: 1 分钟
来源: oschina.net AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:7 分钟

XBatis 1.10.7 的核心修复指向一个容易被忽略、却会直接影响分页总数和统计结果的问题:多列 DISTINCT 查询在转换为 COUNT 时,原有优化可能产生错误结果。对于依赖 ORM 自动生成统计 SQL 的项目,这类缺陷往往不会报异常,而是悄悄返回一个看似合理的错误数字,因此比语法错误更难发现。

为什么多列 DISTINCT 的 COUNT 容易出错

单列去重计数通常比较直接:

SELECT COUNT(DISTINCT user_id)
FROM login_record;

但业务需要按多个字段的组合去重时,语义就变成了“统计唯一元组的数量”。例如,同一个用户可以存在于多个租户中,真正的唯一键是 (tenant_id, user_id)

SELECT DISTINCT tenant_id, user_id
FROM login_record;

将它优化成计数 SQL 时,不能随意删除字段,也不能只保留其中一列。不同数据库对 COUNT(DISTINCT col1, col2) 的支持和语法也不完全一致。更稳妥、语义更明确的写法是保留原始去重查询,再在外层计数:

SELECT COUNT(*)
FROM (
    SELECT DISTINCT tenant_id, user_id
    FROM login_record
) AS distinct_rows;

1.10.7 修复的正是摘要中明确提到的“distinct 多列在 count 时优化错误”问题。它对分页插件、报表统计、去重列表总数尤其重要,因为这些场景通常由框架自动把列表查询改写为计数查询。

用最小数据集验证计数语义

升级 ORM 后,不要只检查接口能否返回数据,还要构造会暴露错误优化的重复组合。下面可以直接使用 Docker 和 PostgreSQL 运行;执行前需要安装 Docker,并确保本机的 55432 端口未被占用。

docker run --name xbatis-count-test \
  -e POSTGRES_PASSWORD=postgres \
  -p 55432:5432 \
  -d postgres:16

until docker exec xbatis-count-test pg_isready -U postgres >/dev/null 2>&1; do
  sleep 1
done

docker exec -i xbatis-count-test psql -U postgres <<'SQL'
CREATE TEMP TABLE login_record (
    tenant_id BIGINT NOT NULL,
    user_id BIGINT NOT NULL
);

INSERT INTO login_record (tenant_id, user_id) VALUES
    (1, 100),
    (1, 100),
    (1, 101),
    (2, 100);

SELECT COUNT(*) AS expected_count
FROM (
    SELECT DISTINCT tenant_id, user_id
    FROM login_record
) AS distinct_rows;
SQL

docker rm -f xbatis-count-test

预期结果是 3,而不是只按 tenant_id 计算得到的 2,也不是只按 user_id 计算得到的 2。在真实项目中,可以把 login_record 替换成业务表,并让 XBatis 执行同样的多列去重分页查询,然后核对框架返回的总数。

把生成 SQL 纳入回归测试

ORM 减少了 Mapper、条件拼装和重复 CRUD 代码,但它也把一部分 SQL 决策转移到了框架内部。涉及 DISTINCTGROUP BY、连接查询和分页时,建议临时开启 MyBatis SQL 日志。Spring Boot 项目可以这样实践:

mybatis:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

logging:
  level:
    org.xbatis: DEBUG

具体日志分类名可能随项目依赖和包结构变化;如果 org.xbatis 没有输出,可改成项目 Mapper 所在的包名。验证时重点比较两条 SQL:列表查询是否保留预期的去重字段,以及计数查询是否统计完整的字段组合。

可以为分页服务增加一个不依赖具体 ORM API 的断言模板。以下示例假设业务服务返回 total 和当前页记录,方法名需要按项目实际接口替换:

@Test
void shouldCountDistinctTenantAndUserPairs() {
    repository.insert(1L, 100L);
    repository.insert(1L, 100L);
    repository.insert(1L, 101L);
    repository.insert(2L, 100L);

    PageResult<LoginRecord> page = service.findDistinctUsers(1, 10);

    assertEquals(3L, page.getTotal());
    assertEquals(3, page.getRecords().size());
}

这段代码是可改造的测试骨架,并非 XBatis 1.10.7 的固定 API。关键在于测试数据必须同时包含“完全重复的组合”和“单列相同但组合不同的记录”,否则旧问题可能无法被触发。

ORM 省代码,但不能省掉边界测试

XBatis 的价值在于减少单表、连表和动态条件查询中的持久层样板代码,让开发者用更紧凑的 API 构建 SQL。1.10.7 的这次修复也提醒我们:生成 SQL 越自动化,越需要用结果断言守住复杂查询的语义。

升级时建议检查以下项目:

  • 搜索包含多列 DISTINCT 的列表和分页查询。
  • 对比升级前后的实际 SQL与执行计划。
  • 使用重复组合数据验证 total,不要只验证当前页记录。
  • 覆盖项目实际使用的数据库方言,避免只在 H2 等测试数据库上通过。
  • 对金额、权限范围和报表指标等关键统计保留手写 SQL 或数据库级对照测试。

如果项目没有多列去重计数,升级风险相对有限;如果分页总数直接影响结算、导出或运营报表,则应把这一修复当作数据正确性变更处理,而不是普通依赖版本更新。


相关推荐