把业务逻辑写进数据库中:老办法的新思路(以 PostgreSQL 为例)

把业务逻辑写进数据库中?这不是上个世纪的"存储过程架构"吗?

在微服务、Serverless、大数据、分布式集群盛行的今天,这个老派思路,为什么还值得认真谈一次?


📚 背景知识:什么是业务逻辑?业务逻辑在哪运行?

在开发一个系统时,我们大致可以把代码分为三类:

  1. 表示层逻辑(UI逻辑) :界面展示与交互行为。
  2. 业务逻辑(Domain Logic) :系统核心规则,例如"库存不足不能下单"。
  3. 数据访问逻辑(Data Access) :对数据库的读写操作。

常规架构中,业务逻辑通常由应用层处理:

复制代码
前端 → 应用服务(业务逻辑) → 数据库(存数据)

把业务逻辑写进数据库是指:

  • 把部分或全部业务规则直接写入数据库中,例如使用存储过程(Stored Procedure)触发器(Trigger)自定义函数(Function)约束(Constraint) 等特性。
  • 应用层只负责参数传递、UI 展示。

🏛 历史背景:IBM IMS 系统与"数据库中的业务逻辑"

把业务逻辑写进数据库的做法并非新生事物,其历史可追溯到 20 世纪 60 年代。1966 年,IBM 推出了 IMS(Information Management System) ,这是一套为美国阿波罗登月计划设计的数据库管理系统,其架构强调:

  • 数据和事务逻辑紧密结合:应用程序通过调用数据库事务来实现完整的业务操作
  • 逻辑写在数据库端:通过"程序化数据库"方式执行复杂流程
  • 高性能与一致性:IMS 以事务安全和高吞吐著称,在银行、电信等领域广泛使用

IMS 的成功,使"胖数据库"的理念深入人心。后来,随着关系型数据库和 PL/SQL 的出现,存储过程成为实现复杂业务逻辑的标准工具。

IMS 系统发明的 Trigger 和 Transaction 等一系列现代数据库的默认机制,被后人总结为 ACID 特性,直到今天,它也还是最值得信赖的数据保障机制。


🛠 架构演进:胖数据库的回归

在 90 年代至 2000 年代初期,很多系统采用"胖数据库、瘦应用"的架构:

  • 使用 SQL 和 PL/pgSQL 写出大量逻辑
  • 应用层作为 UI 或接入层存在

后来,后端语言和框架发展迅速,业务逻辑转移到了服务层,数据库变成了"纯存储层"。

但在某些系统中,数据库仍然承担着关键业务逻辑职责,尤其在:

  • 银行、证券、保险等高一致性系统
  • ERP、库存、订单系统
  • 多系统共享数据库的场景

✨ 为什么考虑把业务逻辑写进 PostgreSQL?

✅ 更强的事务一致性

  • 数据库内部逻辑天然支持事务(BEGIN/COMMIT/ROLLBACK)
  • 无需依赖应用框架或中间件控制一致性

✅ 性能更高

  • 减少前后端之间的往返调用
  • 可以在一次事务中完成校验+更新+插入

✅ 可复用、统一管理

  • 多个系统调用数据库中的存储过程,不用重复实现逻辑

✅ 数据完整性保障

  • 使用触发器/约束可强制规则执行,防止数据脏写

📊 PostgreSQL 实战示例:下订单流程

🔹 场景:用户下订单

需求如下:

  1. 检查库存是否足够
  2. 库存足够则创建订单并扣减库存
  3. 不足则报错拒绝
  4. 自动记录日志
  5. 保证所有步骤原子性

我们将使用 PostgreSQL 的以下功能实现:

  • FUNCTION + DO 块实现事务控制
  • TRIGGER 实现日志记录
  • CHECK 限制库存不能为负
  • FOREIGN KEY 确保用户和商品合法

🗃 表结构:

SQL 复制代码
-- 用户表,记录用户信息
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    name TEXT NOT NULL
);

-- 商品表,记录库存和商品信息
CREATE TABLE products (
    id SERIAL PRIMARY KEY,
    name TEXT NOT NULL,
    stock INT NOT NULL CHECK (stock >= 0)
);

-- 订单表,记录每笔用户下单信息
CREATE TABLE orders (
    id SERIAL PRIMARY KEY,
    user_id INT REFERENCES users(id),
    product_id INT REFERENCES products(id),
    quantity INT NOT NULL,
    created_at TIMESTAMP DEFAULT now()
);

