9个步骤带你了解PHP如何安全地迁移数据库或字段
问题描述这篇文章要讲的是一个非常具体且棘手的问题唯一 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属性中删除了问号。现在你的系统是安全的总结当然事情可能因项目而异但归根结底你将执行所描述技术的某种变体。这适用于几乎任何依赖数据的实现更改。只需记住三个阶段让两个实现共存实战测试新实现淘汰以前的实现不要害羞或羞于采取多个步骤。即使你知道之后必须删除代码实际上回滚部署或修复实时数据库比在这里描述的任何步骤都要痛苦得多。

相关新闻

Win10默认安装路径修改指南:告别C盘爆满的两种实用方法

Win10默认安装路径修改指南:告别C盘爆满的两种实用方法

C盘又红了。这大概是我这十多年帮人处理电脑问题听到频率最高的一句话,没有之一。很多人一看到C盘爆满,第一反应就是下个"清理大师"哗啦啦删一遍,运气好能腾出几个G,运气不好误删一堆缓存,然后过两个星期&am…

2026/9/25 23:16:51 阅读更多 →
Java+SSM+Flask双后端架构:高校运动会管理系统设计与实现

Java+SSM+Flask双后端架构:高校运动会管理系统设计与实现

做高校信息化系统这些年,运动会管理系统算是最容易被低估、又最值得练手的项目之一。表面上看,无非就是报名、排赛程、录成绩,但真正上手之后你会发现,这一整套逻辑牵扯到多角色权限、赛程冲突检测、按院系汇总积分、数据可视化展…

2026/9/25 23:16:51 阅读更多 →
Claude Code 文件系统地图:File reference 背后的工程边界与 TaoToken 配置骨架

Claude Code 文件系统地图:File reference 背后的工程边界与 TaoToken 配置骨架

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/25 23:16:50 阅读更多 →

最新新闻

RTP转H264文件实战:UDP裸流还原与播放链路解析

RTP转H264文件实战:UDP裸流还原与播放链路解析

简介:这份资源面向从事网络视频传输、监控系统或流媒体开发的工程师与学习者,聚焦于将RTP包中的H264数据解封装并保存为本地文件,同时借助UDP实现摄像头数据的实时读取。包内共14个文件,以6个C头文件与4个cpp源文件为核心&#xf…

2026/9/25 23:55:22 阅读更多 →
Java解析HJ212协议实战:报文结构、CRC校验与编码处理

Java解析HJ212协议实战:报文结构、CRC校验与编码处理

简介:本资源面向环保监测系统开发工程师与Java学习者,提供国标HJ212协议(污染源在线自动监控数据传输标准)的完整解析实现,可直接导入Eclipse项目调用。包内共308个文件,以197个class编译文件与100个java源…

2026/9/25 23:55:22 阅读更多 →
RK3588 RKISP驱动代码解析:从sensor出图到/dev/video节点

RK3588 RKISP驱动代码解析:从sensor出图到/dev/video节点

简介:这份资源是瑞芯微RK平台ISP子系统的Linux内核驱动源码,面向从事嵌入式Linux、摄像头图像处理与V4L2框架开发的工程师及驱动学习者。代码围绕设备树匹配机制展开,从of_device_id的匹配方式入手,完整呈现了CIF与ISP模块的驱动实…

2026/9/25 23:55:22 阅读更多 →
RAR for Linux 原生命令行工具深度指南

RAR for Linux 原生命令行工具深度指南

简介:本资源是Linux平台专用的64位RAR命令行工具v6.1.b1测试版,面向Linux系统管理员、运维工程师及需要处理RAR格式文件的开发者,解决在Ubuntu、Fedora等主流发行版中缺乏原生RAR支持的问题。压缩包共11个文件,含核心可执行文件&a…

2026/9/25 23:54:22 阅读更多 →
K8s 部署 Nacos 集群:StatefulSet 脚本与避坑指南

K8s 部署 Nacos 集群:StatefulSet 脚本与避坑指南

