MongoDB迁移到金仓数据库:商品属性继续灵活存,订单报表交给 SQL
做 MongoDB迁移,开发人员最关心的往往很实际:原来的 MongoTemplate 还能不能用?商品属性是不是都要拆成数据库字段?订单和商品之间的关联查询,能不能少写一些 Java 代码?
这些问题背后,其实是两种不同的诉求。已有接口希望尽量少改,新增需求又希望用上 SQL 的关联、聚合和事务能力。如果迁移时只关注"数据是否导入成功",很容易忽略应用后续怎么开发、怎么维护。
电科金仓 KingbaseES 的 MongoDB 兼容能力,以及基于 SQL 的文档数据处理能力,值得放在同一个业务场景里讨论。本文就以商品扩展属性和订单统计为例,看看文档模型与关系模型如何各自发挥作用,也把迁移中容易混淆的几个地方说清楚。 
一、商品属性适合文档,订单金额适合明确的字段
假设我们正在开发一个商品管理后台。商品编号、名称、上下架状态比较稳定,但不同品类的规格差别很大:保温杯有容量、内胆材质,显示器有分辨率、刷新率,服装还有尺码、面料。
把所有属性都放进一张宽表,字段会不断增加;拆成属性名、属性值的通用明细表,又会让查询和类型约束变复杂。此时,使用文档保存扩展属性很自然:
json
{
"productCode": "CUP-001",
"name": "轻量保温杯",
"category": "cup",
"spec": {
"capacityMl": 500,
"material": "316"
},
"tags": ["便携", "通勤"]
}
但订单不是同一种情况。成交金额需要准确计算,支付状态需要明确约束,订单明细与商品之间需要稳定的关联标识。这些字段具有清晰的业务含义,采用关系表更容易表达规则。
真正让开发变麻烦的,是需求开始横跨这两类数据。比如运营提出:"统计十月份,容量不低于 500 毫升、316 内胆保温杯的已支付销售额。"
如果商品文档和订单分别存放在两套数据库中,应用通常需要先查商品编号,再查询订单,最后整理结果。数据量一大,还要处理编号集合过长、分批查询、两次读取时间不一致等问题。
我的看法是,迁移时不必急着统一所有数据的外形。值得统一的是访问和处理过程:稳定字段继续明确建模,变化较多的属性继续保留文档结构,让数据库承担适合它完成的关联计算。
二、先分清两条迁移路线,避免代码改到一半才发现选错了
根据提供的产品资料,KES MongoDB 兼容版通过配置协议兼容能力和相关扩展,可以接受 MongoDB 客户端连接。这为保留原有文档访问方式提供了基础。
不过,"通过 MongoDB 协议访问"和"通过 SQL 操作文档"是两个层面的问题。
| 路线 | 主要目标 | 应用侧关注点 |
|---|---|---|
| 保留 MongoDB 接口 | 尽量沿用已有集合操作和数据访问代码 | 驱动、认证、命令、聚合操作、索引及事务兼容性 |
| 采用 SQL 文档模型 | 将文档字段纳入关系查询和统一业务建模 | 数据类型映射、JSON 查询、关联条件及 ORM 映射 |
不能因为 KES 支持 JSONB,就认为创建一个 JSONB 字段后,MongoTemplate 会自动把这张表识别成集合。反过来,兼容集合的内部存储也不应该被当成普通业务表随意修改。
如果需要让 SQL 直接访问兼容集合,应使用目标版本明确提供的 SQL 接口或映射机制,并核对其参数与返回类型。没有确认这些接口之前,照着其他产品的函数名拼接查询,很容易写出看上去合理、实际执行不了的示例。
下面选择第二条路线,显式建立包含 JSONB 文档的业务表,演示"基于 SQL 的文档数据开发"。它是一种可选的数据模型改造方案,并非兼容集合的内部结构,也不意味着所有 MongoDB 迁移都必须重建模型。
三、用一组小数据,把 SQL 文档联查讲清楚
KES 在线手册提供 JSON、JSONB 类型及相关文档处理能力。使用这类类型,文档内容就能参与 SQL 表达式,而不只是作为一段字符串被原样取出。
以下示例使用 KES 文档中的 JSONB 类型和函数语法。执行前应确认目标版本与兼容模式支持对应语法,在独立测试环境按顺序运行。这里没有连接实际 KES 实例做运行验证,结果按给定样例数据推导。
先建立商品文档表和订单明细表:
sql
CREATE TABLE demo_product_doc (
product_code VARCHAR(32) PRIMARY KEY,
doc JSONB NOT NULL
);
CREATE TABLE demo_order_line (
line_id BIGINT PRIMARY KEY,
product_code VARCHAR(32) NOT NULL,
pay_status VARCHAR(16) NOT NULL,
paid_at TIMESTAMP,
paid_amount NUMERIC(12, 2) NOT NULL,
FOREIGN KEY (product_code)
REFERENCES demo_product_doc(product_code)
);
这里将商品编号单独作为关系字段保存,文档中保留名称、品类和规格。关联时不必每次从 JSON 内部提取商品编号,也避免了编号同时存在两份、修改时发生不一致的问题。
插入两件商品与三条订单明细:
sql
INSERT INTO demo_product_doc (product_code, doc)
VALUES
(
'CUP-001',
CAST('{
"name": "轻量保温杯",
"category": "cup",
"spec": {"capacityMl": 500, "material": "316"},
"tags": ["便携", "通勤"]
}' AS JSONB)
),
(
'CUP-002',
CAST('{
"name": "迷你保温杯",
"category": "cup",
"spec": {"capacityMl": 350, "material": "304"},
"tags": ["小容量"]
}' AS JSONB)
);
INSERT INTO demo_order_line
(line_id, product_code, pay_status, paid_at, paid_amount)
VALUES
(1, 'CUP-001', 'PAID',
TIMESTAMP '2026-10-08 10:00:00', 79.90),
(2, 'CUP-001', 'PAID',
TIMESTAMP '2026-10-09 11:00:00', 69.90),
(3, 'CUP-002', 'PAID',
TIMESTAMP '2026-10-09 12:00:00', 39.90);
接下来,把商品文档中的目标属性提取出来,与订单明细关联:
sql
WITH product_attr AS (
SELECT
product_code,
jsonb_extract_path_text(doc, 'name') AS product_name,
jsonb_extract_path_text(doc, 'category') AS category,
jsonb_extract_path_text(
doc, 'spec', 'material'
) AS material,
CASE
WHEN jsonb_typeof(
jsonb_extract_path(doc, 'spec', 'capacityMl')
) = 'number'
THEN CAST(
jsonb_extract_path_text(
doc, 'spec', 'capacityMl'
) AS NUMERIC
)
ELSE NULL
END AS capacity_ml
FROM demo_product_doc
)
SELECT
p.product_code,
p.product_name,
SUM(o.paid_amount) AS sales_amount
FROM product_attr p
JOIN demo_order_line o
ON o.product_code = p.product_code
WHERE p.category = 'cup'
AND p.material = '316'
AND p.capacity_ml >= 500
AND o.pay_status = 'PAID'
AND o.paid_at >= TIMESTAMP '2026-10-01 00:00:00'
AND o.paid_at < TIMESTAMP '2026-11-01 00:00:00'
GROUP BY p.product_code, p.product_name
ORDER BY sales_amount DESC, p.product_code;
按照上述样例数据,这条查询应得到:
| product_code | product_name | sales_amount |
|---|---|---|
| CUP-001 | 轻量保温杯 | 149.80 |
jsonb_extract_path_text 用于按路径提取文本,jsonb_extract_path 保留 JSON 值,jsonb_typeof 则用于判断 JSON 类型。这些函数在金仓 SQL 手册中均有说明。
这里特意没有把容量直接强制转换为数值。真实历史数据中可能同时存在 500、"500" 和 "500ml",无条件转换容易让整条查询报错。示例只接受 JSON 数字,其余情况按空值处理,不参与容量筛选。正式迁移时还应把这些异常记录单独统计出来,确认是清洗、修正还是保留,不能让它们悄悄消失在报表里。
这段 SQL 的意义,不是减少了多少行 Java 代码,而是让关联、过滤和汇总有了一个明确的执行位置。应用拿到的已经是报表结果,不需要再维护中间编号集合。
还有一个业务细节必须提前约定:这条查询按照商品的"当前属性"筛选历史订单。如果商品材质后来发生变化,历史统计也会受到影响。需要按成交时属性核算的业务,应在订单明细中保存商品快照,或关联明确的商品版本。数据库能够联查,并不能替业务决定历史口径。
四、文档仍然可以灵活更新,关键字段仍然需要规则
支持 SQL 文档开发,不只是能从 JSON 中读出字段。文档字段也可以参与更新,例如给商品增加一个适用场景:
sql
UPDATE demo_product_doc
SET doc = jsonb_set(
doc,
'{usageScene}',
CAST('"办公室"' AS JSONB),
true
)
WHERE product_code = 'CUP-001';
SELECT
product_code,
jsonb_extract_path_text(doc, 'usageScene') AS usage_scene
FROM demo_product_doc
WHERE product_code = 'CUP-001';
这里增加的是顶层属性,不需要为整个商品表增加一列。KES SQL 参考手册列出了 JSONB_SET 等文档处理函数,可用于这类路径更新。
但灵活存储不代表字段可以没有约定。容量用什么单位、材质用字符串还是编码、缺失值意味着未知还是不适用,都应该在接口契约里说清楚。否则,数据库虽然允许保存,查询时仍然要替这些差异买单。
对于长期参与关联、唯一性约束和高频筛选的属性,我倾向于评估是否提升为独立字段;对于低频、品类差异大、仍在变化的属性,继续放在文档中。这个边界可以随着业务演进调整,不必在第一次建模时就把未来几年所有属性设计完。
同样,也不应给每个 JSON 路径都创建索引。先确认常用查询,再根据目标版本的索引能力和执行计划设计索引。只有两条商品数据时做出的耗时比较,没有多少性能参考价值。
五、Spring Data、Hibernate 可以继续用,但要接对入口
对 Java 项目来说,能够继续使用熟悉的框架,是迁移成本的重要组成部分。不过,Spring Data 是一组数据访问项目的统称,不能把 Spring Data MongoDB、Spring Data JPA 和 Hibernate 当成同一种访问方式。
原有文档应用可以优先评估通过 KES MongoDB 兼容接口继续使用 Spring Data MongoDB。MongoTemplate 底层依赖 MongoDB 客户端和相应协议,因此需要验证实际驱动版本、认证参数以及业务调用的操作是否被目标版本支持。Spring 官方文档也明确展示了 MongoTemplate 与 MongoClient、数据库工厂之间的关系。
关系实体则可以走 KES JDBC 接口,由 Spring Data JPA、Hibernate 等框架管理。金仓官方手册提供了 Hibernate 方言配置说明,同时强调方言包需要与 Hibernate 核心版本匹配。旧项目中的方言配置,不能未经验证就直接复制到新的框架版本中。
对上面的商品销售报表,直接使用 JDBC 或原生 SQL 查询并映射到 DTO,通常就很清楚。没有必要为了坚持使用某一种 ORM 查询语言,把已经明确的 JSON 提取和聚合逻辑绕成多层代码。
所谓框架衔接顺畅,应当落实到项目验证:现有 Repository 是否可以保留,字段是否正确映射,分页结果是否一致,异常是否能正常转换。仅仅启动应用成功,离迁移验证完成还差很远。
尤其要注意事务。一个方法里先调用 MongoTemplate,再调用 JPA Repository,即使两个入口最终连接到同一套数据库,也不能仅凭一个 @Transactional 就认定两次操作自动处于同一个事务。连接、会话与事务管理器是否统一,需要单独确认。
六、迁移验收要比"条数一样"再多走几步
产品资料给出了协议启用参数、预加载组件以及扩展创建步骤。实际部署时,应使用包含对应能力的版本和组件,按照配套手册完成初始化、配置修改及必要的重启,再进行客户端验证。
其中,shared_preload_libraries 应在保留原有配置的基础上追加所需项,不能直接覆盖已有扩展;资料里的演示口令和单连接参数,也不能原样作为生产配置。连接成功只能证明入口可用,不能证明应用全部功能已经兼容。
进入数据迁移阶段后,我会优先检查下面几类内容:
| 检查对象 | 需要确认的事项 |
|---|---|
| 主键 | ObjectId 等类型是否保留语义,转换后能否继续正确关联 |
| 数值 | 金额精度、整数范围、数字与字符串混存是否得到处理 |
| 时间 | 时区、时间精度和时间边界查询是否一致 |
| 空值 | 字段缺失、显式 null、空字符串是否仍按业务规则区分 |
| 数组 | 元素类型、顺序及业务依赖的匹配行为是否保持一致 |
| 应用操作 | 聚合、批量写入、索引、事务和重试行为是否通过验证 |
如果选择重建 JSONB 模型,还要明确 BSON 到目标类型的映射。把扩展 JSON 文本整体塞进 JSONB,并不意味着其中的日期、ObjectId、Decimal128 就自动成为对应的数据库类型。格式能保存与业务能正确使用,是两个不同的验收点。
全量导入后的校验,也不能只比较集合条数。可以同时核对主键集合、关键字段类型、订单金额汇总,以及一组固定业务查询的结果。若源端仍持续写入,还需要经过验证的增量追平方案,或者安排明确的停写窗口。没有处理这段变化,导入成功也不代表切换时数据完整。
七、性能表应该怎么读,才不会给迁移定错目标
提供的产品资料列出了 MongoDB 与 KingbaseES with nativeAPI 在不同数据量下的操作耗时。其中,百万条数据规模的 INSERT 分别为 2275 ms 和 3498 ms,UPDATE 分别为 3083 ms 和 6413 ms。
就这组数据而言,对应测试项中 KES 的耗时更长,因此不能据此写出"迁移后读写性能全面提升"。资料摘录也没有提供完整硬件、索引、并发、持久化和测试方法信息,不能直接把这些数值套用到自己的系统。
更有意义的验证,是用实际业务链路来比较:商品条件筛选是否更稳定,订单报表是否减少了跨系统交互,应用侧结果拼装是否简化,以及整体响应时间和资源消耗是否达到要求。同时还应观察写入延迟、并发情况下的尾延迟,以及备份恢复和故障处理是否满足运行需求。
对这类商品业务,我会先验证原有文档接口能否稳定迁移,再挑一条确实需要跨模型关联的报表,尝试使用 SQL 文档开发。如果模型改造涉及数据同步或双写,则把一致性成本一并算进去,而不是只统计查询端省掉的代码。
MongoDB 迁移到金仓数据库的价值,需要在这些具体问题里体现:商品扩展属性仍然容易调整,订单金额仍然有明确类型和约束,关联分析能够交给 SQL,Java 项目也有机会继续沿用熟悉的开发框架。把每一条访问路径验证清楚,迁移之后的系统才会更容易开发和维护。