MongoDB迁移之后,Spring Data 和 Hibernate 各自接哪类请求

MongoDB迁移到 KingbaseES,代码改造量往往不由集合数量决定,而由应用实际调用了哪些 MongoDB API 决定。Repository 派生查询、聚合管道、批量写入、事务会话和索引管理,验证范围并不相同;驱动连通,只能说明连接层已经打通。

一个 Java 服务里常常不只有一种数据访问方式。文档接口沿用 Spring Data MongoDB,订单和用户实体由 Hibernate 管理,运营报表又需要 SQL 读取文档字段。把框架名字列在技术栈里,不能替代逐项回归。电科金仓的MongoDB 平滑迁移方案介绍了兼容能力和迁移路径;落到应用时,还要检查当前驱动、认证方式、调用 API 与目标版本的组合。

从代码里找出真实调用

迁移评审可以先从 Repository 和 MongoTemplate 的使用点开始。派生查询看起来简单,实际还可能带排序、分页、空值判断和嵌套字段路径。@Query 中的 JSON 片段、聚合管道、批量更新与事务会话要单独登记,不能由一个"基础 CRUD 通过"代替。

例如下面的 Repository 只覆盖按 SKU 和状态查找。分页是否稳定,还取决于排序字段能否唯一确定结果顺序;如果只按状态分页,翻页时同一条记录可能在相邻页重复出现。

java 复制代码
public interface CatalogRepository
        extends MongoRepository<CatalogDocument, String> {

    Optional<CatalogDocument> findBySku(String sku);

    Page<CatalogDocument> findByPublishState(
            String publishState, Pageable pageable);
}

Pageable 查询的回归样例应固定总数、排序字段、边界页和空结果。若业务要求按更新时间倒序,建议再用 SKU 作为次级排序键:

java 复制代码
Pageable page = PageRequest.of(
        pageNumber,
        pageSize,
        Sort.by(Sort.Order.desc("updatedAt"),
                Sort.Order.asc("sku")));

Page<CatalogDocument> result = catalogRepository
        .findByPublishState("PUBLISHED", page);

另一个常见入口是 MongoTemplate。它通常承接动态条件、更新语句和聚合查询,代码搜索时容易被忽略。下面的条件由租户、状态和嵌套属性组成,测试时要分别覆盖字段存在、字段缺失、数组为空和数值类型不匹配等文档:

java 复制代码
Query query = Query.query(
        Criteria.where("tenantId").is(tenantId)
                .and("publishState").is("PUBLISHED")
                .and("attributes.warranty.months").gte(24))
        .with(Sort.by(Sort.Order.asc("sku")))
        .limit(200);

List<CatalogDocument> rows = mongoTemplate.find(
        query, CatalogDocument.class, "catalog");

这种调用清单比"Spring Data 支持"几个字更有用。迁移前可以按方法记录集合、操作类型、是否使用事务、输入边界和预期结果。应用日志中的慢查询也要纳入,低频的月末批处理不一定会出现在日常联调流量里。

Spring Data 保留文档访问习惯

Spring Data MongoDB 的 Repository 和模板接口能让现有服务继续用熟悉的对象模型处理文档。连接配置仍要对应目标端给出的服务地址、认证库和 TLS 要求;账号密码不能写在源码或日志里,配置注入方式沿用项目的密钥管理机制。连接成功后,先验证读写、更新条件、删除范围和异常返回,再跑业务层测试。

更新尤其需要关注匹配范围。下例只根据 SKU 更新一个字段,调用后应检查匹配数与修改数;若业务要求"没有找到就创建",则要单独验证 upsert 行为,不能把普通更新成功当成插入也正确。

java 复制代码
Query filter = Query.query(
        Criteria.where("tenantId").is(tenantId)
                .and("sku").is(sku));
Update update = new Update()
        .set("publishState", "PUBLISHED")
        .currentDate("updatedAt");

UpdateResult result = mongoTemplate.updateFirst(
        filter, update, CatalogDocument.class, "catalog");

if (result.getMatchedCount() != 1) {
    throw new IllegalStateException("catalog record not found");
}

数组更新、正则条件、聚合管道和批量写入要按应用实际用法验证。文档数据库的表达能力很宽,兼容框架不意味着所有操作符、驱动版本和参数组合都自动一致。测试样例应来自代码调用点,而不是只用一组手写的 find 和 save 做冒烟。

Hibernate 管关系实体,事务边界写在服务层

Hibernate 适合继续处理结构明确、需要关系约束的实体。实体类映射到目标端的表,外键、唯一约束和事务仍由关系数据库连接管理。下面的实体示例只包含订单主数据,不把文档里的任意嵌套属性都塞进 Java 字段:

java 复制代码
@Entity
@Table(name = "sales_order")
public class SalesOrder {
    @Id
    @Column(name = "order_id")
    private Long orderId;

    @Column(name = "customer_id", nullable = false)
    private Long customerId;

