REA模型详解:从业务事件到数据库表结构的实战指南

发布时间:2026/10/11 14:00:26
REA模型详解:从业务事件到数据库表结构的实战指南
如果你在浏览器里敲下“rea”三个字母会得到很多完全不同的答案一个爱尔兰姓氏、某个能源组织的缩写、又或者是一份海外报告的编号。但在信息系统与业务建模圈子REA 只有一个默认解释——Resource-Event-Agent资源-事件-代理模型。我第一次认真研究它是因为一个供应链项目对账对到怀疑人生订单、库存、资金三条线各算各的补丁打了一个又一个数据还是交叉污染。后来参考 REA 的思路先把“建表”放下回到业务事件本身去重新画关系图问题才真正解开。这篇文章就当一次完整复盘聊聊 REA 到底怎么建模、怎么翻译成数据库表结构、又有哪些实操坑值得提前避开。适合正在做订单、库存、财务或供应链相关系统的开发、设计和需求分析人员慢慢读。1. REA模型到底是什么从三个单词拆出它的内核1.1 一个快递类比把Resource、Event、Agent一次性讲透REA 的名字已经把核心说完了Resource 是资源Event 是事件Agent 是代理。三个词放在一起意思是一个业务活动由某个代理执行事件事件让资源发生状态变化。你网上下单一本书商家仓库里那本书就是 Resource快递员把书发出、你签收这个“发货”和“签收”就是 Event商家、快递员、客户则是 Agent。如果用传统库存表只会看到“库存量从 10 变成 9”看不到为什么少了一本REA 则会记录一条完整链路客户下订单承诺仓库发货实际事件你收货付款另一个实际事件。整条链路上资源的变化都被事件串了起来。这带出了 REA 的核心思想业务数据不该只记当前余额更该记录“余额是怎么来的”。余额是结果事件是原因。系统里一旦把事件本身存下来想要库存净值、应收余额、采购在途数都可以从事件明细重新推演不需要为每个需求造一张冗余表。刚开始我确实不习惯传统表结构直接看到“现存量”很方便但后来发现业务规则只要调整传统余额表就得跟着改字段事件表却基本可以不动。REA 等于把“结果”和“过程”彻底分离了。1.2 REA和复式记账、普通业务表的本质区别做过财务系统的都熟悉复式记账收款时借银行贷应收讲究平衡。REA 并不想替代会计它只是在业务数据层更愿意描述事实。两者最大的差异在抽象层次会计凭证面向记账规则一笔复杂交易会被拆成借贷两行REA 面向业务过程一笔交易会被拆成多个相关事件语义更贴近业务人员表达。比如“销售一瓶水收 3 元现金”科目表里可能记“借现金 3贷主营业务收入 3”但这个过程丢掉了卖的是哪瓶水、卖给哪个客户、哪个销售员。REA 会把“出售商品”和“收取现金”当作两个事件商品资源流向客户现金资源流向企业两个事件通过销售订单关联客户和销售员作为 Agent 挂到对应事件上。这样一来想知道哪个客户贡献大、哪个商品毛利高都非常自然。和普通业务表比REA 也不是多加几张关联表而已。普通表设计经常把资源、事件、代理混在一张表里比如订单表既有商品名称数量又冗余了客户名称、销售员名称和仓库位置。好处是查询快坏处是更新不一致风险成倍增加。REA 强制你把“谁—干了什么—影响了什么”分开建模前期写 SQL 要多几步关联后期维护和扩展的收益远超开销。提示REA 不是银弹。如果系统只给单一角色做简单台账REA 会显得笨重。它真正适合的是数据被多个部门从不同视角使用、且业务链路有明确事件流转的系统。1.3 为什么遗留系统改造成本高REA却相对抗变化遗留系统里最常见的场景是库存表既有数量又顺手存了最近采购价和供应商 ID。供应商一换要同时更新主数据和库存表。更难受的是业务后来又要求支持“先下单后支付”原库存表没有地方记录“已承诺但未发货”数量于是只好加一张预订表两表频繁同步稍不留神就超卖。REA 模型处理这类变化很从容。库存数量不是单表字段而是通过入库事件总流入减出库事件总流出算出来。要支持“已承诺未发货”就在承诺事件里记录资源数量计算可卖库存时用“现有资源量减去已承诺但未交付的数量”推导即可完全不用改表结构。REA 把精力放在理清业务变量之间的因果链而不是急着给每个变量定死字段这是它扛变化的关键。2. 四步建模法把业务场景“翻译”成REA的语言2.1 第一步圈定资源别把余额和流水混在一起资源是企业里值得被追踪、且能被消耗或获得的东西。现金、存货、固定资产、工时都可以算但要注意资源是存量概念事件是流量概念。“应收账款余额”不是资源它是销售收款事件累计的结果“库存量”也不是资源本体商品才是资源库存量只是商品资源的状态统计值。我带团队做建模时第一步只给一张白纸让所有人写出业务里“看得见摸得着或能在账上体现价值”的东西。有人写了“销售订单”我提醒他订单在完成以前是承诺不是资源本身资源是订单上约定的商品和将来会流入的货币资金。如果上来就把订单、合同当资源后面的关系会越画越复杂。这步的原则很简单资源要对业务有真实价值且能在多个事件之间流转。边界定好REA 图就不会画成蜘蛛网。2.2 第二步识别事件从动词里找业务真相资源确定以后就看资源为什么会发生变化。方法是把业务流里的动词都找出来下单、发货、收货、验收、开票、收款、付款、退单、领用、入库。这些动词就是候选事件。但并不是所有动词都要建事件。“审批”更像控制动作不直接让资源转移如果把审批也做成事件事件类型会被噪音塞满。REA 建模里常用一个判断标准事件要能回答“改变了什么资源”和“谁参与”。审批回答不了资源变化那就作为状态字段挂到某个事件上而不是单独建事件表。下单则特殊一些。它本身不改变商品资源但它创建了对未来资源转移的承诺。所以在 REA 的扩展流派里承诺Commitment也是一个重要元素。我把事件分成实际事件和承诺事件实际事件是真实转移比如发货、收款承诺事件是双方约定未来会发生转移比如订单签订。这个区分能解释很多业务问题客户下单后仓库不能随便动库存因为一部分库存已经被承诺占用了。大量超卖问题就是系统没把承诺事件和发货事件分开导致的结果。2.3 第三步绑定代理明确每个事件里的角色代理是参与事件的人和单位。一个事件通常至少有两个代理一个是内部代理一个是外部代理即使是内部调拨也有出库方和入库方。“发货”事件的内部代理是仓库操作员外部代理是承运商或客户“收款”事件的内部代理是财务人员外部代理是客户。代理挂全后续做绩效分析会很省力想分析哪个销售员毛利贡献高按销售员分组关联销售事件和成本事件计算即可。在表设计上客户、供应商、员工、仓库管理员都可以视为代理的子类。底层建议用一张统一的 agent 表再加 agent_type 区分而不是一上来就建客户表、供应商表、员工表三套。否则后面想加一个经销商角色就要改关联结构成本很高。保留统一代理表再加业务视图做查询既灵活又不失去可读性。2.4 第四步给元素画关系用转换/交换/因果三类关系连接REA 图最后要回答资源、事件、代理之间靠什么关系连起来三类关系必须记牢资源与事件之间是存量-流动关系一个事件会流入或流出资源。事件与事件之间是交换或转换关系比如销售事件触发收款事件采购付款事件形成闭环。事件与代理之间是参与关系代理负责或受益于事件。画一个典型销售流程销售订单承诺触发发货实际事件发货事件流出商品资源同时触发收款事件收款事件流入现金资源客户参与订单和收款销售员参与订单。把这些连线画出来逻辑会非常清楚。我常用白板先画一次再用项目组熟悉的建模工具存成标准图。这个图就是数据库设计的直接依据别跳步。3. 实操落地把REA模型翻译成数据库表结构3.1 通用表结构设计原则先把字典表定下来用 REA 建模之后得到的不是一两张大业务表而是一组语义清晰的基础表。落地时我会分五类资源表、代理表、事件表、承诺表、关联明细表。资源表存商品、资金账户等主数据代理表存客户、供应商、员工事件表存每一条实际业务动作承诺表存订单、合同这类“将来要发生”的记录关联明细表负责多对多明细。这些表之间会大量使用外键所以建表前最好先把字典表建好事件类型、资源类型、代理类型、币种、计量单位。没有字典表的 REA 项目常常会因为“销售出库”和“销售发货”两种叫法不一致而导致统计对不上。我习惯把字典字段先定义为 varchar 编码再配一个中文名称解释表便于业务方共同维护。3.2 一个可复用的SQL建表样例从采购到付款用最简的采购付款场景演示。某公司向供应商采购商品收货后过段时间付款。按照 REA资源有商品和现金代理有供应商、采购员、财务事件有下采购订单承诺、收货实际、付款实际。下面是骨架 SQL直接能跑也能继续扩展。-- 代理表 CREATE TABLE agent ( id BIGINT PRIMARY KEY, agent_type VARCHAR(20), -- CUSTOMER / SUPPLIER / EMPLOYEE name VARCHAR(100) ); -- 资源表 CREATE TABLE resource ( id BIGINT PRIMARY KEY, resource_type VARCHAR(20), -- GOODS / CASH name VARCHAR(100), unit VARCHAR(20) ); -- 承诺表订单/合同 CREATE TABLE commitment ( id BIGINT PRIMARY KEY, commitment_type VARCHAR(20), -- PURCHASE_ORDER / SALES_ORDER external_agent_id BIGINT REFERENCES agent(id), internal_agent_id BIGINT REFERENCES agent(id), committed_time TIMESTAMP ); -- 承诺明细 CREATE TABLE commitment_line ( id BIGINT PRIMARY KEY, commitment_id BIGINT REFERENCES commitment(id), resource_id BIGINT REFERENCES resource(id), quantity DECIMAL(18,4), unit_price DECIMAL(18,4), currency VARCHAR(20) ); -- 实际事件表 CREATE TABLE business_event ( id BIGINT PRIMARY KEY, event_type VARCHAR(20), -- RECEIPT / PAYMENT / DESPATCH event_time TIMESTAMP, external_agent_id BIGINT REFERENCES agent(id), internal_agent_id BIGINT REFERENCES agent(id), source_commitment_id BIGINT REFERENCES commitment(id) ); -- 资源流向事件明细记录事件对资源的影响 CREATE TABLE resource_event_line ( id BIGINT PRIMARY KEY, event_id BIGINT REFERENCES business_event(id), resource_id BIGINT REFERENCES resource(id), direction VARCHAR(10), -- IN / OUT quantity DECIMAL(18,4), amount DECIMAL(18,4) );这段结构的关键点是事件和资源通过 resource_event_line 关联事件和代理通过 business_event 外键关联订单承诺也是独立表。如果想表达“这次收货执行的是哪张订单”在 business_event 里用 source_commitment_id 指向 commitment 表即可。有了这几张表库存当前量怎么查对商品 A把入库事件流入数量总和减去出库事件流出数量总和。应收余额怎么查对某客户把销售发货对应的应收金额减去已收款金额。所有衡量指标都能通过 SQL 聚合算出来不需要额外维护汇总表。这个思路和传统在业务表里直接存余额的设计差异很大也是很多财务系统对不上的根源。3.3 订单、发货、收款三张表怎么连起来不出错把上面的骨架落到销售场景最容易出错的是关联方向。销售链路大体是销售订单承诺→ 销售发货实际事件→ 应收确认实际事件→ 收款实际事件。如果只做一张订单表和一张流水表关联通常只有一个订单号REA 里要连续用两种关联一是承诺事件触发实际事件二是实际事件与实际事件之间互相交换。我习惯在 business_event 上保留两个基础字段source_commitment_id 表示触发本次事件的那条承诺related_event_id 表示和本次事件交换的另一个事件。比如发货事件里 source_commitment_id 指向销售订单之后触发收款事件收款事件里 related_event_id 指向那条发货事件。这样追踪资金和货物流转时一条 SQL 就能从收款跳到发货再跳到订单不用靠时间戳硬凑。实际操作中我不推荐在事件表里放太多描述性字段。备注一类信息可以单独建 event_remark 子表。不然再标准的三范式设计也会因为几个开发顺手加备注字段而渐渐腐烂。3.4 期初数据和财务系统衔接的处理技巧改 REA 最常见的疑问老系统已经有库存余额和应收余额历史事件补录不完怎么办我的做法是做一个期初快照事件。把切换时点的库存、资金、应收应付余额统一作为一条“初始化”事件方向为入库或单独建期初资源余额表。这样 REA 事件流是连续的查询当前量时把期初快照和其他事件聚合即可。和财务总账衔接时REA 事件表不是直接替代会计凭证。更稳妥的做法是 REA 负责业务过程数据财务模块通过一个“对账引擎”把业务事件翻译成借贷分录。翻译规则不复杂发货事件结转成本、确认收入收款事件减少应收。但有个坑事件时间是业务发生时间会计入账时间可能晚两个工作日。REA 不会修改业务事实所以我在事件表里额外加一个 accounting_time 字段专门给财务抽取用。新旧系统并行期间大多数对账差异都是这个时间差造成的。4. 常见问题与排查技巧实录4.1 商品到底算主数据还是资源建模时最纠结的问题初学者经常在主数据和资源之间反复横跳。我的理解是商品主数据表和 REA 资源表不冲突。主数据负责描述规格、条码、状态是静态档案REA 资源表追踪的是“在业务中流动的对象实例”。如果是多批次、多效期管理还要引入库存批次表让每个批次对应一个资源 ID。主数据表与资源表分开用外键关联是稳妥方案。4.2 多币种多单位怎么建模企业做跨境后币种就躲不开。REA 不建议把金额简单写成 DECIMAL 就完事。资源如果是现金账户币种要有事件发生时的汇率和记账汇率也要分开记录。我在 resource_event_line 里同时存 quantity、amount_occurred业务发生原币金额、exchange_rate、amount_reporting本位币折算金额。报表用哪个金额取决于需求但事件本身的原始事实不能丢。很多汇率差异出现在重估场景单独开一张重估事件表也比直接改原事件更清晰。4.3 事件表膨胀到几千万行会不会跑不动会但这不代表 REA 设计失败而是需要分层和归档。我会把数据分为在线区、历史区、统计区。在线区保留最近 12~18 个月原始事件做事务和近实时查询历史区按月分区或归档用 ETL 同步到数仓统计区预先算好在途、库存、应收等常用指标。REA 对象模型在线区是必要的历史区可以适度反范式化把 join 结果物化成宽表。性能优化的核心点有几个事件表主键用自增或雪花 ID索引覆盖 external_agent_id、event_time、event_type 联合索引resource_event_line 必须以 event_id 开头建复合索引不要在事件表上频繁执行 count(*) 统计事件总量可以维护一个事件流水号序列做近似计数。4.4 并发操作同一资源REA怎么配合REA 不直接解决并发控制但事件模型让并发控制更容易。因为资源当前量是可推导的可以在资源表上维护乐观锁版本号。用户操作资源 A 时先读 version生成事件时检查 version 是否变化变化就说明有并发出库需要提示刷新重试。这样比直接 update 库存字段冲突少很多。另一个技巧是把“承诺事件占用资源”和“出库事件确认资源”分两步。库存预留不要在发货时才扣减而是在承诺创建时先预占发货时确认。很多超卖问题都是因为库存扣减只发生在发货那一刻高并发下只靠数据库锁来扛非常吃力。4.5 团队协作建模文档画到什么程度代码写多了模型图容易被忽略。REA 图只需要画到事件、资源、代理、关系这个粒度具体字段放进数据字典。我会给每个事件加一句话业务说明比如“收货事件采购订单到达仓库后仓库人员核验数量并入库”。图中线条不要超过 20 条太多说明这个域建模还是太粗。评审时让业务方看动词对不对让开发看外键关系能不能走通双方达成闭环后再建表返工成本才能降到最低。5. 关于REA我最后想讲的几句实在话第一句不要为了 REA 而 REA。如果业务就是简单进销存团队又已经跑得很顺强行改成事件模型会让团队有一段时间不适应。REA 真正的收益在业务复杂度上升之后体现所以选择一个子域先试点是最好路径。我见过一上来就重构整个订单、库存、财务、采购四大域的项目最终全部退回就是因为一次动的面太大。第二句业务方必须在建模现场。事件识别特别依赖业务方的表达他们说的“下单”到底算承诺还是实际事件只有业务方和系统设计者一起确认才不会跑偏。不要让开发闷头看文档猜模型一通宵画出来的图大概率会被退货。第三句事件只负责记录事实业务规则放在校验层。REA 模型会告诉你“发生了什么”但不会告诉你“这件事允不允许发生”。比如订单取消之后不应发货这类状态校验必须在应用服务层控制。我踩过的最典型的坑就是发货事件已经生成但源头订单早已被取消少了前置状态机校验。事件表记下了不该发生的事实只能靠额外补偿事件去修正。REA 给了模型但模型永远替代不了业务规则。