# 9 个步骤教你如何安全地迁移数据库或字段

2025-12-12 0 551

9 个步骤教你如何安全地迁移数据库字段

问题描述

这篇文章要讲的是一个非常具体且棘手的问题:唯一 ID 迁移

现在有一个实体 User,由 User::$id 标识,看起来像这样:

final class User
{
  public function __construct(
    public int $id,
  ) {}
}

访问它的数据的方式是通过一个名为 UserRepository 的仓储接口。这里提供一个简单的 SQLite 实现:

interface UserRepository
{
  /**
   * @throws  UserNotFoundException
   */
  public function findById(
    int $id
  ): User;
}

final class SqliteUserRepository
  implements UserRepository
{
  public function findById(
    int $id
  ): User {
    $sql = \"...\";
    $stmt = $this->pdo->prepare($sql);
    $stmt->execute([
      \'id\' => $id,
    ]);
    // ...
  }
}

非常简单的设置。

现在假设你的团队决定,出于安全原因,用户的唯一标识符不应再是整数,而应该采用 UUID。

当然,不允许停机。
原文链接 9 个步骤教你如何安全地迁移数据库或字段

解决方案

为了绝对安全,将分三个阶段实施:

  1. 让两个 ID 共存
  2. 实战测试 UUID 实现
  3. 淘汰以前的整数 ID 实现

分阶段进行的主要原因是,测试不足以确保一切都按预期工作。这个字段可能被其他作业通过 API 使用,或者我现在甚至无法想象的东西。

所以以防万一,我希望能够在任何时刻安全地回滚到以前的实现。

假设这里描述的每一步都有适当的测试覆盖,理想情况下是在重构发生之前。

步骤 1 – 从原始类型解耦

无论你接下来做什么,没有这个都不容易!User 类中的那个 int 原始类型就是在要求爆炸发生。

如果你想从整数平滑过渡到 UUID,最好的办法是首先将代码与原始类型解耦。一种方法是封装你的原始类型。我将创建一个名为 UserId 的类,让代码依赖它而不是 int

final class UserId
{
  public function __construct(
    public int $id,
  ) {}

  public function getId(): int
  {
    return $this->id;
  }
}

final class User
{
  public function __construct(
    public UserId $id,
  ) {}
}

interface UserRepository
{
  /**
   * @throws  UserNotFoundException
   */
  public function findById(
    UserId $id
  ): User;
}

上面的代码应该会使重构稍微容易一些。当调用 getId() 时,UserId 仍然返回 int,但这没关系!最重要的是,我们的代码依赖于 UserId——一个我们控制的类型——而不是原始整数——我们根本无法控制它。

现在只需覆盖所有现有代码以使用 UserId 而不是 int $id

final class SqliteUserRepository
  implements UserRepository
{
  public function findById(
    UserId $id
  ): User {
    $sql = \"...\";
    $stmt = $this->pdo->prepare($sql);
    $stmt->execute([
      \'id\' => $id->getId(),
    ]);
    // ...
  }
}

到这里,什么都没有改变。感觉可以安全地合并和部署,不应该有任何东西会崩溃。顺便说一句,测试帮助很大。一定要进行测试!

步骤 2 – 让两个字段共存

现在确保可以向 users 表添加一个新字段。这样就可以了:

sqlite> ALTER TABLE `users` ADD `uuid` VARCHAR;

现在它既不能是 NOT NULL 也不能是 UNIQUE,因为每个现有记录的值都将是 NULL

回到 UserId 类,确保它现在的实现中有 uuid

final class UserId
{
  public function __construct(
    public int $id,
    public ?UuidInterface $uuid,
  ) {}

  public function getId(): int
  {
    return $this->id;
  }

  public function getUuid(): ?UUidInterface
  {
    return $this->uuid;
  }
}

它仍然是可空的,因为,嗯,它在数据库中是 null!

现在需要确保发生两件事:

  1. 每个现有记录都将有一个非空的 uuid;并且
  2. 每个新记录都将已经带有填充的 uuid

当看到在任何给定时刻,users.uuid 永远不会是 NULL 时,则认为两者在数据库层都很好地共存。

