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未定义导致请求失败。
原方案问题分析
原代码存在三个关键问题:
- 别名拼写错误:
CASE WHEN favorite.id IS NOT NULL中的favorite应为favorites(查询中定义的别名是favorites) - 实体实例不包含自定义字段:
getMany()返回的是Products实体类的实例,不会自动包含通过addSelect()添加的自定义字段isFavorite - 未处理未登录场景:当用户未登录时
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实体中添加临时字段,通过查询直接赋值:
- 修改
Products实体:
import { Transient } from 'typeorm'; @Entity() export class Products { // ... 原有字段 ... @Transient() // 标记为临时字段,不映射到数据库 isFavorite: boolean; }
- 查询代码:
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
相关产品推荐
相关产品推荐

