我第一次认真研究REA,是在给一家小型贸易公司做进销存系统数据重构的时候。当时最头疼的不是写不好某个SQL,而是整个库跑了两三年,库存对不上、资金流水对不上,业务部门说系统难用,老板说报表不可信。翻来覆去找原因,最后发现问题不在某一张表,而在我们一开始建模的时候就没有把“业务到底发生了什么”这件事想清楚。后来接触到REA这个建模框架,很多困扰我的问题一下子顺了。
如果你也在做业务系统建模、领域模型设计、财务系统重构,或者负责数据中台里业务口径的统一,这篇东西值得花十分钟看完。它不会教你某个具体框架的API怎么调,但会给你一套能在任何技术栈里落地的建模方法论。REA全称Resources-Events-Agents,翻译过来就是资源—事件—主体,它最早是会计信息系统领域提出的一套通用业务建模框架。与其说是建模方法,不如说它是一种观察业务的方式:先搞清楚企业拥有的资源、发生的经济事件、参与的主体,再把它们之间的关系理清楚。这套思路可以用来设计数据库表结构,也可以用于理解现有业务逻辑的缺陷。
1. 项目概述:REA解决的不只是技术问题
1.1 用一句话说清楚REA
REA的核心主张其实非常朴素:传统复式记账把业务记录成“借”和“贷”两边,借贷必相等,这保证了对账一致,却牺牲了业务语义。你在数据库里看到一笔“借:库存商品 100,贷:银行存款 100”,如果不回看业务单据,根本不知道这笔采购是哪个供应商、谁经手的、对应哪个订单、仓库收到了还是没收到。REA不关注借贷,它直接记录“哪种资源发生了怎样的变化,谁参与了这个变化”。
这样一改,数据表里每一行都是一个可以追溯到真实业务事件的记录。库存增减不再是孤立的数字变化,而是与采购订单、收货确认、付款安排、销售人员、供应商等实体建立明确关联。对做系统的人来说,这意味着以后做报表、做对账、做业务分析,不用再从散落的表里靠JOIN猜关系,数据天然带着业务语义。
1.2 哪些人需要重点了解它
我观察下来,最需要REA的不是刚学数据库的新手,而是以下几类人:
- 做ERP或供应链系统的开发人员:天天和库存、订单、应收应付打交道,常规设计总是“别人怎么建表我也怎么建”,但团队里没人说得出为什么这样建。
- 做财务信息化或业财一体化的实施人员:业务系统说“我出库了”,财务系统说“我结转成本了”,两边数字对不上,本质上是两边对同一事件的建模方式不一致。
- 搬数据中台,做指标口径统一的数据工程师:订单金额、销售收入、回款金额,这些指标在不同部门有不同的定义。想统一口径,首先要统一对核心业务事件的认知。
- 刚入门领域建模,想找一套“不是拍脑袋”的分析框架的产品经理或架构师。
如果你只是写写简单的CRUD,屁股后面不需要这套东西。但只要你做的系统要支撑真实的业务运转、并且指望数据能越用越值钱,REA提供的这套视角就能帮你少走很多弯路。
2. 核心结构拆解:资源、事件、主体各自怎么理解
2.1 资源:企业拥有或控制的、具有经济价值的东西
资源在REA里不是“数据库里的资源表”,而是更接近经济学意义上的概念——凡是能给企业带来未来经济利益的对象,都可以看作资源。库存商品是资源,现金是资源,设备是资源,应收账款在法律意义上也可以视作一种资源。
判断一样东西算不算资源,有个很实用的检验方法:看它是否会在业务过程中被消耗、转化、交换或产出。原材料被消耗变成产成品,产成品被交换变成现金或应收账款——这就是资源在两个状态间流动。如果你建模时发现某个实体既不参与交换也不参与转化,它大概率不是核心资源,而只是某次事件的辅助属性。
这里要特别注意:REA关注的是经济资源(economic resources),像订单编号、备注、审批意见这类信息不是经济资源,它们是对事件的描述性信息。我刚接触时总想把所有字段都挂到某种“资源”上去,结果把模型弄得非常笨重。记住这一点能省很多纠结。
2.2 事件:整个建模里最该花心思的地方
事件(Event)代表企业中发生的、导致资源增减或变化的行为。采购订单的确认、仓库的入库、出库、向客户开票、收到客户付款、向供应商付款……这些全是事件。
REA对事件的处理有个值得反复琢磨的分割原则:增资源和减资源要拆成不同的事件记录。传统库存表通常是一张“进出明细”,用正负号区分入库还是出库。REA则要求你明确区分“将资源转化为己方可用”的事件和“将资源提供出去或消耗掉”的事件。以采购入库为例,“收货入库”是增加可用库存的事件,“验收入库”如果是入库的前提条件,那它可以是不同的事件;同理,“销售出库”是减少资源的事件,而“收取货款”是另一个增加现金资源的事件。
为什么要拆?因为可追溯性。如果一张表里正负号随便填,业务人员可以改数字掩盖问题;如果你把“收到实物”和“确认入账”做成两个事件,并且由不同岗位负责,任何一笔库存异常都能定位到具体环节。做数据系统的人,最怕的就是“日志齐全但责任模糊”,事件拆分就是在数据层面建立责任边界。
事件和时间的关系也值得注意:每个事件必须要有时间属性。事件时间不是你录入系统的时间,而是业务实际发生的时间。这个区别在月底对账的时候尤其重要——系统生成时间可以迁移,业务时间不可更改。哪条数据该算当月,应该以业务时间为准。
2.3 主体:谁发起了这个事件
主体(Agent)简单说就是“参与事件的人或组织”。内部主体比如采购员、库管员、销售员,外部主体比如供应商、客户、物流公司。一个事件可以有多方主体参与。采购入库这个事件,内部是库管员在操作,外部是供应商在送货;销售出库事件,通常是销售员发起、客户接收。
主体建模时我建议遵循一个“内外剥离”的习惯:内部主体尽量落成员工或岗位表,外部主体按往来单位维护,最好统一到一个“参与方”维表。因为同一个外部单位可能既是供应商又是客户,比如一个包装材料商,你既向它采购包装盒,又把尾料卖给它。如果供应商和客户分开建两张表,遇到这种双向往来的单位就得做冗余匹配,容易产生脏数据。统一到一张参与方维表,用角色表去描述它在特定业务中的身份,后续扩展起来会更从容。
2.4 三个要素组合成核心命题
把资源、事件、主体放在一起,REA的建模思路就很直白了:
- 一个事件至少涉及一个主体——事件不可能凭空发生;
- 一个事件至少涉及一个资源——没有资源增减的“事件”在REA里没有建模价值(纯粹的审批节点不算经济事件);
- 一个资源的增加或减少,必然由某个事件引发。
掌握了这三点,看到任何业务描述都能很快拆出实体来。比如“业务员跟客户签了个合同”,这个描述里业务员是主体,客户是主体,合同如果只是约束双方权利义务的文书、不直接导致资源增减,那它更接近事件的触发条件而不算是另一个经济事件。“签合同”这个动作可以被建模为“销售订单确认”事件的前置文档型实体,而真正值得记进REA模型的是订单确认后的出库事件、开票事件和收款事件。
3. 实操过程:拿一个销售业务完整走一遍REA建模
3.1 第一步:画出业务流转地图
开始建模前,不要急着写字段,先把业务过程从“发起”到“终止”过一遍。我习惯用一段大白话描述完整流程,同时标出资源的每一次增减。
以一家做成品销售的公司为例,完整销售循环大概是这样:业务员和客户谈好价格和数量,录销售订单;仓库根据订单拣货、出库并发货;财务按出库记录向客户开具销售发票;客户按发票期限付款到公司账户;财务确认收款并核销该笔应收。若客户退货,则反向发生退货入库和退款。
这段描述已经能看到多个资源和多个事件。资源至少包括:库存商品、应收账款(或现金)、银行存款;事件至少包括:销售出库(减少库存商品)、开票(将应收权利从无到有地确立)、收款(减少应收账款、增加银行存款)、退货入库(增加库存商品、反向的销售事件)。
3.2 第二步:识别核心事件并分清增与减
把事件列出来之后,要再检查一遍:每个事件是否连接了资源增减,是否连接了至少一个主体。销售出库连接的是库存商品的减少,参与主体是库管员和客户;销售开票连接的是应收账款的确立,参与主体是财务人员和客户;收款连接的是应收账款的消除和银行存款的增加,参与主体是财务人员和客户。
在这个步骤里我发现一个常见误区:很多人会把“订单审批”也当成一个事件。但审批本身不改变任何资源状态,它只是给后续经济事件放行。REA模型允许把审批作为事件的关联信息存下来,但不建议给它单独建“经济事件表”。否则你会得到一堆没有资源增减的“空事件”,模型看起来复杂,实则信息密度很低。
3.3 第三步:补充资源流向和配对关系
REA有一个比传统建模多出来的关键动作:把“资源的减少”和“资源的增加”配对。销售出库导致了库存商品减少,同时应收账款被确立了一笔增加;后续收款让应收账款减少,银行存款增加。这样一对对看下来,“钱货两清”本质上就是资源增减链条的完整闭环。
做一个简单的配对表,可以长这样:
| 业务环节 | 资源减少 | 资源增加 | 主要事件 | 参与主体 |
|---|---|---|---|---|
| 销售出库 | 库存商品 | 应收账款 | 销售出库 | 库管员、客户 |
| 开具发票 | —(单据动作) | 应收账款确认依据 | 销售开票 | 财务人员、客户 |
| 客户付款 | 应收账款 | 银行存款 | 收款确认 | 财务人员、客户 |
| 退货入库 | 应收退货权 | 库存商品 | 退货入库 | 质检员、客户 |
配好对之后,你很容易发现业务链条上的“断裂点”。比如这个公司销售出库后迟迟不开票,那“库存减少”和“应收确立”之间就出现了一个时间窗。有些企业为了调报表故意把时间窗拉得很大,从建模角度这属于异常状态,系统应该能及时预警。REA的配对思想天然支持这一点,它能从数据结构上支撑财务内控,而不只靠制度约束。
3.4 第四步:把模型落到数据库实体上
有了上面整理的元素,物理表结构基本呼之欲出。我的常用做法是四类实体表基架:
核心事件表记录每次业务动作,通过外键引用资源和参与方。事件表的核心字段应包含:事件编号、事件类型、业务时间、系统时间、参与方ID、经手人ID,以及用于关联资源流水的外部键组。不建议把金额字段直接放到事件表里当唯一事实,因为金额可能是基于多行明细汇总出来的结果,一旦后续发生部分退货,金额要跨多张表联动修正,独立成事件明细行会更灵活。
资源流水表记录每个事件对资源的增减影响,字段包括:资源ID、事件ID、增减方向、数量、金额、关联订单号。这张表实际上是所有“进出存”报表的事实来源。有了它,库存余额可以实时汇总,也可以按事件回溯每笔流水。
资源状态表描述资源当前的存量或剩余价值,比如某批次商品还在哪个仓库、可用数量多少、冻结多少。这个表不存流水,只存状态。每次事件提交时联动更新它。
主体维表固定维护内外部参与方,字段包括:主体ID、类型(员工/供应商/客户/物流方)、名称、税号、联系方式等。若一个主体承担多个角色,增加主体角色表按时期记录角色关系即可。
SQL层面,核心事件表和资源流水表大概长这样:
-- 核心事件表 CREATE TABLE core_event ( event_id BIGINT PRIMARY KEY, event_type VARCHAR(40) NOT NULL, -- sale_issue, sale_invoice, cash_receipt... business_time DATETIME NOT NULL, created_time DATETIME DEFAULT CURRENT_TIMESTAMP, participant_id BIGINT NOT NULL, -- 外部主体 internal_agent_id BIGINT NOT NULL, -- 内部经手人 remark VARCHAR(500) ); -- 资源流水表 CREATE TABLE resource_flow ( flow_id BIGINT PRIMARY KEY, event_id BIGINT NOT NULL, resource_id BIGINT NOT NULL, direction TINYINT NOT NULL, -- 1=增加, -1=减少 quantity DECIMAL(18,4) NOT NULL, amount DECIMAL(18,2) NOT NULL, reference_no VARCHAR(64), -- 订单号/发票号等 CONSTRAINT fk_flow_event FOREIGN KEY (event_id) REFERENCES core_event(event_id) );落表之后,别急着写业务逻辑,先用一个简单查询验证模型的合理性:查某月销售出库事件对应的库存减少总金额和销售开票事件对应的应收增加总金额,两者与业务报表比对。多数情况下对不上,原因基本都是某几张表的单据被事后修改而没有留下事件记录。这时候就体现出“事件不可变、状态可变”的好处了——业务修改只能通过新事件叠加,不能回头改旧事件。
4. 常见问题与排查技巧:REA建模中避不开的几个坑
4.1 这个对象到底是资源还是事件
这是我在实践中被问得最多的问题。有一个客户企业的积分商城系统,积分本身算不算资源?如果是,积分的发放和消耗算事件;如果算,那么积分兑换出去的优惠券又算什么?
我自己的判断方法是:看它是否具备“可以被交换、转化、消耗”的属性,并且这种属性是否会产生经济后果。积分可以消耗、可以换取商品,它有经济后果(公司未来要履约),所以积分是资源。优惠券本质上是兑换权,也是资源,但在实际建模中可以把它的生命周期折叠进“兑换事件”和“履约事件”,不一定需要单独建资源表——除非企业要精细核算优惠券的递延成本。
反过来,客户叫“VIP”也好、叫“黑金”也好,如果这种身份不随交易被消耗、也不能在交换中转化,那它不是REA里的资源,而是主体的属性,挂在主体维表上就够了。
4.2 一张表全量建模还是分多个子域建模
小系统可以一张大表走到黑,但企业级系统硬套一个大REA全局模型,往往会把模型膨胀到没人能维护。我的经验是把整个系统拆成几个边界清晰的循环:采购循环、销售循环、库存循环、资金循环。每个循环内部用REA建模,循环之间通过“资源交接”对接。
销售循环结束时,应收账款这个资源产生了;资金循环开始,收款的职责转给结算系统。两个循环共享同一个资源实体(应收账款),但事件表各自独立。如果把这所有东西揉到一个大模型里,事件类型可能有上百种,建表字段会有大量空值。分而治之,每个子循环都能保持简洁,边界处的资源实体保证了全局一致性。
4.3 怎么应对已有系统的改造
很多团队不是从零开发,而是要改造一个跑了好几年的老系统。老系统的核心问题通常是“流水表”承担了太多职责:既有事件属性、又有资源变化、还有参与方冗余,一个流水行对应多种业务含义。
改造我建议分层推进。第一步不是改表,而是在现有表基础上做视图层映射:把老流水表的典型字段映射为“资源+事件+主体”三要素。第二步在老业务前端搭配一个记录“业务时间+事件类型+参与方”的旁路日志,只更新不改。第三步等旁路日志覆盖一段时间、数据校验通过后,再真正迁移到新REA结构。这个过程快则两三个月、慢则半年,但比起推倒重来要稳得多。
4.4 典型错误汇总
我把过去踩过的和一些同事踩过的典型错误整理成一个速查表:
| 常见错误 | 说明 | 建议 |
|---|---|---|
| 把“状态”当成事件 | 比如把订单状态从“待发货”改成“已发货”直接写UPDATE,不生成事件 | 状态变更必须由事件驱动,旧状态保留在历史事件链里 |
| 把金额冗余到多张表 | 订单表写金额、应收表也写金额、流水表再写金额,数据一改就到处对不上 | 只在一处“事实表”保留最终事实,其余靠明细关联计算 |
| 事件和时刻混为一谈 | 用“操作时间”替代“业务时间”,月底统计数据经常漂移 | 业务时间独立校验,不允许前端随意修改 |
| 内部外部主体不区分 | 同一家单位在供应商表和客户表各存一份,后续合并困难 | 统一参与方维表,角色单独管理 |
| 忽略“逆事件” | 退货、退款、红冲没有建模成反向事件,而是直接删除原流水 | 逆事件保留完整追溯链,原始事件不可删除,逆事件引用原事件编号 |
这些错误在传统建模里很常见,因为它们不会让系统立刻报错,只会在几个月后的对账、审计、报表汇总中集中爆发。REA的作用不是“不允许你犯错”,而是通过结构设计,让你犯错的空间变小。
5. 实操心得与扩展思路
整套REA梳理下来,我最想强调的不是某个具体表结构,而是“先理清业务,再写字段”的工作习惯。很多团队做数据建模,上来就给业务方提了一堆字段清单让对方填,填回来就跑表结构,跑完才发现南辕北辙。如果你也遇到这种问题,可以试试先组织业务人员、财务人员和开发坐在一起,用半天时间只做一件事:模拟十笔真实业务,从下单到收款走一遍,把每一步涉及的资源增减和参与方写在白板上。做完这一步,表结构基本自己就出来了。
REA后续还能往两个方向扩展。一个是往前延伸到业务事件溯源架构(Event Sourcing)——事件表天然就是不可变日志,配合同一个事件ID去回放业务状态,这在交易类系统里非常有价值。另一个是往上延伸到指标口径管理——既然每个指标都能拆解到某个资源增减事件,那“收入”、“回款”、“净库存”的定义就有了共同的数据底座,做数据中台时不容易各说各话。
最后再说一个小技巧:给事件类型命名时,哪怕现有系统只跑一种业务,也尽量设计成带分类的编码(例如前缀是按业务域,后缀是动作。将来要加新事件类型只做增量,不用重命名)。我见过不少系统上线两年后事件类型命名规范已经五花八门,回看源头基本都是第一个开发图省事没留扩展位。REA给了我们一个相对稳固的框架,剩下就要靠建模时那些不起眼的小习惯,把框架落成长久能用的真东西。