用 Antigravity CLI 批量改造 DAO:把 Spanner 双写迁移变成可验证的流水线

2026-09-05 38 预计阅读时间: 1 分钟
来源: cloud.google.com 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.

预计阅读时间:10 分钟

数据库迁移真正棘手的部分,往往不是把历史数据复制到新库,而是在生产服务持续处理请求时,让新旧存储长期保持一致。Google 财务工程团队在迁移到 Spanner 时,需要为 30 多个 DAO 补齐转换器、双写分支、错误处理和单元测试。逐个手工修改不仅耗时,还容易在时间戳、空值和字段映射上产生细微偏差。

他们采用的办法,是先把迁移模式标准化,再通过 Antigravity CLI 的无头模式批量生成代码,并让构建系统负责验证和纠错。这里的重点不是“让 AI 自由改代码”,而是把 AI 放进一个输入明确、输出受限、失败可反馈的工程闭环。

三阶段迁移,解决三类不同问题

一次不中断服务的迁移可以拆成三个阶段:

  1. 历史回填:把旧存储中的已有记录复制到 Spanner,同时维护主外键和记录间的引用关系。
  2. 双写与双读:修改 DAO,使迁移窗口内的新增和更新同时进入旧存储与 Spanner;必要时从两边读取并比较结果。
  3. 接口级一致性验证:拦截或回放 RPC 流量,确认每次写入都在两个存储中产生等价结果。

这三个阶段不能互相替代。历史回填只覆盖过去的数据,双写负责迁移期间的新流量,而 API 校验用于发现单元测试不容易覆盖的序列化、默认值和调用链问题。

还要注意,所谓“双写”并不天然等于跨库原子事务。旧库成功而 Spanner 失败,或者 Spanner 已提交但调用方超时,都会形成不一致。因此 DAO 至少要记录两侧结果、保留可重试标识,并建立补偿或对账任务。金融数据场景下,静默忽略次库失败通常不可接受。

先固定 MutationConverter,再让自动化扩展

自动化改造能够稳定运行的前提,是把开放式设计问题收敛成确定的接口。源实践把领域模型到 Spanner Schema 的翻译隔离到 MutationConverter,DAO 不直接散落表名和列映射。

下面是可以据此改造的 Go 示例。运行前需要把模块路径、模型字段和表名替换为项目中的实际定义,并安装 Cloud Spanner Go SDK:

package migration

import (
    "errors"

    "cloud.google.com/go/spanner"
)

type TransferAmount struct {
    TransferID  string
    AmountCents int64
    Currency    string
}

type TransferAmountMutationConverter interface {
    ToInsertMutation(*TransferAmount) (*spanner.Mutation, error)
    ToUpdateMutation(*TransferAmount) (*spanner.Mutation, error)
}

type transferAmountConverter struct {
    tableName string
}

func NewTransferAmountConverter(tableName string) TransferAmountMutationConverter {
    return &transferAmountConverter{tableName: tableName}
}

func (c *transferAmountConverter) ToInsertMutation(entity *TransferAmount) (*spanner.Mutation, error) {
    if entity == nil {
        return nil, errors.New("entity cannot be nil")
    }

    columns := []string{
        "TransferId",
        "AmountCents",
        "CurrencyCode",
        "LastModifiedTimestamp",
    }
    values := []any{
        entity.TransferID,
        entity.AmountCents,
        entity.Currency,
        spanner.CommitTimestamp,
    }
    return spanner.Insert(c.tableName, columns, values), nil
}

func (c *transferAmountConverter) ToUpdateMutation(entity *TransferAmount) (*spanner.Mutation, error) {
    if entity == nil {
        return nil, errors.New("entity cannot be nil")
    }

    return spanner.Update(c.tableName,
        []string{"TransferId", "AmountCents", "CurrencyCode", "LastModifiedTimestamp"},
        []any{entity.TransferID, entity.AmountCents, entity.Currency, spanner.CommitTimestamp},
    ), nil
}

这个边界带来三个直接收益:字段映射可以独立测试;DAO 只负责调用顺序和错误策略;自动化工具面对的是重复、结构化的任务,而不是每次重新理解整套业务设计。