    @Column(name = "order_status", nullable = false, length = 20)
    private String orderStatus;

    @Column(name = "created_at", nullable = false)
    private LocalDateTime createdAt;
}

需要把订单与文档状态一起改变时,先确认两个框架实际使用的事务管理器。一个 @Transactional 注解通常只覆盖它绑定的事务管理器;不能因为两个连接地址属于同一套数据库产品,就默认两种客户端 API 已经加入同一个原子事务。

java 复制代码
@Transactional(transactionManager = "relationalTxManager")
public void createOrder(SalesOrder order) {
    orderRepository.save(order);
    outboxRepository.save(new OutboxEvent(
            order.getOrderId(), "ORDER_CREATED"));
}

如果文档写入失败会影响订单流程,可以把待处理事件先写进关系事务的 outbox,由单独消费者完成文档侧更新,再将处理状态落账。消费者要按事件编号幂等处理;失败时保留重试次数、错误摘要和最后尝试时间。这样故障恢复有记录可查,也不会把两个事务管理器的边界藏在一个服务方法里。

SQL 负责另一类读取,不代替框架回归

运营分析偶尔需要直接从 SQL 读取文档字段,例如按嵌套属性筛选商品。文档存储结构可以保留在 JSON 列中,SQL/JSON 函数负责从路径读取值。以下字段和数据是演示用的最小模型,JSON_VALUE 的具体可用语法应按目标版本和兼容模式确认:

sql 复制代码
CREATE TABLE catalog_document (
    sku          VARCHAR(40) PRIMARY KEY,
    document_body JSON NOT NULL,
    updated_at   TIMESTAMP NOT NULL
);

SELECT sku,
       JSON_VALUE(document_body, '$.attributes.color') AS color,
       CAST(JSON_VALUE(document_body,
                       '$.attributes.warrantyMonths') AS INTEGER)
           AS warranty_months
FROM catalog_document
WHERE CAST(JSON_VALUE(document_body,
                      '$.attributes.warrantyMonths') AS INTEGER) >= 24
ORDER BY updated_at DESC, sku;

这类 SQL 适合报表、批量核对和数据治理,不表示 Java 业务调用必须全部改成 JDBC。框架层负责领域对象的写入和应用规则,SQL 查询负责需要明确筛选口径的分析任务。路径拼错、数字被存成字符串、字段缺失等情况都要有回归数据;条件值使用参数绑定,避免把用户输入拼成 SQL。

迁移后的数据核验也可以用 SQL 先做轮廓检查:

sql 复制代码
SELECT JSON_VALUE(document_body, '$.publishState') AS publish_state,
       COUNT(*) AS document_count,
       MAX(updated_at) AS latest_update
FROM catalog_document
GROUP BY JSON_VALUE(document_body, '$.publishState')
ORDER BY publish_state;

总数和分组分布适合发现漏批次,不能证明每个嵌套数组和日期字段都正确。抽样时应按业务主键回到源文档,对照目标 JSON、框架反序列化对象和 SQL 取值。高风险集合再补全量主键校验、应用回归和写入期间的数据一致性检查。

验收清单落到方法,而不是框架名称

一个应用的验收表可以按操作类型拆开:Repository 派生查询、模板查询、更新和删除、批量写入、事务会话、索引操作、SQL/JSON 查询。每一项都记录驱动版本、认证方式、目标数据库版本、样例输入和核验结果。对还没被业务代码调用的 API,不需要为了"功能齐全"全部测试;对正在生产使用的低频分支,也不能因为平时很少触发而跳过。

灰度期间可先将查询结果做抽样比对,写操作按业务主键检查影响行数和最终状态。日志里只保留必要的请求标识、集合名、操作类型和耗时,不记录凭证或完整敏感文档。遇到差异时,先固定一条源记录和一组调用参数,再判断是驱动映射、字段类型、API 行为还是业务代码假设导致。

Spring Data 和 Hibernate 可以在同一应用中承担不同工作:前者维持文档访问模型,后者管理关系实体,SQL/JSON 查询处理明确的文档分析需求。切换完成的标准仍是当前服务实际调用过的路径都通过回归,而不是依赖项换成了兼容驱动。

字段映射要按业务值核对

迁移文档时,字段名相同不代表 Java 对象读出来的值就相同。MongoDB 文档中的 ObjectId、日期、Decimal128、数组和缺失字段,都可能映射到 Java 中不同的类型或默认值。尤其是金额、精确时间和可空字段,不能只比较 JSON 文本长得像不像。

文档字段情况 Java 侧核对点 目标数据核对点
ObjectId 或业务主键 字符串长度、空值处理、对象引用 主键是否唯一,引用是否仍指向同一对象
日期和时区 Instant、Date、LocalDateTime 的转换规则 是否统一时区,毫秒精度是否保留
Decimal128 金额 BigDecimal 精度与舍入策略 小数位、边界值及汇总金额
缺失字段与显式 null 反序列化后的默认值 查询条件是否把两者区分开
数组与嵌套对象 泛型、空数组和未知字段 数组顺序、元素数量和嵌套路径

