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

NestJS + TypeORM 如何优雅处理实体中Buffer类型ULID与DTO字符串类型的转换?

NestJS + TypeORM 如何优雅处理实体中Buffer类型ULID与DTO字符串类型的转换?

我太懂这种DTO和实体类型不匹配的糟心感了,既要符合DDD里DTO不能有业务逻辑的规则,又要少写重复代码,确实得找个优雅的路子。结合NestJS和TypeORM的生态,给你几个实际项目里验证过的方案,你可以按需选:

方案一:TypeORM 自定义ULID列类型(最推荐,一劳永逸)

这应该是最干净的解决方案了,直接在TypeORM层面统一类型,让实体的userId直接用string类型,底层自动帮你在string和Buffer之间转换,服务层和DTO全用string,完全不用额外处理转换逻辑。

步骤很简单:

  1. 先确保你已经安装了ulid库(毕竟用ULID肯定少不了它)
  2. 自定义一个ULID列类型,实现TypeORM的ColumnType接口:
// src/common/types/ulid-column.ts
import { ColumnType, ValueTransformer } from 'typeorm';
import { ulid } from 'ulid';

export class UlidColumn implements ColumnType {
  name = 'ulid';
  default?: string;

  getColumnType(): string {
    return 'binary(16)';
  }

  to(value: string): Buffer {
    return ulid.toBuffer(value);
  }

  from(value: Buffer): string {
    return ulid.fromBuffer(value);
  }

  getValueTransformer(): ValueTransformer {
    return {
      to: this.to.bind(this),
      from: this.from.bind(this),
    };
  }
}
  1. 在TypeORM的配置里注册这个自定义类型:
// src/config/typeorm.config.ts
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
import { UlidColumn } from './common/types/ulid-column';

export const typeOrmConfig: TypeOrmModuleOptions = {
  // 其他配置(数据库地址、账号密码等)...
  type: 'mysql', // 换成你实际用的数据库类型
  entities: [__dirname + '/../**/*.entity{.ts,.js}'],
  extra: {
    connection: {
      types: {
        ulid: new UlidColumn(),
      },
    },
  },
};
  1. 最后修改你的实体类,直接用string类型:
@Entity('users', { schema: 'DB' })
export class Users {
  @ApiProperty({
    example: '01J3Z772NCBMD9BETHBMB74HBW',
    description: 'User ULID',
  })
  @Column('ulid', { primary: true, name: 'user_id' })
  userId: string;
}

这样一来,从数据库读出来的userId自动是string,存的时候TypeORM自动把string转成16位Buffer存到数据库,DTO的userId还是string,服务层全程用string,完全不用管转换,也不影响mapped-types的使用,完美符合你的需求。

方案二:TypeORM 实体订阅器(适合已存在的实体,无需修改列定义)

如果不想改现有的实体列定义,用TypeORM的实体订阅器也是个好办法,它能监听实体的加载、插入等生命周期事件,自动帮你转换类型,代码也很简洁,不会重复。

  1. 创建一个ULID转换的订阅器:
// src/common/subscribers/ulid-subscriber.ts
import { EntitySubscriberInterface, EventSubscriber, LoadEvent, InsertEvent, UpdateEvent } from 'typeorm';
import { ulid } from 'ulid';
import { Users } from '../users/entities/user.entity';

// 可以多个实体共用这个订阅器,只要实体里有Buffer类型的userId
@EventSubscriber()
export class UlidSubscriber implements EntitySubscriberInterface {
  listenTo() {
    return [Users]; // 这里可以加所有需要处理的实体
  }

  // 实体从数据库加载后,把Buffer转成string
  afterLoad(event: LoadEvent<any>) {
    if (event.entity.userId instanceof Buffer) {
      event.entity.userId = ulid.fromBuffer(event.entity.userId);
    }
  }

  // 实体插入数据库前,把string转成Buffer
  beforeInsert(event: InsertEvent<any>) {
    if (typeof event.entity.userId === 'string') {
      event.entity.userId = ulid.toBuffer(event.entity.userId);
    }
  }

  // 处理更新操作的类型转换
  beforeUpdate(event: UpdateEvent<any>) {
    if (typeof event.entity.userId === 'string') {
      event.entity.userId = ulid.toBuffer(event.entity.userId);
    }
  }
}
  1. 在TypeORM配置里注册这个订阅器:
// src/config/typeorm.config.ts
export const typeOrmConfig: TypeOrmModuleOptions = {
  // 其他配置...
  subscribers: [__dirname + '/../**/*.subscriber{.ts,.js}'],
};

这样服务层拿到的Users实体的userId就是string,存的时候自动转成Buffer,DTO也不用做任何修改,而且可以多个实体共用这个订阅器,几乎没有重复代码。

方案三:Class-Transformer 装饰器(解决你之前返回null的问题)

你之前用@Transform返回null,大概率是没正确设置转换规则或者用错了ulid的方法,其实class-transformer配合class-validator是可以搞定的,而且很灵活。

实体层转换(Buffer转string)

在实体的userId字段上加@Transform装饰器,设置toPlainOnly: true,这样当把实体转成普通对象时(比如服务层处理、返回给前端),自动转成string:

import { Transform } from 'class-transformer';
import { ulid } from 'ulid';

@Entity('users', { schema: 'DB' })
export class Users {
  @ApiProperty({
    example: '01J3Z772NCBMD9BETHBMB74HBW',
    description: 'User ULID',
  })
  @Column('binary', { primary: true, name: 'user_id', length: 16 })
  @Transform(({ value }) => {
    // 确保只有Buffer类型才转换,避免重复转换出问题
    return value instanceof Buffer ? ulid.fromBuffer(value) : value;
  }, { toPlainOnly: true })
  userId: Buffer | string; // 这里类型写成联合类型,兼容转换前后的状态
}

DTO层转换(string转Buffer)

在DTO的userId字段上加@Transform,设置toClassOnly: true,这样当把DTO转成实体类时,自动把string转成Buffer:

import { Transform } from 'class-transformer';
import { ulid } from 'ulid';

export class CreateUserDto {
  @IsULID()
  @ApiProperty({
    example: '01J3Z772NCBMD9BETHBMB74HBW',
    description: 'User ULID',
  })
  @Transform(({ value }) => {
    // 这里@IsULID已经验证过是合法字符串,直接转Buffer即可
    return typeof value === 'string' ? ulid.toBuffer(value) : value;
  }, { toClassOnly: true })
  userId: string;
}

这样在服务层用plainToClass或者Nest自动处理的时候,就能正确转换类型,而且不会返回null,你之前的问题可能是没调用正确的ulid转换方法,或者没设置toPlainOnly/toClassOnly导致双向转换出问题。

方案对比

  • 方案一最推荐,属于从底层解决问题,代码最简洁,后续维护成本最低,完全不影响其他逻辑;
  • 方案二更适合已经上线的项目,不想改动现有实体列定义的情况,订阅器可以批量处理多个实体;
  • 方案三比较灵活,适合局部场景,但需要在实体和DTO上加装饰器,稍微有一点代码,但胜在不用改TypeORM配置。

备注:内容来源于stack exchange,提问作者Sandel

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.16 08:28:00