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

TypeORM QueryBuilder为商品列表添加isFavorite布尔字段的问题

问题:商品列表返回时添加isFavorite字段并兼容未登录场景

实体定义

Products实体

@Entity()
export class Products {

    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    name: string;

    @Column()
    code: string;

    @Column({
        default: 0
    })
    quantity: number;

    @Column()
    price: number;

    @ManyToOne(() => Category, {
        cascade: true
    })
    @JoinColumn()
    category: Category;

    @OneToMany(() => Favorites, favorite => favorite.product)
    favorites: Favorites[];

    @Column({
        type: 'varchar',
        length: 1000
    })
    description: string;

    @Column({
        type: 'varchar',
        length: 1000
    })
    attributes: string;

    @OneToMany(() => Images, img => img.product, {
        onDelete: 'SET NULL'
    })
    images: Images[]

    @CreateDateColumn()
    createdAt: Date

    @UpdateDateColumn()
    updatedAt: Date

    @DeleteDateColumn()
    deletedAt: Date
}

Favorites实体

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

    @ManyToOne(() => Products)
    @JoinColumn()
    product: Products

    @ManyToOne(() => User)
    @JoinColumn()
    user: User

    @CreateDateColumn()
    createdAt: Date

    @UpdateDateColumn()
    updatedAt: Date

    @DeleteDateColumn()
    deletedAt: Date
}

需求说明

返回全部商品列表时,新增布尔类型isFavorite字段:当前用户已收藏该商品则返回true,否则返回false;同时兼容用户未登录的请求,不能因user.id未定义导致请求失败。

原方案问题分析

原代码存在三个关键问题:

  1. 别名拼写错误:CASE WHEN favorite.id IS NOT NULL中的favorite应为favorites(查询中定义的别名是favorites)
  2. 实体实例不包含自定义字段:getMany()返回的是Products实体类的实例,不会自动包含通过addSelect()添加的自定义字段isFavorite
  3. 未处理未登录场景:当用户未登录时user?.id为undefined,直接传入参数会导致SQL语法错误

修正方案

方案一:使用getRawMany()手动映射结果

直接获取原始查询结果,再手动映射为包含isFavorite的商品对象,无需修改实体:

async getProductsWithFavoriteStatus(user?: User, categoryId?: number) {
    const queryBuilder = this.productsRepository.createQueryBuilder('products')
        .leftJoinAndSelect('products.category', 'category')
        // 仅当categoryId存在时添加分类过滤条件
        .where(categoryId ? 'category.id = :categoryId' : '1=1', { categoryId })
        // 左连接收藏表,仅关联当前用户(如果用户存在)
        .leftJoin('products.favorites', 'favorites', user ? 'favorites.user.id = :userId' : '1=0', { userId: user?.id })
        // 添加isFavorite字段:如果有匹配的收藏记录则为true,否则false
        .addSelect('CASE WHEN favorites.id IS NOT NULL THEN true ELSE false END', 'isFavorite')
        // 避免重复商品(同一商品可能有多条收藏记录,需分组)
        .groupBy('products.id')
        .addGroupBy('category.id'); // 关联category时需一起分组

    const rawResults = await queryBuilder.getRawMany();

    // 手动映射为包含isFavorite的商品对象
    return rawResults.map(result => {
        // 提取并转换products前缀的字段
        const productRaw = Object.fromEntries(
            Object.entries(result).filter(([key]) => key.startsWith('products_'))
        );
        const product = {} as Products;
        for (const [key, value] of Object.entries(productRaw)) {
            product[key.replace('products_', '') as keyof Products] = value;
        }

        // 处理category字段
        if (result.category_id) {
            const categoryRaw = Object.fromEntries(
                Object.entries(result).filter(([key]) => key.startsWith('category_'))
            );
            const category = {} as Category;
            for (const [key, value] of Object.entries(categoryRaw)) {
                category[key.replace('category_', '') as keyof Category] = value;
            }
            product.category = category;
        }

        // 添加isFavorite字段
        product['isFavorite'] = result.isFavorite;
        return product as Products & { isFavorite: boolean };
    });
}

方案二:修改实体添加临时字段,使用getMany()映射

在Products实体中添加临时字段,通过查询直接赋值:

  1. 修改Products实体:
import { Transient } from 'typeorm';

@Entity()
export class Products {
    // ... 原有字段 ...

    @Transient() // 标记为临时字段,不映射到数据库
    isFavorite: boolean;
}
  1. 查询代码:
async getProductsWithFavoriteStatus(user?: User, categoryId?: number) {
    const queryBuilder = this.productsRepository.createQueryBuilder('products')
        .leftJoinAndSelect('products.category', 'category')
        .where(categoryId ? 'category.id = :categoryId' : '1=1', { categoryId })
        .leftJoin('products.favorites', 'favorites', user ? 'favorites.user.id = :userId' : '1=0', { userId: user?.id })
        // 将计算值映射到实体的isFavorite字段
        .addSelect('CASE WHEN favorites.id IS NOT NULL THEN true ELSE false END', 'products.isFavorite')
        .groupBy('products.id')
        .addGroupBy('category.id');

    return queryBuilder.getMany() as Promise<(Products & { isFavorite: boolean })[]>;
}

关键优化点

  • 兼容未登录场景:未登录时leftJoin条件设为1=0,确保不关联任何收藏记录,isFavorite自动为false
  • 避免重复数据:通过groupBy分组,保证同一商品只返回一条记录
  • 字段映射正确:两种方案均保证isFavorite字段能正确出现在返回结果中

内容的提问来源于stack exchange,提问作者alireza kargar

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.21 00:07:35