步骤 3 – 确保每个新记录都有 UUID

在你的系统中,某个地方存储着 Users。需要确保在它发生的任何地方,UUID 字段都将被填充。

所以给定这个旧的实现:


public function insert(
  User $user
): void {
  // insert into ...
}

只需用 UUID 生成来修补它,应该就没问题了:

public function insert(
  User $user
): void {
  $id = $user->id;

  if ($id->uuid === null) {
    $id->uuid = Uuid::uuid4();
  }

  // insert into ...
}

个人强烈建议你用测试覆盖这个 IF 语句,以防你遗漏了导入或类似的东西。除此之外,不应该引入其他回归。

每个新记录现在应该都有正确填充的 users.uuid

步骤 4 – 为旧记录回填 UUID 字段

这可以用脚本完成。如果你使用迁移框架,可能也会非常简单。

现在只需要获取所有 uuid 为 null 的用户并填充它们。类似这样就可以完成:

$users = getUsersWithEmptyUuid();
foreach ($users as $user) {
  $user->id->uuid = Uuid::uuid4();
  updateUser($user);
}

上面的代码并不能代表每个代码库,但我想你明白了。

步骤 5 – 确保一切正常运行

不要急于切换实现。一定要确保系统正常运行,并且在再运行系统几个小时后,users.uuid 不会是 NULL

只有当你 100% 确定 users.uuid 在此表中永远不会是 NULL 时,才进入下一步。

步骤 6 – 更新 UserRepository 以使用 UUID

看来现在已经可以切换到新的 UUID 实现了。但不建议盲目地切换到新实现。

谨慎总比后悔好,对吧?首先确保用功能开关保护代码。用以下内容更新 SqliteUserRepository

