SQLite 触发器,听起来像是给数据库装了个“自动开关”,但真上手写过的人都知道,这东西比你想象中要简单,也比你预想中更容易翻车。SQLite 作为嵌入式数据库的代表,触发器在本地存储、移动端应用、工具型桌面软件里出镜率相当高,我最早接触它是为了给一个小型订单系统维护库存与流水,后来在 uniapp 的本地存储方案里也大量用到,触发器帮我在应用层少写了几百行重复逻辑。如果你正在用 SQLite 数据库做本地功能,或者正打算把一些数据校验、日志记录交给数据库自己去完成,那么这篇关于创建触发器与实操细节的记录,应该能帮你少踩不少坑。
一次事件触发一段逻辑,这听起来像是编程里的“事件监听”,但触发器是在数据库内部、由数据库引擎主动执行的,应用层完全不需要关心什么时候去调用。它适合谁?如果你是后端开发、移动端开发,或者在做桌面小工具,想在数据写入的同时自动维护统计、日志、冗余字段,就可以把触发器纳入技术方案。搜索引擎里“触发器”这个词经常会跟数字电路里的 D 触发器、JK 触发器混在一起,那是完全不同的领域,这里我们只聊 SQLite 数据库触发器。
1. 先搞懂SQLite触发器是什么:一个动作,自动接一段SQL
1.1 触发器的三要素与SQLite支持范围
触发器本质上就是“事件驱动的小脚本”。你在某张表上挂一段 SQL 逻辑,当这张表发生指定的数据变更动作时,SQLite 会自动执行这段逻辑。里面有三个核心要素:
- 时机(Timing):动作发生之前执行,还是动作发生之后执行。
- 事件(Event):到底是 INSERT、UPDATE 还是 DELETE 触发了它。
- 动作(Action):触发后要执行的 SQL 语句块。
SQLite 支持的时机有 BEFORE、AFTER、INSTEAD OF,事件有 INSERT、UPDATE、DELETE,其中 UPDATE 还可以精确到某一列,比如 UPDATE OF price 表示只有 price 字段被更新时才会触发。这三种时机加上三种事件,能组合出很多种用法,但最基础、最常用的还是 BEFORE INSERT、AFTER INSERT、BEFORE UPDATE、AFTER UPDATE 这几个组合。
有一点需要提前说清楚:SQLite 触发器里没有 MySQL 那种存储过程式的复杂流程控制,也没有函数库可以直接调用。它更像一段“受限但够用”的 SQL 脚本,你在里面可以继续写 INSERT、UPDATE、DELETE、SELECT,还可以通过 NEW.列名 和 OLD.列名 访问正在被修改的那一行数据。NEW 代表新插入或更新后的值,OLD 代表更新或删除前的值,这个设计非常直观。
1.2 为什么把逻辑放进数据库而不是写在业务代码里
很多初学者会问:这些逻辑我在应用层写不行吗?订单插入成功后,我在 Java/C++/JavaScript 里多写几行更新库存的代码,效果不是一样吗?
确实“效果”看起来一样,但有几个实际问题:
第一,应用层代码只在某个入口生效。如果你在 App 的一个页面里写了扣库存逻辑,另一个管理后台单独连数据库改了订单,或者有人直接打开 DB Browser 手工插入了一条订单记录,那应用层的逻辑根本不会执行,库存就会对不上。把逻辑下沉到数据库触发器里,无论数据是从哪个入口进来的,只要发生了 INSERT 或 UPDATE,规则就会被强制执行。
第二,触发器能保证原子性。在事务里,如果订单插入成功但后续库存更新失败,整个事务可以一起回滚。但如果是在应用层分两步写,一旦第二步失败且事务没有正确处理,就很容易出现“订单成功了,库存没扣”的数据错乱。
第三,减少客户端重复代码。比如你的项目有原生 App、小程序、管理后台好几套客户端,都往同一张 SQLite 表里写数据。如果把规则放在数据库层,只需要写一次触发器,所有端都自动遵守同一套业务规则。
注意,触发器不是万能的,过于复杂的业务逻辑塞进数据库会让排障变得困难,后面我会专门说哪些场景适合放,哪些不适合。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 动手前先设计表结构:库存与订单场景
2.1 三张表的设计思路
光讲语法太抽象,我拿一个实际做过的场景来演示。当时我要给一个轻量级的订单系统做本地存储,核心需求是:下单成功后自动扣减商品库存,同时记录一条库存变动日志。数据量不大,但要求逻辑必须稳定,不能因为少写一条日志导致后面盘点对不上。
我设计了三张表:
products:商品主表,保存商品名称、价格、库存、销量统计等字段。orders:订单明细表,每次下单写入一条记录,包含商品 ID、购买数量、成交单价。stock_log:库存变动日志表,记录每次库存的变化情况,包括旧库存、新库存、变动原因、变动时间。
其中日志表是触发器最理想的落点。为什么单独建一张日志表而不是在应用里打日志?因为数据库日志表可以被后续查询、对账、统计直接使用,而且能拿到事务内的准确旧值和新值,这是普通文本日志很难做到的。
2.2 建表SQL:字段和索引一起安排好
建表语句如下:
sql复制CREATE TABLE IF NOT EXISTS products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
price REAL NOT NULL DEFAULT 0,
stock INTEGER NOT NULL DEFAULT 0,
total_sold INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')),
updated_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime'))
);
CREATE TABLE IF NOT EXISTS orders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
product_id INTEGER NOT NULL,
quantity INTEGER NOT NULL DEFAULT 1,
unit_price REAL NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')),
FOREIGN KEY (product_id) REFERENCES products(id)
);
CREATE TABLE IF NOT EXISTS stock_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
product_id INTEGER NOT NULL,
old_stock INTEGER NOT NULL,
new_stock INTEGER NOT NULL,
change_amount INTEGER NOT NULL,
reason TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime'))
);
CREATE INDEX IF NOT EXISTS idx_orders_product_id ON orders(product_id);
CREATE INDEX IF NOT EXISTS idx_stock_log_product_id ON stock_log(product_id);
这里多说一句,updated_at 字段我特意留出来了,后面会用触发器自动维护。total_sold 看起来是冗余字段,因为通过 SUM(quantity) 也能算出来,但在查询量比较大的场景里,冗余字段可以避免每次统计都要扫全表。触发器在这里的价值就是保证冗余字段永远和明细数据保持一致。
从这张表能看到一个关键点:触发器不是孤立存在的,它的价值依赖于你提前设计好“谁是被动更新的表”和“谁是记录日志的表”。如果你连日志表都没有设计,那就只能写一个没有任何落点的触发器,作用会大打折扣。
