如何在TypeORM中创建带前缀的补零PrimaryGeneratedColumn
实现TypeORM带前缀补零的自增主键
我来分享几个更优雅的实现方式,解决你提到的用EntityListeners不够理想的问题:
方案1:数据库层面生成(推荐生产环境使用)
这种方式把主键生成逻辑交给数据库,能彻底避免应用层的并发问题,是最可靠的方案。以PostgreSQL为例,步骤如下:
- 创建自增序列:
CREATE SEQUENCE pr_id_seq START WITH 1 INCREMENT BY 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;
- 给你的实体表绑定触发器:
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的使用习惯。
- 实现自定义主键生成器:
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')}`; } }
- 在TypeORM配置里注册这个生成器:
import { DataSource } from "typeorm"; import { CustomPrimaryGenerator } from "./CustomPrimaryGenerator"; export const AppDataSource = new DataSource({ // 其他配置... primaryColumnGenerators: [ { name: "custom-pr-primary", generator: new CustomPrimaryGenerator() } ] });
- 在实体里使用这个生成器:
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
相关产品推荐
相关产品推荐

