xbatis 1.10.6 正式发布,这次更新的重点很直接:继续把 ORM 的日常写法往“少写、好读、可维护”方向推。来源摘要里提到两个具体变化:新增 COALESCE 函数,以及 Map 结果支持强制包含 value 为 null 的字段。前者改善 SQL 表达能力,后者解决结果映射里“字段存在但值为空”容易被误判的问题。
ORM 的价值不只是少写 SQL
很多团队现在会把“有 AI 帮忙写代码”当成减少 ORM 依赖的理由,但持久层真正麻烦的地方不是生成几行 SQL,而是长期维护:
- 查询条件会变,拼接逻辑要保持清晰。
- 单表查询、连表查询、可选字段返回要统一风格。
- 结果映射不能让调用方猜字段到底是“不存在”还是“存在但为 null”。
- SQL 函数、聚合、默认值处理要能被类型化或结构化地表达。
xbatis 的推荐点集中在 API 简单、构建 SQL 快、能减少大量持久层代码。这个方向对业务系统很实际:代码少不是目标,少掉重复、脆弱、难测的代码才是目标。
COALESCE 让默认值逻辑留在查询层
1.10.6 新增 COALESCE 函数,适合处理“字段为空时给默认值”的场景。例如用户昵称为空时展示用户名,订单备注为空时返回空字符串,统计字段为空时返回 0。
在 SQL 里,典型写法是:
SELECT
id,
COALESCE(nickname, username, 'anonymous') AS display_name
FROM user_account;
这个能力放进 ORM 的 SQL 构建 API 后,收益在于:业务代码不必到处写 if null then default,查询语义也更靠近数据来源。
可以这样实践:假设项目里使用 xbatis 的查询构建器表达字段和函数,持久层可以把“展示名”的规则集中在一个查询方法里。下面示例是按 ORM 常见风格写的改造模板,具体类名和方法名请按你项目里的 xbatis API 调整。
// 假设示例:根据你项目中的 xbatis 实际 API 调整函数入口和字段引用方式。
public List<Map<String, Object>> listUserDisplayNames(UserMapper userMapper) {
return userMapper.query()
.select(UserAccount::getId)
.selectFunc("COALESCE", "display_name",
UserAccount::getNickname,
UserAccount::getUsername,
"anonymous"
)
.listMap();
}
运行前需要替换三处内容:
UserMapper:你的 Mapper 类型。UserAccount:你的实体类。selectFunc/listMap:替换成项目中 xbatis 1.10.6 的实际函数构建和 Map 查询方法。
如果你不确定 ORM 生成的 SQL 是否符合预期,建议在测试环境打开 SQL 日志,直接观察 COALESCE(...) AS display_name 是否出现在最终 SQL 中。
Map 结果保留 null:这是接口契约问题
另一个更新点是“Map 结果强制包含 value 为 null 的字段”。这个变化看起来小,但对 API 返回、报表导出、动态字段查询都很关键。
考虑一个接口返回 Map:
{
"id": 1001,
"nickname": null
}
和下面这个返回不是一回事:
{
"id": 1001
}
前者表示 nickname 字段被查询了,只是数据库里没有值;后者可能表示字段没有查询、被过滤、映射丢失,或者权限不允许返回。对前端、数据同步任务、低代码表格来说,这两种语义必须区分。
可以这样写一个测试来保护这个行为。下面是 JUnit 5 示例,重点不是 xbatis 的具体调用名,而是断言 Map 必须包含空值字段。
import static org.junit.jupiter.api.Assertions.*;
import java.util.Map;
import org.junit.jupiter.api.Test;
class UserMapResultTest {
@Test
void mapResultShouldContainNullValueField() {
// 假设这是 xbatis 1.10.6 查询 Map 后得到的结果。
Map<String, Object> row = Map.of(
"id", 1001
);
// Map.of 不能存 null,这里用可变 Map 模拟真实查询结果。
java.util.Map<String, Object> result = new java.util.HashMap<>(row);
result.put("nickname", null);
assertTrue(result.containsKey("nickname"));
assertNull(result.get("nickname"));
}
}
如果你的接口会把 Map 转成 JSON,还可以补一层序列化测试,确认 JSON 输出不会把 null 字段省略。以 Jackson 为例:
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.HashMap;
import java.util.Map;
public class NullFieldJsonDemo {
public static void main(String[] args) throws Exception {
ObjectMapper mapper = new ObjectMapper();
mapper.setSerializationInclusion(JsonInclude.Include.ALWAYS);
Map<String, Object> row = new HashMap<>();
row.put("id", 1001);
row.put("nickname", null);
System.out.println(mapper.writeValueAsString(row));
}
}
预期输出:
{"nickname":null,"id":1001}
字段顺序不重要,关键是 nickname:null 不能消失。
升级时该重点看什么
xbatis 这类 ORM 的升级,不建议只看“能不能编译过”。持久层代码一旦生成 SQL 变化,影响可能会直接落到接口结果、分页、统计和导出。
建议按这个清单推进:
- 检查依赖版本是否明确升级到
1.10.6。 - 打开 SQL 日志,覆盖单表、连表、动态条件、函数字段查询。
- 给 Map 查询补测试,尤其是
null字段是否保留。 - 对使用默认值逻辑的查询,评估是否能用
COALESCE收敛到查询层。 - 对外部接口保持谨慎:如果过去默认省略 null 字段,升级后要确认调用方是否依赖旧行为。
Maven 项目可以这样检查依赖树:
mvn dependency:tree | grep -i xbatis
Gradle 项目可以这样看运行时依赖:
./gradlew dependencies --configuration runtimeClasspath | grep -i xbatis
取舍:少写代码,但要看见 SQL
xbatis 1.10.6 的两个变化都围绕“表达力”和“结果确定性”:COALESCE 让查询层能表达默认值,Map null 字段保留让结果契约更明确。
采用这类 ORM 时,不要把它当成隐藏 SQL 的黑盒。更好的方式是:用 ORM 消掉重复 CRUD 和脆弱拼接,用日志和测试盯住生成 SQL,用接口契约测试守住结果结构。这样才能真正少写 1/3 甚至更多持久层代码,同时不把维护成本挪到未来。