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

如何在TypeORM中创建带前缀的补零PrimaryGeneratedColumn

实现TypeORM带前缀补零的自增主键

我来分享几个更优雅的实现方式,解决你提到的用EntityListeners不够理想的问题:

方案1:数据库层面生成(推荐生产环境使用)

这种方式把主键生成逻辑交给数据库,能彻底避免应用层的并发问题,是最可靠的方案。以PostgreSQL为例,步骤如下:

  1. 创建自增序列:
CREATE SEQUENCE pr_id_seq START WITH 1 INCREMENT BY 1;
  1. 创建触发器函数,用于生成带前缀的主键:
CREATE OR REPLACE FUNCTION generate_pr_id()
RETURNS TRIGGER AS $$
BEGIN
  NEW.id := 'PR' || LPAD(NEXTVAL('pr_id_seq')::TEXT, 6, '0');
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;
  1. 给你的实体表绑定触发器:
CREATE TRIGGER trigger_generate_pr_id
BEFORE INSERT ON your_entity_table
FOR EACH ROW EXECUTE FUNCTION generate_pr_id();

然后在TypeORM实体里,主键字段只需要定义为普通的PrimaryColumn即可:

import { Entity, PrimaryColumn } from "typeorm";

@Entity()
export class YourEntity {
  @PrimaryColumn({ type: "varchar", length: 8 })
  id: string;

  // 其他字段...
}

优点:完全由数据库保证唯一性和自增逻辑,应用层无需额外处理,并发安全。
缺点:需要依赖特定数据库的语法(不同数据库触发器写法有差异)。

方案2:TypeORM自定义主键生成器

通过扩展TypeORM的主键生成器,实现自定义的主键生成逻辑,这样可以在实体里直接使用,更符合TypeORM的使用习惯。

  1. 实现自定义主键生成器:
import { AbstractPrimaryColumnGenerator, ColumnMetadata, EntityMetadata, InsertQueryBuilder } from "typeorm";

export class CustomPrimaryGenerator extends AbstractPrimaryColumnGenerator {
  async generate(
    entityMetadata: EntityMetadata,
    column: ColumnMetadata,
    queryBuilder: InsertQueryBuilder<any>
  ): Promise<string> {
    // 推荐用数据库序列避免并发问题
    const nextVal = await queryBuilder.connection.query("SELECT NEXTVAL('pr_id_seq')");
    const num = nextVal[0].nextval;
    return `PR${num.toString().padStart(6, '0')}`;
  }
}
  1. 在TypeORM配置里注册这个生成器:
import { DataSource } from "typeorm";
import { CustomPrimaryGenerator } from "./CustomPrimaryGenerator";

export const AppDataSource = new DataSource({
  // 其他配置...
  primaryColumnGenerators: [
    {
      name: "custom-pr-primary",
      generator: new CustomPrimaryGenerator()
    }
  ]
});
  1. 在实体里使用这个生成器:
import { Entity, PrimaryColumn } from "typeorm";

@Entity()
export class YourEntity {
  @PrimaryColumn({ 
    type: "varchar", 
    length: 8,
    generator: "custom-pr-primary"
  })
  id: string;

  // 其他字段...
}

优点:封装在TypeORM生态内,应用层代码更整洁,逻辑集中。
缺点:需要自己处理并发问题(最好结合数据库序列),不同数据库适配需要调整查询语句。

方案3:改进版EntityListeners(适合快速验证)

如果你不想改动数据库,也可以优化EntityListeners的实现,尽量降低并发风险:

import { Entity, PrimaryColumn, BeforeInsert } from "typeorm";
import { getConnection } from "typeorm";

@Entity()
export class YourEntity {
  @PrimaryColumn({ type: "varchar", length: 8 })
  id: string;

  @BeforeInsert()
  async generateId() {
    // 查询当前最大的ID,提取数字部分加1
    const maxRecord = await getConnection()
      .createQueryBuilder()
      .select("MAX(id)", "maxId")
      .from(YourEntity, "entity")
      .getRawOne();
    
    let nextNum = 1;
    if (maxRecord.maxId) {
      const numPart = maxRecord.maxId.slice(2); // 去掉前缀"PR"
      nextNum = parseInt(numPart, 10) + 1;
    }
    this.id = `PR${nextNum.toString().padStart(6, '0')}`;
  }

  // 其他字段...
}

注意:这种方式在高并发场景下可能会出现重复ID的问题,因为查询和插入不是原子操作。如果是低并发场景可以用,生产环境不推荐。


内容的提问来源于stack exchange,提问作者Andrey Smirnov

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.19 07:27:25