final class SqliteUserRepository
    implements UserRepository
{
  public function findById(UserId $id): User
  {
    if (
      isFeatureFlagActive(\'enableNewUsersUuidImplementation\')
    ) {
      // 新实现,使用 Uuid
      $sql = \"...\";
      $stmt = $this->pdo->prepare($sql);
      $stmt->execute([
        \'uuid\' => (string) $id->getUuid(),
      ]);
      // ...
    } else {
      // 旧实现,使用整数 $id
      $sql = \"...\";
      $stmt = $this->pdo->prepare($sql);
      $stmt->execute([
        \'id\' => $id->getId(),
      ]);
      // ...
    }
  }
}

长话短说:如果请求的功能已启用,isFeatureFlagActive() 返回 TRUE,否则返回 FALSE。它可以基于配置、数据库条目或环境变量。这在这里不相关。

重要的是,你可以更改 isFeatureFlagActive() 的返回值,而无需重新部署代码。这样你就可以安全地回滚到以前的实现,没有太多摩擦。

步骤 7 – 部署、启用和监控

首先部署它,确保 isFeatureFlagActive() 始终返回 FALSE,这样就会选择原始实现。

然后将 isFeatureFlagActive() 切换为返回 TRUE,这样就会选择新实现——同样,这可以通过数据库记录、环境变量、SaaS 工具或你喜欢的任何东西来完成。

哦不!出问题了!网站突然变得超级慢!!

关闭你的功能开关,这样 isFeatureFlagActive() 将再次返回 FALSE

事情似乎又恢复正常了。回到你的 IDE,试着弄清楚发生了什么。也许做一些点击测试和调试来理解是什么导致它如此缓慢。

最终你会意识到你没有索引 users.uuid 列,所以由于你的巨大表,查询它变得超级慢。尽快修复它!

步骤 8 – 使 UUID 唯一并建立索引

由于使用的是 SQLite 实现,这里是应该完成此操作的代码片段:

sqlite> CREATE UNIQUE INDEX `users_uuid_uq` ON `users`(`uuid`);

理想情况下,你还应该使 users.uuidNOT NULL,但我跳过了它,因为它需要更多的 SQLite 步骤,这些步骤与我想在这里演示的内容无关。

好了,现在应该没问题了。将你的更改传播到生产环境,看看功能开关的代码现在表现如何。

一切都好,对吧?是时候清理了。

步骤 9 – 清理你的数字 ID

既然东西已经部署并经过实战测试,是时候清理以前的数字 id 字段了。

无论你是删除实际字段还是只是不在代码中使用它,这都是项目决策——什么不是呢?

但最终你的 SqliteUserRepository 会看起来像这样:

final class SqliteUserRepository
    implements UserRepository
{
  public function findById(
    UserId $id
  ): User {
    $sql = \"...\";
    $stmt = $this->pdo->prepare($sql);
    $stmt->execute([
      \'uuid\' => (string) $id->getUuid(),
    ]);
    // ...
  }
}

插入记录的函数现在也值得一些关爱。让我们删除以前的 IF 语句:

...

public function insert(
  User $user
): void {
  $user->id->uuid = Uuid::uuid4();

  // insert logic
}

...

如果你决定也从数据库中删除数字 id,必须确保 UserId 代码也被清理,并删除 $id 属性:

final class UserId
{
  public function __construct(
    public UuidInterface $uuid,
  ) {}

  public function getUuid(
  ): UuidInterface {
    return $this->uuid;
  }
}

因为现在 UUID 完全没有理由为空,也从 $uuid 属性中删除了问号。现在你的系统是安全的!

总结

当然,事情可能因项目而异,但归根结底,你将执行所描述技术的某种变体。

这适用于几乎任何依赖数据的实现更改。只需记住三个阶段:

  1. 让两个实现共存
  2. 实战测试新实现
  3. 淘汰以前的实现

不要害羞或羞于采取多个步骤。即使你知道之后必须删除代码!实际上回滚部署或修复实时数据库比在这里描述的任何步骤都要痛苦得多。

收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

申明:本文由第三方发布,内容仅代表作者观点,与本网站无关。对本文以及其中全部或者部分内容的真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。本网发布或转载文章出于传递更多信息之目的,并不意味着赞同其观点或证实其描述,也不代表本网对其真实性负责。

左子网 编程相关 # 9 个步骤教你如何安全地迁移数据库或字段 https://www.zuozi.net/36044.html

常见问题
  • 1、自动:拍下后,点击(下载)链接即可下载;2、手动:拍下后,联系卖家发放即可或者联系官方找开发者发货。
查看详情
  • 1、源码默认交易周期:手动发货商品为1-3天,并且用户付款金额将会进入平台担保直到交易完成或者3-7天即可发放,如遇纠纷无限期延长收款金额直至纠纷解决或者退款!;
查看详情
  • 1、描述:源码描述(含标题)与实际源码不一致的(例:货不对板); 2、演示:有演示站时,与实际源码小于95%一致的(但描述中有”不保证完全一样、有变化的可能性”类似显著声明的除外); 3、发货:不发货可无理由退款; 4、安装:免费提供安装服务的源码但卖家不履行的; 5、收费:价格虚标,额外收取其他费用的(但描述中有显著声明或双方交易前有商定的除外); 6、其他:如质量方面的硬性常规问题BUG等。 注:经核实符合上述任一,均支持退款,但卖家予以积极解决问题则除外。
查看详情
  • 1、左子会对双方交易的过程及交易商品的快照进行永久存档,以确保交易的真实、有效、安全! 2、左子无法对如“永久包更新”、“永久技术支持”等类似交易之后的商家承诺做担保,请买家自行鉴别; 3、在源码同时有网站演示与图片演示,且站演与图演不一致时,默认按图演作为纠纷评判依据(特别声明或有商定除外); 4、在没有”无任何正当退款依据”的前提下,商品写有”一旦售出,概不支持退款”等类似的声明,视为无效声明; 5、在未拍下前,双方在QQ上所商定的交易内容,亦可成为纠纷评判依据(商定与描述冲突时,商定为准); 6、因聊天记录可作为纠纷评判依据,故双方联系时,只与对方在左子上所留的QQ、手机号沟通,以防对方不承认自我承诺。 7、虽然交易产生纠纷的几率很小,但一定要保留如聊天记录、手机短信等这样的重要信息,以防产生纠纷时便于左子介入快速处理。
查看详情

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务