xbatis 1.10.8:用更少的持久层代码完成单表与连表查询

2026-09-13 21 预计阅读时间: 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.

预计阅读时间:11 分钟

xbatis 1.10.8 正式发布。根据版本摘要,本次版本包含部分代码优化,并让 xbatis-ddl-auto 支持 SYNC 模式。对使用 MyBatis 体系的 Java 项目来说,xbatis 的价值不只是少写几段 XML,而是把常见的单表查询、条件组合和连表场景收敛到更简洁的 API 中。

ORM 是否值得使用,关键不在于“能不能生成 SQL”,而在于它能否让开发者快速表达查询意图,同时保留对 SQL、索引和执行计划的控制。xbatis 这类工具解决的正是持久层中重复度最高、维护成本却不低的部分。

1.10.8 关注点:SYNC 模式进入 DDL 自动化

本次摘要明确提到,xbatis-ddl-auto 新增了 SYNC 模式支持。可以把它理解为一种面向开发和测试环境的数据库结构同步能力:应用启动或初始化阶段,根据实体与表结构定义,尝试让数据库结构跟上代码模型。

这里需要区分几种常见模式:

  • NONE:不执行 DDL,适合生产环境或由独立迁移工具管理数据库。
  • CREATE:创建表结构,通常适合一次性初始化或临时环境。
  • UPDATE:根据模型尝试增量更新结构。
  • SYNC:让实体定义与数据库结构进行同步,具体支持的字段、索引、约束和删除策略需要以当前版本文档为准。

一个可以改造到项目中的配置示例如下。配置键名和枚举值应以项目实际集成方式为准,下面展示的是使用 SYNC 模式时的配置意图:

# application-dev.yml
xbatis:
  ddl-auto:
    mode: SYNC

建议只在本地开发、集成测试或一次性验证环境启用自动 DDL。生产环境仍应使用 Flyway、Liquibase 或团队已有的 SQL 迁移流程,并在发布前审查建表、改列、索引和数据迁移语句。自动同步可以节省重复劳动,但不应替代可审计的数据库变更记录。

ORM 的价值:减少重复,而不是隐藏数据库

传统 MyBatis 项目通常需要同时维护实体类、Mapper 接口、XML SQL、结果映射和条件分支。单表查询一旦增加分页、排序、可选条件和字段投影,XML 很容易从几行变成几十行。

xbatis 的使用思路更接近“用 API 描述查询结构”:

  • 单表场景直接围绕实体和条件构建 SQL。
  • 需要关联时显式表达表之间的连接关系。
  • 不需要关联时仍然可以保持单表查询,避免为了复用一段 SQL 而引入不必要的 JOIN。
  • 常见的条件组合、分页和排序可以由框架统一处理。

这种方式的收益通常集中在三个地方:

  1. 重复代码减少:大量简单 CRUD 不再需要为每个实体重复编写完整 XML。
  2. 修改范围更小:字段、条件和查询逻辑集中在相对紧凑的代码中。
  3. 查询意图更清楚:开发者可以直接看到查询使用了哪些条件、关联了哪些表。

摘要中提到,单表、连表以及不连表的场景都可以使用,并可能减少三分之一甚至三分之二的持久层代码。实际节省比例取决于项目的查询复杂度、历史 XML 数量和团队编码规范,不能简单理解为所有 SQL 都能自动替换。

一个可改造的查询示例

下面示例使用 Java 展示推荐的组织方式。由于不同 xbatis 集成方式可能使用不同的实体注解、Mapper 基类或条件构造器名称,示例中的 XbatisQueryeqleftJoin 等调用代表对应版本中的同类 API,接入时应替换为项目实际 API。

业务需求是查询有效订单,同时返回用户名称;当调用方不需要用户信息时,保持单表查询,不额外执行 JOIN:

import java.util.List;

public final class OrderRepository {
    private final OrderMapper orderMapper;

    public OrderRepository(OrderMapper orderMapper) {
        this.orderMapper = orderMapper;
    }

    public List<OrderSummary> findOrders(OrderSearch request) {
        XbatisQuery<OrderEntity> query = XbatisQuery.from(OrderEntity.class)
                .select("id", "user_id", "status", "total_amount", "created_at")
                .eq("deleted", false)
                .eqIfPresent("status", request.status())
                .geIfPresent("created_at", request.createdFrom())
                .ltIfPresent("created_at", request.createdTo())
                .orderByDesc("created_at")
                .page(request.page(), request.size());

        if (!request.includeUser()) {
            return orderMapper.selectSummary(query);
        }

        return orderMapper.selectSummary(
                query.leftJoin("sys_user", "sys_user.id = order.user_id")
                     .selectAs("sys_user.username", "user_name")
        );
    }