-- 日志表,用于记录订单操作行为
CREATE TABLE order_log (
    id SERIAL PRIMARY KEY,
    order_id INT,
    action TEXT,
    log_time TIMESTAMP DEFAULT now()
);

⚙ 存储过程(函数)实现业务逻辑:

SQL 复制代码
-- place_order 函数:检查库存,创建订单,扣减库存
CREATE OR REPLACE FUNCTION place_order(
    p_user_id INT,
    p_product_id INT,
    p_quantity INT
) RETURNS VOID AS $$
DECLARE
    available_stock INT;
BEGIN
    -- 锁定该商品行,避免并发冲突
    SELECT stock INTO available_stock FROM products
    WHERE id = p_product_id FOR UPDATE;

    IF available_stock < p_quantity THEN
        RAISE EXCEPTION '库存不足';
    END IF;

    -- 扣减库存
    UPDATE products SET stock = stock - p_quantity WHERE id = p_product_id;

    -- 创建订单
    INSERT INTO orders(user_id, product_id, quantity)
    VALUES (p_user_id, p_product_id, p_quantity);

END;
$$ LANGUAGE plpgsql;

⚡ 自动记录日志的触发器:

sql 复制代码
-- 触发器函数:在每次订单插入后,自动写入日志表
CREATE OR REPLACE FUNCTION log_order_insert()
RETURNS TRIGGER AS $$
BEGIN
    INSERT INTO order_log(order_id, action)
    VALUES (NEW.id, '创建订单');
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

-- 创建触发器
CREATE TRIGGER trg_log_order
AFTER INSERT ON orders
FOR EACH ROW
EXECUTE FUNCTION log_order_insert();

📩 调用示例:

SQL 复制代码
-- 调用下单函数:用户1购买商品101的2件
SELECT place_order(1, 101, 2);

如果库存不足,将自动抛出异常并终止事务;否则,订单创建、库存更新和日志写入都在一个事务中完成。


✅ 与应用层逻辑的对比(以 Node.js 为例)

以"下订单"流程为例,下面是将业务逻辑写在 Node.js 应用层的常规写法:

📦 Node.js 中传统写法(Express + ORM 示例)

js 复制代码
// 下订单:应用层业务控制
app.post('/order', async (req, res) => {
  const { userId, productId, quantity } = req.body;

  const product = await db.products.findOne({ where: { id: productId } });
  if (product.stock < quantity) {
    return res.status(400).json({ error: '库存不足' });
  }

  await db.sequelize.transaction(async (t) => {
    await db.products.update({ stock: product.stock - quantity }, { where: { id: productId }, transaction: t });
    const order = await db.orders.create({ userId, productId, quantity }, { transaction: t });
    await db.order_log.create({ orderId: order.id, action: '创建订单' }, { transaction: t });
  });

  res.json({ success: true });
});

🆚 与数据库侧逻辑对比

  • 复杂度:Node.js 中需手动管理事务、依赖 ORM;而数据库侧只需调用一个函数即可
  • 一致性:数据库事务天然封装,应用层需开发者小心处理异常回滚
  • 复用性:Node.js 每个服务都需实现逻辑,数据库中只需调用同一函数
特性 写在数据库中 写在应用中(Node.js)
性能 🔼 减少往返 🔽 多次 SQL 请求
事务一致性 🔼 数据库内事务 🔽 ORM 或手动控制
可复用性 🔼 多端共享函数 🔽 每端都要实现
调试测试 🔽 较难 🔼 成熟工具
版本管理 🔼 Flyway 管理 .sql 脚本 🔼 Git、CI
微服务适配 🔽 拆分受限 🔼 模块化更好

✅ 结论:如果业务规则稳定、调用频繁、对一致性和性能要求极高,可以考虑写在数据库。

如果业务变动频繁、协作团队偏向前后端协作开发,写在 Node.js 等应用层会更灵活易维护。

🧰 补充一下 PostgreSQL 中触发器与函数基本用法

📌 自定义函数(Function)

  • 使用 CREATE FUNCTION 定义
  • 支持 RETURNS 返回值(VOID、INT、TABLE等)
  • 支持变量声明、条件判断、循环控制
  • 支持事务语义与异常处理
  • 示例:
SQL 复制代码
CREATE OR REPLACE FUNCTION add_numbers(a INT, b INT) RETURNS INT AS $$
BEGIN
    RETURN a + b;
