CQRS实践疑问:是否应在写模型中扩展非规范化读字段?
问题背景
我正在开发一款基于事件溯源(Event Sourcing)和CQRS原则的交易追踪应用。每个Transaction关联一个Asset,相关模型代码如下:
class Asset(models.Model): code = models.CharField(...) current_price = models.DecimalField(...) sector = models.CharField(...) ... def get_roi(self, percentage: bool = False) -> Decimal: # 涉及多表关联的高成本计算 return self.transactions.roi(incomes=self.incomes.sum(), percentage=percentage)["ROI"] class Transaction(models.Model): asset = models.ForeignKey(to=Asset, on_delete=models.CASCADE, related_name="transactions") ... class Income(models.Model): asset = models.ForeignKey(to=Asset, on_delete=models.CASCADE, related_name="incomes") ...
资产列表接口中有多个在规范化数据库中计算成本极高的字段,这些字段会在以下场景发生变化:
- 创建或删除
Transaction - 创建或删除
Income current_price变更
为了扩展性,我希望将这些高计算成本的字段与写模型分离,但读模型需要包含一些几乎不会变更的字段(如sector),用于生成按sector聚合ROI的报表。这意味着每次sector变更时也需要同步读模型,但这类字段的变更与Asset聚合领域的业务规则无关,这种同步方式让我觉得既奇怪又冗余。我考虑过直接在写模型中扩展这些非规范化字段,但这也存在问题。请问最优方案是什么?
最优方案建议
1. 读模型按需关联写模型的静态字段
让读模型只存储动态计算字段(如ROI、基于current_price的衍生值)和Asset的唯一标识(如asset_id)。当需要生成按sector聚合的报表时,在查询读模型的同时,通过asset_id关联写模型的sector字段。
- 优势:完全避免sector变更时的同步操作,写模型无需关心读模型的静态字段需求,严格遵循CQRS读写分离的核心原则。
- 注意事项:如果报表查询频率极高,关联查询可能带来性能损耗。可以在报表层添加缓存策略——比如按sector缓存聚合结果,仅当ROI或sector发生变更时更新缓存。
2. 将静态字段变更纳入事件流,用事件驱动同步读模型
给Asset领域新增一个AssetSectorUpdated事件,当sector字段变更时发布该事件,读模型的投影器监听此事件并更新自身的sector字段。
- 优势:保持读模型的独立性,所有读模型的更新都通过事件驱动,符合Event Sourcing的架构一致性。虽然sector变更和业务规则无关,但事件驱动的方式让整个系统的数据流更清晰,不会破坏领域边界。
- 注意事项:需要在写模型中添加sector变更的事件发布逻辑,属于少量额外开发,但逻辑可控。
3. 拆分读模型为动态聚合层与静态维度层
将读模型拆分为两个独立部分:
- 动态聚合层:存储每个Asset的ROI、current_price等动态计算值,仅监听Transaction/Income/Price变更事件。
- 静态维度层:存储Asset的sector、code等静态字段,仅在这些字段变更时更新。
报表查询时可以直接关联两层数据,或者构建物化视图定期合并两层数据,供报表快速查询。
- 优势:彻底分离动态和静态字段的更新逻辑,避免不必要的同步操作,同时满足报表的聚合需求。
- 注意事项:如果需要实时报表,需调整物化视图的刷新频率,或者采用实时关联查询的方式。
方案选择建议
如果报表的实时性要求不是极端严格,优先选择方案1,它最简洁且符合CQRS的设计初衷。如果报表查询性能要求极高,且sector变更频率极低,可以考虑方案2;若系统存在多种不同维度的报表需求,方案3的分层设计会更灵活。
内容的提问来源于stack exchange,提问作者Murilo Sitonio