在制定转换规范时,应明确以下容易产生歧义的规则:

  • 可空字段使用 nil、Spanner nullable 类型,还是领域层包装类型。
  • 时间字段来自业务时间、注入的 FakeTimeSource,还是 spanner.CommitTimestamp
  • 插入、更新和 upsert 分别使用哪种 Mutation。
  • 金额是否统一使用最小货币单位,禁止经过浮点数转换。
  • 旧库默认值与 Spanner 默认值不同时,以哪一侧为准。

把无头 CLI 接入“生成、测试、修复”循环

IDE 聊天适合探索单个文件,但 30 个 DAO 的迁移更需要可重复执行的批处理。Antigravity CLI 的 -p 无头模式可以从脚本接收提示词,不依赖人工终端交互。

下面给出一个可改造的最小 Bash 工作流。示例假设可执行文件名为 antigravity,并使用 go test;如果项目使用内部构建系统,应替换 TEST_CMD 和代码收集逻辑。

#!/usr/bin/env bash
set -euo pipefail

DAO_NAME="${1:?usage: ./migrate-dao.sh <dao-name>}"
MAX_ATTEMPTS="${MAX_ATTEMPTS:-3}"
TEST_CMD="${TEST_CMD:-go test ./...}"
PROMPT_FILE="prompts/spanner-dual-write.md"
LOG_FILE=".migration-${DAO_NAME}.log"

[[ -f "$PROMPT_FILE" ]] || {
  echo "missing prompt template: $PROMPT_FILE" >&2
  exit 1
}

for attempt in $(seq 1 "$MAX_ATTEMPTS"); do
  context=$(rg -n --glob '*.go' "$DAO_NAME|type .*DAO|MutationConverter" . || true)
  failures=$(test -f "$LOG_FILE" && tail -n 200 "$LOG_FILE" || true)

  prompt=$(printf '%s\n\nTarget DAO: %s\n\nRepository context:\n%s\n\nPrevious test failures:\n%s\n' \
    "$(<"$PROMPT_FILE")" "$DAO_NAME" "$context" "$failures")

  antigravity -p "$prompt"

  if bash -lc "$TEST_CMD" 2>&1 | tee "$LOG_FILE"; then
    echo "migration verified for ${DAO_NAME}"
    exit 0
  fi

done

echo "migration failed after ${MAX_ATTEMPTS} attempts" >&2
exit 1

提示词文件也应进入版本控制。它不应只写“为这个 DAO 添加双写”,而要列出允许修改的目录、转换器接口、错误处理策略、测试要求和禁止事项。例如要求保留旧库为主写路径、禁止吞掉 Spanner 错误、必须注入假时间源,并为 nil、时间戳和可空字段生成测试。

测试失败后,可以把编译错误和断言输出反馈给下一轮生成,但必须限制重试次数。无限自修复循环可能反复扩大修改范围,也会掩盖真正的架构问题。每个 DAO 最好形成独立变更,方便人工审查和回滚。

一致性校验不能只看“测试通过”

单元测试应确认两个写入客户端都收到预期调用,Mutation 的表名、列顺序和值完全符合约定,并通过假时间源消除不稳定结果。不过,单元测试只能验证代码路径,还需要在预生产环境执行端到端对账。

对账时不要直接比较数据库驱动返回的原始对象。提交时间戳、空值表示和数值类型可能存在合法差异。更稳妥的方式是先定义规范化规则,再比较业务等价值;只有明确要求逐字节一致的字段,才进行字节级比较。同时记录请求 ID、业务主键、两侧版本和差异原因,以便重放与修复。

落地时的检查清单

开始批量生成前,先手工完成一两个代表性 DAO,并让团队确认其接口和测试模式。之后再把它们作为自动化参考样本。

上线前至少确认:历史回填支持断点续传;双写失败可观测且可重试;写入具有幂等键;提示词和生成规则进入版本控制;每个变更都经过编译、单测和人工审查;预生产环境持续执行新旧库对账;切流和回滚条件有明确指标。

Antigravity CLI 在这套方案中承担的是规模化代码变换,而不是迁移正确性的最终裁判。真正的安全边界仍然来自稳定的转换接口、可重复的构建测试、线上可观测性,以及能够定位和修复差异的对账机制。


相关推荐