CRUD 操作
四个动作,完整的循环
CRUD 是四个英文单词的缩写:Create、Read、Update、Delete。创建、读取、更新、删除。
这四个操作描述了数据的完整生命周期。数据被创建,被读取,被修改,最终被删除。无论是数据库、API、还是用户界面,几乎所有系统都围绕这四个动作构建。
看起来很基础,但基础的东西往往是最重要的。它们是地基,是公理,是你构建一切的起点。
💡 Click the maximize icon to view in fullscreen
为什么是这四个
你可能会想,为什么不是五个,不是三个,而恰好是这四个?
因为这四个操作是完备的。它们覆盖了数据可能经历的所有状态转换。
Create 让数据从无到有。没有创建,就没有数据。这是起点。
Read 让数据被使用。数据不被读取就没有价值。存储本身不是目的,读取才是。
Update 让数据适应变化。世界在变,需求在变,数据也要变。没有更新,数据会过时。
Delete 让数据归于虚无。不是所有数据都应该永久保留。有些数据失去意义,有些数据需要遗忘,有些数据必须清除。删除是必要的结束。
这四个操作构成了一个闭环。数据在这个循环中流转,系统在这个循环中运作。
权限的边界
CRUD 不只是技术操作,它也是权限设计的基本单位。
你给一个用户什么权限?你不会说"给他 50% 的权限"或"给他中等权限"。你会说:他可以读取,但不能修改;他可以创建,但不能删除。
权限总是以 CRUD 为维度划分的。
这四个操作的权重不同。读取是最轻的,删除是最重的。
几乎所有人都有读取的权限。读取不改变状态,风险最小。创建和更新有一定门槛,因为它们会改变系统状态。删除权限通常被严格限制,因为它不可逆。
这种不对称性不是随意的。它反映了操作的破坏性和可恢复性。越破坏性强、越不可逆的操作,权限控制就越严格。
最小权限原则
用户应该拥有完成任务所需的最小权限集合。不是"默认全给,出问题再收回",而是"默认不给,需要时再授予"。
读取 < 创建 < 更新 < 删除,权限的授予应该遵循这个递增的谨慎度。
删除的两种哲学
删除是 CRUD 中最特殊的操作。
第一种删除是物理删除。数据真的被移除了,从数据库中抹去,不留痕迹。这种删除彻底、干净,但不可逆。
第二种删除是逻辑删除。数据还在,只是被标记为"已删除"。查询时过滤掉这些记录,对用户来说它们已经不存在了,但系统仍然保留着数据。
为什么需要逻辑删除?
因为很多删除是后悔的。用户误操作,想要恢复。数据需要审计,需要追溯。有些删除在业务上是删除,但在法律上不允许真正删除。
逻辑删除用一个字段(比如 is_deleted 或 deleted_at)来标记状态,数据本身不动。这带来了灵活性,但也带来了复杂性。每次查询都要记得过滤已删除的数据,每个统计都要考虑这个字段。
物理删除是彻底的告别,逻辑删除是温和的隐藏。 选择哪种,取决于你对数据的态度和业务的需求。
Update 的陷阱
更新看起来简单,但它隐藏着最多的复杂性。
一个更新请求来了,系统要做什么?读取当前数据,修改部分字段,写回数据库。听起来直接,但问题在于:如果有两个更新请求同时到达会怎样?
第一个请求读取了数据,第二个请求也读取了数据。它们基于同一个初始状态做修改,然后分别写回。结果是后写入的覆盖了先写入的。前一个更新丢失了。
这叫做并发冲突。
解决方案有很多。乐观锁,悲观锁,版本号,时间戳。但核心思想是一致的:更新不是孤立的操作,它依赖于当前状态。 你不能假设你读取时的状态和写入时的状态是一样的。
另一个陷阱是部分更新。用户提交了一个表单,只修改了三个字段,其他字段呢?是保持不变,还是设为空值,还是设为默认值?
如果处理不当,部分更新会意外清空数据。你以为只改了标题,结果内容变成了 null。
并发环境下的更新操作需要额外保护
两个用户同时编辑同一篇文章。用户 A 改了标题,用户 B 改了内容。如果没有适当的并发控制,一个人的修改可能会覆盖另一个人的修改。
常见的解决方案:
- 乐观锁:使用版本号,更新时检查版本是否变化
- 悲观锁:编辑时锁定数据,直到提交或取消
- 字段级锁:只锁定被修改的字段,而不是整条记录
Read 不只是读
读取被认为是无害的。它不改变数据,不影响其他人,似乎可以随意执行。
但读取也有成本。
数据库需要处理查询,服务器需要返回数据,网络需要传输内容。如果读取请求太多太频繁,系统会被压垮。
更隐蔽的问题是数据泄露。读取本身不改变数据,但它暴露数据。如果读取权限控制不当,敏感信息会被看到。
所以读取也需要控制。谁能读,能读什么,能读多少。这不是技术问题,是业务问题,是隐私问题,是安全问题。
读取是最轻的操作,但不是免费的操作。
API 设计的对应
如果你在设计 RESTful API,CRUD 直接对应到 HTTP 方法。
- Create → POST
- Read → GET
- Update → PUT / PATCH
- Delete → DELETE
这不是巧合。REST 架构从设计之初就考虑了资源的生命周期。HTTP 方法的语义和 CRUD 的语义是对齐的。
PUT 和 PATCH 的区别在于,PUT 是完全替换,PATCH 是部分更新。这反映了前面提到的更新操作的两种模式。
这种对应让 API 的语义变得清晰。看到 POST 就知道是创建,看到 DELETE 就知道是删除。不需要读文档,方法本身就在说话。
超越 CRUD
有些操作不完全符合 CRUD 的模式。
归档是删除吗?不完全是。数据还在,只是移到了别的地方。
复制是创建吗?部分是。它创建了新数据,但这个数据不是全新的,而是基于已有数据。
发布是更新吗?也许。它改变了数据的状态,但这个状态改变有特殊的业务含义。
这些操作在底层可能是 CRUD,但在业务层面它们有自己的语义。
这提醒我们:CRUD 是技术层的抽象,不是业务层的全部。 系统需要技术层的一致性,但也需要业务层的表达力。
不要强行把所有业务操作塞进 CRUD 的框架。CRUD 是基础,但不是限制。
数据的生命,系统的逻辑
CRUD 之所以重要,不是因为它复杂,而是因为它基础。
所有复杂的系统,归根到底都在处理数据的创建、读取、更新、删除。表面上的功能千差万别,底层的操作无非这四种。
理解 CRUD,就是理解数据的生命周期。数据如何产生,如何流动,如何变化,如何消失。这个循环是系统的心跳。
权限系统围绕 CRUD 构建,因为权限本质上是"谁能对什么数据做什么操作"的问题。
API 设计基于 CRUD,因为 API 就是向外暴露数据操作的接口。
数据库设计考虑 CRUD,因为索引、约束、触发器都要服务于这些操作的效率和正确性。
CRUD 是简单的,但简单不意味着浅薄。 它是复杂系统的基本单位,是你思考数据流转的起点。
最后
当你设计一个功能,一个系统,一个架构,问自己:这里的数据是怎么创建的,谁能读取,如何更新,何时删除?
这四个问题会引导你思考权限、流程、状态、生命周期。它们会暴露设计中的模糊和矛盾。
CRUD 不是教条。它是一个框架,一个视角,一种思考数据的方式。
掌握它,不是为了背诵定义,而是为了在复杂的业务逻辑中,始终能回到最基础的问题:这个数据,是如何被创建、读取、更新、删除的?
CRUD 是否足以描述所有数据操作?在什么场景下,CRUD 模型会显得不够用?
软删除(逻辑删除)和硬删除(物理删除)在实际系统中如何选择?各自带来什么样的技术债务?
如何在 CRUD 操作中保证数据一致性?事务、锁、版本控制各自适用于什么场景?
RESTful API 之外,GraphQL 和 gRPC 如何映射 CRUD 操作?它们带来了什么新的思考方式?
在微服务架构中,跨服务的 CRUD 操作如何协调?分布式事务的复杂性如何应对?
CQRS(命令查询职责分离)模式将 Read 和 Write 分开,这种分离带来什么好处和代价?
Related Posts
Articles you might also find interesting
管理后台需要两次设计
第一次设计回答"发生了什么",第二次设计回答"我能做什么"。在第一次就试图解决所有问题,结果是功能很多但都不够深入。
文档标准是成本计算的前提
API文档不只是写给开发者看的。它定义了系统的边界、成本结构和可维护性。统一的文档标准让隐性成本变得可见。
数据库参数国际化:从 13 个迁移学到的设计原则
数据不该懂语言。当数据库参数嵌入中文标签时,系统的边界就被语言限制了。这篇文章从 13 个参数对齐迁移中提炼出设计原则——国际化不是功能,是系统设计的底层约束。
错误隔离
失败是必然的。真正的问题不是失败本身,而是失败如何蔓延。错误隔离不是为了消除失败,而是为了控制失败的范围。
实现幂等性处理,忽略已处理的任务
在代码层面识别和忽略已处理的任务,不是简单的布尔检查,而是对时序、并发和状态的深刻理解
单例模式管理 Redis 连接
连接不是技术细节,而是系统与外部世界的第一次握手,是可靠性的起点
使用Secret Token验证回调请求的合法性
在开放的网络中,信任不能被假设。Secret Token 是对身份的确认,对伪装的识别,也是对安全边界的坚守
幂等性检查
在不确定的系统中,幂等性检查是对重复的容忍,是对稳定性的追求,也是对失败的预期与接纳
PostgreSQL 原生不支持直接添加枚举值
当一个类型系统拒绝改变,它在保护什么?
告警分级与响应时间
不是所有问题都需要立即响应。RPC失败会在凌晨3点叫醒人。安全事件每15分钟检查一次。支付成功只记录,不告警。系统的响应时间应该匹配问题的紧急程度。
API 测试各种边界情况
边界情况是系统最脆弱的地方,也是最容易被忽略的地方。测试边界情况不是为了追求完美,而是为了理解系统的真实边界。
BullMQ 队列
队列不是技术选型,而是对时间的承认,对顺序的尊重,对不确定性的应对
BullMQ Worker
Worker 的本质是对时间的重新分配,是对主线的解放,也是对专注的追求
配置不会自动同步
视频生成任务永远pending,代码完美部署,队列正确配置。问题不在代码,在于配置的独立性被低估。静默失败比错误更危险。
执行数据库迁移的三种路径
CLI、MCP 与线上 SQL——每种方法背后的权衡与适用场景。迁移不只是执行命令,更是选择控制权与便利性之间的平衡点。
诊断 Supabase 连接失败:借助 MCP 工具链
连接失败不仅是配置问题,更是关于理解系统状态边界的过程。通过 Supabase MCP 与 Claude Code,让不可见的问题变得可观测。
Stripe Webhook中的防御性编程
三个Bug揭示的真相:假设是代码中最危险的东西。API返回类型、环境配置、变量作用域——每个看似合理的假设都可能导致客户损失。
让文档跟着代码走
文档过时是熵增的必然。对抗衰败的方法不是更频繁的手工维护,而是让文档"活"起来——跟随代码自动更新。三种文档形态,三种生命周期。
双重验证:Stripe生产模式的防御性切换
从测试到生产不是更换API keys,而是建立一套双重验证系统。每一步都在两个环境中验证,确保真实支付不会因假设而失败。
端到端 Postback 模拟测试
真实的测试不是模拟完美的流程,而是重现真实世界的混乱。Postback 测试的价值在于发现系统在不确定性中的表现。
在运行的系统上生长新功能
扩展不是推倒重来,而是理解边界,找到生长点。管理层作为观察者和调节器,附着在核心系统上,监测它,影响它,但不改变它的运行逻辑。
请求包含 gzip 压缩的任务结果 JSON
数据传输的本质是在空间和时间之间做选择,压缩是对带宽的节约,也是对等待的妥协
缺失值的级联效应
一个NULL值如何在调用链中传播,最终导致错误的错误消息。理解防御层的设计,在失败传播前拦截。
监控观察期法
部署不是结束,而是验证的开始。修复代码只是假设,监控数据才是证明。48小时观察期:让错误主动暴露,让数据证明修复。
用 MCP 让 Claude Code 执行 Prisma 迁移
借助 Model Context Protocol,Claude Code 可以直接操作 Supabase 云数据库,完成 Prisma schema 的迁移和部署