问题描述这篇文章要讲的是一个非常具体且棘手的问题唯一 ID 迁移。现在有一个实体User由User::$id标识看起来像这样123456finalclassUser{publicfunction__construct(publicint$id,) {}}访问它的数据的方式是通过一个名为UserRepository的仓储接口。这里提供一个简单的 SQLite 实现123456789101112131415161718192021222324interfaceUserRepository{/*** throws UserNotFoundException*/publicfunctionfindById(int$id): User;}finalclassSqliteUserRepositoryimplementsUserRepository{publicfunctionfindById(int$id): User {$sql...;$stmt$this-pdo-prepare($sql);$stmt-execute([id$id,]);// ...}}非常简单的设置。现在假设你的团队决定出于安全原因用户的唯一标识符不应再是整数而应该采用 UUID。当然不允许停机。解决方案为了绝对安全将分三个阶段实施让两个 ID 共存实战测试 UUID 实现淘汰以前的整数 ID 实现分阶段进行的主要原因是测试不足以确保一切都按预期工作。这个字段可能被其他作业通过 API 使用或者我现在甚至无法想象的东西。所以以防万一我希望能够在任何时刻安全地回滚到以前的实现。假设这里描述的每一步都有适当的测试覆盖理想情况下是在重构发生之前。步骤 1 - 从原始类型解耦无论你接下来做什么没有这个都不容易User类中的那个int原始类型就是在要求爆炸发生。如果你想从整数平滑过渡到 UUID最好的办法是首先将代码与原始类型解耦。一种方法是封装你的原始类型。我将创建一个名为UserId的类让代码依赖它而不是int12345678910111213141516171819202122232425262728finalclassUserId{publicfunction__construct(publicint$id,) {}publicfunctiongetId(): int{return$this-id;}}finalclassUser{publicfunction__construct(publicUserId$id,) {}}interfaceUserRepository{/*** throws UserNotFoundException*/publicfunctionfindById(UserId$id): User;}上面的代码应该会使重构稍微容易一些。当调用getId()时UserId仍然返回int但这没关系最重要的是我们的代码依赖于UserId——一个我们控制的类型——而不是原始整数——我们根本无法控制它。现在只需覆盖所有现有代码以使用UserId而不是int $id。1234567891011121314finalclassSqliteUserRepositoryimplementsUserRepository{publicfunctionfindById(UserId$id): User {$sql...;$stmt$this-pdo-prepare($sql);$stmt-execute([id$id-getId(),]);// ...}}到这里什么都没有改变。感觉可以安全地合并和部署不应该有任何东西会崩溃。顺便说一句测试帮助很大。一定要进行测试步骤 2 - 让两个字段共存现在确保可以向users表添加一个新字段。这样就可以了1sqliteALTERTABLEusersADDuuidVARCHAR;现在它既不能是NOT NULL也不能是UNIQUE因为每个现有记录的值都将是NULL。回到UserId类确保它现在的实现中有uuid1234567891011121314151617finalclassUserId{publicfunction__construct(publicint$id,public?UuidInterface$uuid,) {}publicfunctiongetId(): int{return$this-id;}publicfunctiongetUuid(): ?UUidInterface{return$this-uuid;}}它仍然是可空的因为嗯它在数据库中是 null现在需要确保发生两件事每个现有记录都将有一个非空的 uuid并且每个新记录都将已经带有填充的 uuid当看到在任何给定时刻users.uuid永远不会是NULL时则认为两者在数据库层都很好地共存。步骤 3 - 确保每个新记录都有 UUID在你的系统中某个地方存储着 Users。需要确保在它发生的任何地方UUID 字段都将被填充。所以给定这个旧的实现12345publicfunctioninsert(User$user): void {// insert into ...}只需用 UUID 生成来修补它应该就没问题了1234567891011publicfunctioninsert(User$user): void {$id$user-id;if($id-uuid null) {$id-uuid Uuid::uuid4();}// insert into ...}个人强烈建议你用测试覆盖这个 IF 语句以防你遗漏了导入或类似的东西。除此之外不应该引入其他回归。每个新记录现在应该都有正确填充的users.uuid。步骤 4 - 为旧记录回填 UUID 字段这可以用脚本完成。如果你使用迁移框架可能也会非常简单。现在只需要获取所有 uuid 为 null 的用户并填充它们。类似这样就可以完成12345$users getUsersWithEmptyUuid();foreach($usersas$user) {$user-id-uuid Uuid::uuid4();updateUser($user);}上面的代码并不能代表每个代码库但我想你明白了。步骤 5 - 确保一切正常运行不要急于切换实现。一定要确保系统正常运行并且在再运行系统几个小时后users.uuid不会是NULL。只有当你 100% 确定users.uuid在此表中永远不会是NULL时才进入下一步。步骤 6 - 更新 UserRepository 以使用 UUID看来现在已经可以切换到新的 UUID 实现了。但不建议盲目地切换到新实现。谨慎总比后悔好对吧首先确保用功能开关保护代码。用以下内容更新SqliteUserRepository1234567891011121314151617181920212223242526finalclassSqliteUserRepositoryimplementsUserRepository{publicfunctionfindById(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 实现这里是应该完成此操作的代码片段1sqliteCREATEUNIQUEINDEXusers_uuid_uqONusers(uuid);理想情况下你还应该使users.uuid为NOT NULL但我跳过了它因为它需要更多的 SQLite 步骤这些步骤与我想在这里演示的内容无关。好了现在应该没问题了。将你的更改传播到生产环境看看功能开关的代码现在表现如何。一切都好对吧是时候清理了。步骤 9 - 清理你的数字 ID既然东西已经部署并经过实战测试是时候清理以前的数字 id 字段了。无论你是删除实际字段还是只是不在代码中使用它这都是项目决策——什么不是呢但最终你的SqliteUserRepository会看起来像这样1234567891011121314finalclassSqliteUserRepositoryimplementsUserRepository{publicfunctionfindById(UserId$id): User {$sql...;$stmt$this-pdo-prepare($sql);$stmt-execute([uuid (string)$id-getUuid(),]);// ...}}插入记录的函数现在也值得一些关爱。让我们删除以前的 IF 语句1234567891011...publicfunctioninsert(User$user): void {$user-id-uuid Uuid::uuid4();// insert logic}...如果你决定也从数据库中删除数字 id必须确保UserId代码也被清理并删除$id属性1234567891011finalclassUserId{publicfunction__construct(publicUuidInterface$uuid,) {}publicfunctiongetUuid(): UuidInterface {return$this-uuid;}}因为现在 UUID 完全没有理由为空也从$uuid属性中删除了问号。现在你的系统是安全的总结当然事情可能因项目而异但归根结底你将执行所描述技术的某种变体。这适用于几乎任何依赖数据的实现更改。只需记住三个阶段让两个实现共存实战测试新实现淘汰以前的实现不要害羞或羞于采取多个步骤。即使你知道之后必须删除代码实际上回滚部署或修复实时数据库比在这里描述的任何步骤都要痛苦得多。