    public record OrderSearch(
            String status,
            String createdFrom,
            String createdTo,
            boolean includeUser,
            int page,
            int size
    ) {}

    public record OrderSummary(
            Long id,
            Long userId,
            String status,
            String userName,
            Long totalAmount
    ) {}

    // 这里代表项目中的 xbatis Mapper 接口或基类。
    interface OrderMapper {
        List<OrderSummary> selectSummary(XbatisQuery<OrderEntity> query);
    }

    static final class OrderEntity {}

    // 这里代表当前 xbatis 版本对应的查询构造器。
    static final class XbatisQuery<T> {
        static <T> XbatisQuery<T> from(Class<T> type) { throw new UnsupportedOperationException(); }
        XbatisQuery<T> select(String... columns) { throw new UnsupportedOperationException(); }
        XbatisQuery<T> selectAs(String column, String alias) { throw new UnsupportedOperationException(); }
        XbatisQuery<T> eq(String column, Object value) { throw new UnsupportedOperationException(); }
        XbatisQuery<T> eqIfPresent(String column, Object value) { throw new UnsupportedOperationException(); }
        XbatisQuery<T> geIfPresent(String column, Object value) { throw new UnsupportedOperationException(); }
        XbatisQuery<T> ltIfPresent(String column, Object value) { throw new UnsupportedOperationException(); }
        XbatisQuery<T> orderByDesc(String column) { throw new UnsupportedOperationException(); }
        XbatisQuery<T> page(int page, int size) { throw new UnsupportedOperationException(); }
        XbatisQuery<T> leftJoin(String table, String condition) { throw new UnsupportedOperationException(); }
    }
}

这个例子的重点不是强行规定某个方法名,而是体现三项工程决策:

  • 只选择接口真正需要的字段,避免无意中读取大字段。
  • 可选条件统一由 IfPresent 一类方法处理,避免手写大量 if 和 XML 分支。
  • 只有在调用方确实需要用户信息时才增加 JOIN,避免把所有查询都变成复杂关联查询。

对于复杂统计、窗口函数、数据库特有语法和需要精细调优的 SQL,仍然可以保留手写 SQL。ORM 的目标是覆盖高频、稳定、结构清晰的查询,而不是消灭所有 SQL。

与 MyBatis XML 的边界

xbatis 建立在 MyBatis 生态之上,这意味着团队通常不需要放弃已有的 Mapper、事务和数据源体系。更现实的迁移策略是增量替换:

  • 新增的简单 CRUD 优先使用 xbatis。
  • 已经稳定运行、拥有复杂 SQL 的模块暂时保留 XML。
  • 对重复度高的列表查询,先抽取条件和分页逻辑,再评估是否迁移。
  • 迁移后对比生成 SQL、参数绑定、返回结果和执行计划。

需要特别注意四类风险:

  1. 生成 SQL 不等于高性能 SQL:必须检查索引命中、JOIN 顺序、排序和分页方式。
  2. 动态排序不能直接拼接用户输入:排序字段应使用白名单映射,不能把请求参数原样放入 SQL。
  3. 自动 DDL 可能产生不可逆变化:线上环境不能依赖未经审查的结构同步。
  4. 抽象层过厚会增加排障成本:生产环境要打开 SQL 日志或提供可观测的 SQL 追踪能力。

落地检查清单

可以按下面的顺序评估 1.10.8:

  • 在独立开发库确认 SYNC 对建表、加字段、索引和约束的实际行为。
  • 选一个单表 CRUD 模块,比较 XML 行数、测试数量和生成 SQL。
  • 选一个包含可选 JOIN 的列表接口,检查不需要关联数据时是否仍执行 JOIN。
  • 为分页、排序、空条件、重复数据和软删除补充测试。
  • 用真实数据量执行 EXPLAIN,确认减少代码没有换来更差的执行计划。
  • 明确生产环境的 DDL 责任边界,自动同步和迁移脚本不要同时接管同一张表。

xbatis 1.10.8 的 SYNC 支持让开发环境的实体与表结构管理更方便,而更长期的价值仍然来自简洁的查询表达能力。适合的采用方式不是一次性重写整个持久层,而是从重复最多、规则最清楚的单表和轻量连表查询开始,在 SQL 可见、测试可验证的前提下逐步扩大使用范围。


相关推荐