You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.02 08:45:20