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

如何在TypeORM中通过标题自动生成并更新唯一Slug?

在TypeORM(NestJS + PostgreSQL)中自动生成并更新唯一Slug

实现方案

1. 安装slug生成工具

使用slugify将标题转换为标准化的无特殊字符slug:

npm install slugify

2. 核心实现逻辑

通过数据库约束保证slug唯一性,结合业务逻辑处理创建/更新时的slug生成,同时避免并发冲突。

修改Blog实体

给slug字段添加数据库唯一约束,作为最后一道防线:

import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';

@Entity()
export class Blog {
    @PrimaryGeneratedColumn()
    id: number;

    @Column({ type: "text", nullable: false, unique: true })
    slug: string;

    @Column({ type: "text", nullable: false })
    title: string;

    @Column({ type: "text", nullable: false })
    body: string;

    @CreateDateColumn()
    created_at: Date;

    @UpdateDateColumn()
    updated_at: Date;
}

封装slug生成服务

创建独立服务统一处理唯一slug的生成逻辑:

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, Not } from 'typeorm';
import slugify from 'slugify';
import { Blog } from './blog.entity';

@Injectable()
export class BlogSlugService {
    constructor(
        @InjectRepository(Blog)
        private readonly blogRepo: Repository<Blog>,
    ) {}

    async generateUniqueSlug(title: string, excludeId?: number): Promise<string> {
        let baseSlug = slugify(title, { lower: true, strict: true });
        let targetSlug = baseSlug;
        let counter = 1;

        // 循环检查直至找到未被使用的slug
        while (true) {
            const exists = await this.blogRepo.findOne({
                where: { slug: targetSlug },
                ...(excludeId && { where: { id: Not(excludeId) } }),
            });

            if (!exists) break;

            targetSlug = `${baseSlug}-${counter}`;
            counter++;
        }

        return targetSlug;
    }
}

在业务服务中集成

在博客的创建和更新流程中自动处理slug:

import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Blog } from './blog.entity';
import { BlogSlugService } from './blog-slug.service';

@Injectable()
export class BlogService {
    constructor(
        @InjectRepository(Blog)
        private readonly blogRepo: Repository<Blog>,
        private readonly slugService: BlogSlugService,
    ) {}

    async createBlog(dto: { title: string; body: string }) {
        const slug = await this.slugService.generateUniqueSlug(dto.title);
        const blog = this.blogRepo.create({ ...dto, slug });
        return this.blogRepo.save(blog);
    }

    async updateBlog(id: number, dto: { title?: string; body?: string }) {
        const blog = await this.blogRepo.findOneBy({ id });
        if (!blog) throw new NotFoundException('博客不存在');

        // 仅当标题变更时重新生成slug
        if (dto.title && dto.title !== blog.title) {
            blog.slug = await this.slugService.generateUniqueSlug(dto.title, id);
        }

        Object.assign(blog, dto);
        return this.blogRepo.save(blog);
    }
}

3. 可选:用TypeORM订阅者实现内聚逻辑

如果希望将slug处理逻辑和实体绑定,可以使用TypeORM订阅者:

import { EntitySubscriberInterface, EventSubscriber, InsertEvent, UpdateEvent } from 'typeorm';
import { Blog } from './blog.entity';
import slugify from 'slugify';
import { Not } from 'typeorm';

@EventSubscriber()
export class BlogSubscriber implements EntitySubscriberInterface<Blog> {
    listenTo() {
        return Blog;
    }

    async beforeInsert(event: InsertEvent<Blog>) {
        const blog = event.entity;
        blog.slug = await this.getUniqueSlug(blog.title, event.manager);
    }

    async beforeUpdate(event: UpdateEvent<Blog>) {
        const blog = event.entity;
        const originalBlog = await event.manager.findOne(Blog, { where: { id: blog.id } });
        
        if (blog.title !== originalBlog?.title) {
            blog.slug = await this.getUniqueSlug(blog.title, event.manager, blog.id);
        }
    }

    private async getUniqueSlug(title: string, manager: any, excludeId?: number) {
        let baseSlug = slugify(title, { lower: true, strict: true });
        let targetSlug = baseSlug;
        let counter = 1;

        while (true) {
            const exists = await manager.findOne(Blog, {
                where: { slug: targetSlug },
                ...(excludeId && { where: { id: Not(excludeId) } }),
            });

            if (!exists) break;

            targetSlug = `${baseSlug}-${counter}`;
            counter++;
        }

        return targetSlug;
    }
}

注册订阅者:在NestJS模块的providers数组中添加BlogSubscriber即可。


内容的提问来源于stack exchange,提问作者Md Ali

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.07 04:46:12