MongoDB迁移到金仓数据库:商品属性继续灵活存,订单报表交给 SQL

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 项目也有机会继续沿用熟悉的开发框架。把每一条访问路径验证清楚,迁移之后的系统才会更容易开发和维护。

相关推荐
zhoujungis1 小时前
限流算法实测:固定窗口 2 倍溢出、令牌桶突刺 | 尘世栈帧
后端
栉甜1 小时前
Vue2学习内容(上)
前端·javascript·vue.js·学习
梦曦i2 小时前
v0.2.0 架构升级:class + reactive 重塑状态管理
前端·uni-app x
布丁爱干饭2 小时前
寄给午后
前端
小蒜学长2 小时前
编程语言在线学习平台的设计与实现(代码+数据库+LW)
java·spring boot·后端·编程语言在线学习平台·在线编码
java1234_小锋2 小时前
【技术专题】CSS3 网页技术 - CSS3 布局进阶
前端·css·css3
zhippt_10142 小时前
只剩截图没有源文件?图转PPT把图片还原成可编辑幻灯片
前端·人工智能·powerpoint
shmily麻瓜小菜鸡3 小时前
TypeScript 知识点补充(第三部分:工程化与高级类型)
前端·javascript·typescript
zhoujungis3 小时前
登录态 1 小时就掉?Django + Vue 双 Token 认证实战 | 尘世栈帧
后端