END;
$$ LANGUAGE plpgsql;

📌 触发器(Trigger)

  • 使用 CREATE TRIGGER 声明
  • 触发时机:BEFORE / AFTER / INSTEAD OF
  • 支持 FOR EACH ROW(逐行)或 FOR EACH STATEMENT(一次)
  • 常与函数配合使用:触发某操作时自动调用函数
  • 示例:记录用户表插入行为
SQL 复制代码
CREATE OR REPLACE FUNCTION log_user_insert()
RETURNS TRIGGER AS $$
BEGIN
    INSERT INTO audit_log(table_name, action)
    VALUES ('users', 'insert');
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_user_insert
AFTER INSERT ON users
FOR EACH ROW
EXECUTE FUNCTION log_user_insert();

📆 版本控制与维护建议

  1. 所有数据库函数、触发器、初始数据放入 SQL 脚本(建议按模块拆分)
  2. 使用 Flyway、Sqitch 或 Liquibase 等工具进行数据库迁移管理
  3. 存储过程内避免过度业务嵌套逻辑,只保留核心流程判断
  4. 复杂规则使用 UDF 拆解,保持函数职责清晰
  5. 编写数据库层级单元测试(pgTAP、utPLSQL)

☑ 哪些场景适合把业务写进 PostgreSQL?

✅ 适合:

  • 高并发订单、库存等写密集型业务
  • 业务规则稳定但对一致性要求高(如财务、支付)
  • 多前端系统共用同一业务逻辑的场景(如后台、App、小程序)

❌ 不适合:

  • 多服务拆分(微服务)架构中跨库逻辑
  • 规则频繁变化、动态性高的业务(如策略系统)
  • 研发团队缺乏数据库编程经验的情况

🔄 总结

数据库不仅仅是存储器,它也可以是规则的守门人

通过 PostgreSQL 的强大特性(事务、触发器、函数、约束等),我们可以:

  • 将部分核心逻辑转移到数据库中
  • 提高性能与一致性
  • 降低重复实现和维护成本

这种思路适用于对数据一致性要求极高、规则变化不频繁的系统。虽然不是银弹,但在对的场景下,它是值得考虑的架构武器之一。

"数据库是被低估的计算平台。"

📎 附录:我们真的需要"大数据"和"微服务"吗?

在开发实践中,我们常常接触到"互联网标签"------大数据、微服务、分布式、高可用、弹性扩展、容器化、CI/CD、DAU 过亿......这些术语令人眼花缭乱,也容易让人误以为它们是每个项目的"标配"。

但现实往往不是这样:

  • 绝大多数开发者 一生中所参与的系统,DAU 不会超过十万,甚至连万级都难达到
  • 系统复杂度的瓶颈更多来自不清晰的逻辑、不一致的数据模型、不规范的协作流程,而不是流量或分布式架构。
  • 很多系统盲目引入 Kafka、Redis、微服务拆分、容器集群,最后反而增加了维护成本、降低了可靠性

因此,回归本质------用最简单的技术满足业务需要,用最接近数据的方式表达规则和保障一致性,是对工程效率和质量的最大尊重。

如果你的项目只是一个日活几千的后台管理系统、一个内网的流程平台、一个局域网的协作工具------那把逻辑写进 PostgreSQL,不仅够用,还可能是最优解。

技术选型的重点不在于潮流,而在于恰当。

相关推荐
程序员阿超的博客1 小时前
React动态渲染:如何用map循环渲染一个列表(List)
前端·react.js·前端框架
magic 2451 小时前
模拟 AJAX 提交 form 表单及请求头设置详解
前端·javascript·ajax
掘金-我是哪吒1 小时前
分布式微服务系统架构第145集:Jeskson文档-微服务分布式系统架构
分布式·微服务·云原生·架构·系统架构
你怎么知道我是队长2 小时前
GO语言---匿名函数
开发语言·后端·golang
小小小小宇5 小时前
前端 Service Worker
前端
只喜欢赚钱的棉花没有糖6 小时前
http的缓存问题
前端·javascript·http
小小小小宇6 小时前
请求竞态问题统一封装
前端
loriloy6 小时前
前端资源帖
前端
源码超级联盟6 小时前
display的block和inline-block有什么区别
前端
GISer_Jing6 小时前
前端构建工具(Webpack\Vite\esbuild\Rspack)拆包能力深度解析
前端·webpack·node.js