Node.js事件溯源应用:聚合根缓存与值对象还原方案探讨
解决NestJS CQRS事件溯源中聚合根缓存的値对象还原问题
针对你用cache-manager+Redis缓存聚合根时,class-transformer无法正确还原値对象的问题,给你几个可行的落地方案:
方案1:给値对象补充class-transformer装饰器
class-transformer默认只会处理简单类型,自定义値对象需要显式配置类型映射和字段暴露:
// 示例値对象:Money import { Expose, Type } from 'class-transformer'; export class Money { @Expose() amount: number; @Expose() currency: string; constructor(amount: number, currency: string) { this.amount = amount; this.currency = currency; } public isPositive(): boolean { return this.amount > 0; } } // 聚合根示例 import { AggregateRoot } from '@nestjs/cqrs'; import { Expose, Type } from 'class-transformer'; export class Order extends AggregateRoot { @Expose() id: string; @Expose() @Type(() => Money) // 明确指定値对象类型 total: Money; constructor(id: string, total: Money) { super(); this.id = id; this.total = total; } } // 序列化/反序列化时指定选项 import { instanceToPlain, plainToInstance } from 'class-transformer'; // 存缓存 const order = new Order('1', new Money(100, 'USD')); const plainOrder = instanceToPlain(order, { excludeExtraneousValues: true }); await cacheManager.set(`order:${order.id}`, plainOrder); // 取缓存 const cachedPlain = await cacheManager.get(`order:${order.id}`); const restoredOrder = plainToInstance(Order, cachedPlain, { enableImplicitConversion: true });
核心是给値对象的字段加@Expose,聚合根中引用値对象的字段加@Type(() => 你的値对象类),确保反序列化时能正确识别类型。
方案2:自定义序列化/反序列化方法
放弃class-transformer,给聚合根和値对象手动实现序列化逻辑,完全控制结构:
// 値对象Money export class Money { constructor(public amount: number, public currency: string) {} // 自定义序列化 toJSON(): Record<string, any> { return { amount: this.amount, currency: this.currency, __type: 'Money' // 标记类型,反序列化时识别 }; } // 自定义反序列化 static fromJSON(data: Record<string, any>): Money { return new Money(data.amount, data.currency); } } // 聚合根Order export class Order extends AggregateRoot { constructor(public id: string, public total: Money) { super(); } toJSON(): Record<string, any> { return { id: this.id, total: this.total.toJSON(), __type: 'Order' }; } static fromJSON(data: Record<string, any>): Order { return new Order(data.id, Money.fromJSON(data.total)); } } // 存缓存 const order = new Order('1', new Money(100, 'USD')); await cacheManager.set(`order:${order.id}`, order.toJSON()); // 取缓存 const cachedData = await cacheManager.get(`order:${order.id}`); const restoredOrder = Order.fromJSON(cachedData);
这种方式完全自己控制序列化结构,不会有class-transformer的隐式问题,适合复杂値对象场景。
方案3:给cache-manager配置自定义序列化器
直接在cache-manager的Redis配置中替换默认序列化逻辑,处理値对象:
import { CacheModule } from '@nestjs/common'; import * as redisStore from 'cache-manager-redis-store'; @Module({ imports: [ CacheModule.register({ store: redisStore, host: 'localhost', port: 6379, // 自定义序列化器 serializer: { serialize: (value: any) => { if (value instanceof AggregateRoot) { return JSON.stringify(value.toJSON()); // 复用方案2的toJSON方法 } return JSON.stringify(value); }, deserialize: (value: string) => { const data = JSON.parse(value); if (data.__type === 'Order') { return Order.fromJSON(data); } return data; } } }) ] }) export class AppModule {}
这种方式把序列化逻辑统一放到缓存配置里,业务代码不用重复处理。
方案4:缓存事件快照(更贴合事件溯源架构)
事件溯源里更推荐的缓存方式是快照缓存,而非直接缓存聚合根实例:
- 定期(比如每N个事件)生成聚合根的快照,快照包含当前状态的结构化数据(不需要是实例)
- 缓存快照的同时,记录快照对应的最新事件版本
- 获取聚合根时,先取快照,再重放快照之后的事件,得到最新状态
这种方式既解决性能问题,又避免实例序列化的麻烦,快照用纯JSON结构,重放事件时自动重建聚合根实例:
// 生成快照 async function createOrderSnapshot(orderId: string) { // 1. 获取所有事件,重放得到聚合根 const events = await eventStore.getEventsForAggregate(orderId); const order = new Order(); order.loadFromHistory(events); // 2. 生成快照(纯JSON,包含状态和版本) const snapshot = { id: order.id, total: { amount: order.total.amount, currency: order.total.currency }, version: events.length // 快照对应的最新事件版本 }; // 3. 缓存快照 await cacheManager.set(`order:snapshot:${orderId}`, snapshot); } // 获取聚合根 async function getOrder(orderId: string) { // 1. 尝试获取快照 const snapshot = await cacheManager.get(`order:snapshot:${orderId}`); let order: Order; let lastVersion = 0; if (snapshot) { // 从快照初始化聚合根 order = new Order(snapshot.id, new Money(snapshot.total.amount, snapshot.total.currency)); lastVersion = snapshot.version; } else { order = new Order(); } // 2. 获取快照之后的事件,重放 const newEvents = await eventStore.getEventsForAggregate(orderId, lastVersion + 1); order.loadFromHistory(newEvents); // 3. 按需更新快照(比如每10个事件更新一次) if (newEvents.length >= 10) { await createOrderSnapshot(orderId); } return order; }
这种方式更符合事件溯源的设计思想,快照相当于状态的中间缓存,即使缓存失效,也能通过事件重放恢复,同时减少大量事件重放的性能开销。
内容的提问来源于stack exchange,提问作者petervd81
相关产品推荐
相关产品推荐