这张表不是要把每个文档字段都改成关系列,而是把应用依赖的类型语义列出来。源文档里没有 status 和 status: null 可能代表两种业务状态;若 Java DTO 将两者都初始化为空字符串,迁移后某些过滤条件就会改变。金额字段则要拿最大值、最小值和边界小数单独测试,防止数据库侧精度合适、Java 侧却先被 double 舍入。

主键也要明确策略。若源端用 ObjectId,目标端使用字符串保存时要保留可逆的原始标识;若应用另有业务编码,应确认它才是跨系统关联键。迁移过程中生成的新标识不能被误当作业务键传回老接口,否则缓存、消息和外部链接都会出现无法匹配的记录。

灰度读取比一次性切换更容易定位差异

能双读的服务可以挑固定的一小段业务请求,同时读取源端与目标端,比较业务字段和排序结果;不适合双写的场景,可以在测试环境回放同一份请求样本。比较内容应包括命中数、主键集合、金额合计、更新时间和空字段分布。只比总行数,会漏掉一条记录丢失、另一条重复导入后数量刚好抵消的问题。

目标端 SQL 可以先做基础分布核验:

sql 复制代码
SELECT COUNT(*) AS document_count,
       COUNT(DISTINCT sku) AS distinct_sku_count,
       MIN(updated_at) AS earliest_update,
       MAX(updated_at) AS latest_update
FROM catalog_document;

SELECT JSON_VALUE(document_body, '$.publishState') AS publish_state,
       COUNT(*) AS rows_in_state
FROM catalog_document
GROUP BY JSON_VALUE(document_body, '$.publishState')
ORDER BY publish_state;

如果总数与去重主键数不一致,先查重复键处理;如果状态分布不同,抽样查看源文档、目标 JSON 和 Java 反序列化对象。查询样本应覆盖空字段、历史版本中的文档和刚更新记录,避免只抽取结构最规整的常见数据。

灰度期间还要区分读取和写入。查询流量可以按租户或业务键分批切换,写入切换前则要确认增量数据的最终同步位置。原服务仍在写源端时,目标端结果只能算某一时刻的快照;需要在切换窗口记录增量追平点,之后再停止源端写入或启用经过验证的写入策略。

若某个调用在目标端仍有差异,灰度时只回退对应接口或租户,并保持源端继续接收写入。两端同时接收新写入会让下一轮增量核对多出重复和冲突。

框架升级和数据库迁移分开排查

如果迁移时同时升级 Spring Boot、Spring Data、Hibernate 和 JDBC 驱动,接口异常就很难归因。排期允许时,先固定框架和驱动版本完成数据库兼容回归,再做框架升级;必须一起升级时,至少保存升级前依赖树、连接配置和关键 Repository 测试结果。

前面的 MongoTemplate 查询若源端有结果、目标端为空,先拿同一组 tenantId 和 sku 重放。去掉 warranty.months 条件后结果恢复,就检查这个字段在源文档和目标 JSON 中的类型;仍为空,再核对 publishState 和租户条件。找到具体差异后,再查对应的驱动请求和目标端返回值。Hibernate 实体映射异常则核对 JDBC 方言、列类型和约束。 重放时把这组入参记下来,避免两端实际查的不是同一份数据。

生产切换前,在预发布环境重建一次连接池,确认新连接确实指向目标端;月末批处理和异常补偿也单独回放,常用接口联调不一定会经过这些分支。切换窗口以增量追平点为界,按业务主键核对目标文档和更新时间,确认数据跟上后再移除旧连接配置。

相关推荐
画中有画1 小时前
在自动化脚本中操作本地数据库
数据库·oracle·自动化
Decay__1 小时前
09_MySQL 闪回与时间点恢复 —— binlog 回放、mysqlbinlog、PITR 与误删恢复
运维·数据库·sql·mysql·架构
太理摆烂哥2 小时前
MySQL用户管理
数据库·mysql
梦影_4 小时前
MySQL进阶教程(五)—— SQL调优(下)
android·大数据·数据库·后端·sql·mysql·database
考虑考虑4 小时前
mariadb数据库
数据库·后端·mariadb
John_Hui5 小时前
Windows 10 安装版本:mysql-8.1.0-winx64.zip
数据库·windows·mysql
莫晓燚5 小时前
双机部署若依 (RuoYi‑Vue) 传统环境部署---实战
linux·运维·数据库·redis·mysql·nginx·ruoyi
updayday8546 小时前
客户附件不该发往旧邮箱,如何在发送前拦截 - Ping64 邮件安全管控
大数据·网络·数据库·安全·智能路由器
倔强的石头_6 小时前
连接异常排障手册:拒连、超时、认证失败、空闲断开
数据库