简介:这份资源面向需要在 Kubernetes 上部署 Nacos 集群的运维与后端开发人员,尤其适合刚接触 K8s 编排、希望跳过繁琐配置直接跑通集群的初学者。它把 Nacos 集群部署拆解为按执行顺序排列的极简脚本,覆盖数据库配置、Headless Service、Sta…

2026/9/25 23:54:22 阅读更多 →
开源免费|ClawManager深度解析:企业级OpenClaw集群管理神器

开源免费|ClawManager深度解析:企业级OpenClaw集群管理神器

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/25 23:54:22 阅读更多 →

日新闻

AI元人文:从工具使用到思维重构的深度探索

AI元人文:从工具使用到思维重构的深度探索

最近半年我一直在琢磨一件事:AI元人文到底是什么?说白了,就是“用元视角重新审视人与AI的关系”,也在“探索AI如何反向逼着我们发现自己的思考边界”。标题里的“元探索”,在我看就是一层套一层的追问——当你用AI解决…

2026/9/25 0:00:41 阅读更多 →
Python+CNN车牌识别实战:从数据预处理到模型训练与部署

Python+CNN车牌识别实战:从数据预处理到模型训练与部署

简介:基于Python与卷积神经网络的车牌识别项目,面向计算机视觉初学者及智能交通开发者,目标是帮助用户掌握从数据预处理、模型构建到实际部署的完整流程。压缩包共25个文件,包含jpg/png图像样本、py训练脚本、md说明文档、dat数据…

2026/9/25 0:00:41 阅读更多 →
Vim基础操作全攻略:保存退出、模式切换与高频命令实战

Vim基础操作全攻略:保存退出、模式切换与高频命令实战

1. 项目概述1.1 核心需求解析今天聊聊Vim。写这个题目的原因是:几乎每个后端开发者、运维人员、数据工程师某天都会遇到一个场景——深夜加班,服务器登录界面只有黑底白字,编辑器只有vi/vim,你必须在五分钟内完成一次配置修改并保…

2026/9/25 0:00:41 阅读更多 →

周新闻

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

直接铺开项目本身吧。这几个月我一直在折腾一件事:用Flutter给OpenHarmony做一款游戏集合类的App,说白了就是把若干小游戏塞进一个壳里,用统一入口分发。这个方向本身不算新鲜,真正让我花了不少心思的,是首页那堆游戏卡…

2026/9/25 19:27:14 阅读更多 →
Word表格编号全攻略:从列表编号到题注交叉引用

Word表格编号全攻略:从列表编号到题注交叉引用

写Word文档,最让人头疼的往往是那些“看起来不起眼”的小问题。比如表格编号这事:今天在表后面多加了两个空白行,明天给客户交稿前发现整个章节的编号全部错位,光是挨个改序号就能耗掉大半个下午。我前阵子帮人整理一份上百页的技…

2026/9/25 11:15:26 阅读更多 →
从第一个站到第二个站:独立开发者的静态网站选型与落地实践

从第一个站到第二个站:独立开发者的静态网站选型与落地实践

1. 项目概述1.1 核心需求解析做独立开发者这几年,说实话,第一个网站上线的那天晚上我兴奋得没睡着。但等它跑了半年,流量惨淡、功能臃肿、代码自己都懒得看第二遍之后,我才慢慢琢磨明白一个道理:第一个网站是练手&…

2026/9/25 20:29:09 阅读更多 →

月新闻

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能分类:[AI/大模型]细分主题:AI 增强型 CI/CD 流水线自动化与 GitOps 实践:Agent 工作流、工具调用与任务拆解:从原型到生产的验收清单很多团队在尝试用大…

2026/9/25 20:29:43 阅读更多 →
容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场分类:[工程技术]细分主题:Kubernetes 生产环境运维与排障实战:可复制的项目复盘模板与决策记录大部分团队的事故复盘报告,最后都变成了躺在 Confluence 或钉…

2026/9/25 20:29:31 阅读更多 →
容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步分类:[工程技术]细分主题:Docker 容器化技术与镜像安全管理:核心链路的逐步实现与关键代码取舍面对一个积累了五六年历史包袱的单体架构应用(包含 Web 接口、后台…

2026/9/25 19:27:26 阅读更多 →