使用TypeORM QueryBuilder实现多对多关系批量Upsert并支持级联
解决TypeORM QueryBuilder实现多对多关系的批量Upsert+级联问题
我之前也遇到过类似的困扰——TypeORM的save方法不支持批量Upsert,而QueryBuilder又没法像save那样自动处理嵌套的多对多级联,RelationalQueryBuilder还不兼容MySQL的Upsert语法。经过一番折腾,我总结出了一套分步骤的解决方案,能同时实现Product的批量Upsert和ProductTag的级联维护,下面具体说:
前提准备:优化实体约束
首先得给ProductTag的name字段加上唯一约束,避免重复创建相同标签;同时给多对多中间表添加复合唯一约束,防止同一个产品和标签重复关联:
// ProductTag实体优化 @Entity() export class ProductTag { @PrimaryGeneratedColumn() id: number; @Column('varchar', { unique: true }) // 添加唯一约束,确保标签名唯一 name: string | null; @ManyToMany(type => Product, product => product.productTags) products: Product[] | null; } // Product实体的@JoinTable优化 @Entity() export class Product { // ... 原有字段 ... @ManyToMany(type => ProductTag, productTag => productTag.products, { cascade: ['insert', 'update'] }) @JoinTable({ name: 'product_product_tag', // 明确中间表名称(可根据自己需求修改) joinColumn: { name: 'productId', referencedColumnName: 'id' }, inverseJoinColumn: { name: 'productTagId', referencedColumnName: 'id' }, uniqueConstraints: [{ columns: ['productId', 'productTagId'] }] // 复合唯一约束,避免重复关联 }) productTags: ProductTag[] | null; }
步骤1:Upsert ProductTag,确保所有标签都存在
先从传入的products数组中提取所有标签名,去重后批量Upsert到ProductTag表,保证后续关联时标签都存在:
// 提取所有去重的标签名 const allTagNames = Array.from( new Set(products.flatMap(p => p.productTags?.map(t => t.name).filter(Boolean) || [])) ); // 批量Upsert标签,并获取标签名与ID的映射 const tagNameToIdMap = await getConnection() .createQueryBuilder() .insert() .into(ProductTag) .values(allTagNames.map(name => ({ name }))) .orUpdate({ conflict_target: ['name'], overwrite: [] }) // 标签名唯一,重复时无需更新 .execute() .then(async () => { const tags = await getRepository(ProductTag).find({ where: { name: In(allTagNames) } }); return new Map(tags.map(tag => [tag.name, tag.id])); });
步骤2:批量Upsert主实体Product
处理Product的批量Upsert,这里只需要处理Product自身的字段,关联关系后面单独维护:
// 整理Product的Upsert数据(剔除productTags字段) const productUpsertData = products.map(p => ({ shopifyId: p.shopifyId, shopId: p.shopId, name: p.name, productType: p.productType, updated: new Date() // 或者使用传入的updated时间 })); // 执行Product的批量Upsert await getConnection() .createQueryBuilder() .insert() .into(Product) .values(productUpsertData) .orUpdate({ conflict_target: ['shopifyId'], overwrite: ['name', 'shopId', 'productType', 'updated'] }) .execute(); // 获取Product的shopifyId与ID的映射,用于后续关联 const productShopifyIdToIdMap = await getRepository(Product) .find({ where: { shopifyId: In(productUpsertData.map(p => p.shopifyId)) } }) .then(products => new Map(products.map(p => [p.shopifyId, p.id])));
步骤3:同步多对多中间表
先删除目标产品的旧关联,再插入新的关联关系,保证关联数据是最新的:
// 整理需要插入的关联数据(productId + productTagId) const productTagRelations = products.flatMap(p => { const productId = productShopifyIdToIdMap.get(p.shopifyId); if (!productId) return []; return (p.productTags?.map(tag => { const tagId = tagNameToIdMap.get(tag.name); if (!tagId) return null; return { productId, productTagId: tagId }; }).filter(Boolean) || []) as { productId: number; productTagId: number }[]; }); // 删除目标产品的所有旧关联 const targetProductIds = Array.from(productShopifyIdToIdMap.values()); await getConnection() .createQueryBuilder() .delete() .from('product_product_tag') // 对应@JoinTable中设置的中间表名 .where('productId IN (:...productIds)', { productIds: targetProductIds }) .execute(); // 插入新的关联关系 await getConnection() .createQueryBuilder() .insert() .into('product_product_tag') .values(productTagRelations) .execute();
为什么要这么做?
TypeORM的QueryBuilder本身不支持自动解析嵌套的多对多关系并执行级联Upsert,所以必须拆分流程:
- 先确保关联的子实体(ProductTag)都存在,避免关联时出现外键错误
- 处理主实体的Upsert逻辑
- 单独维护多对多中间表,保证关联关系与传入数据一致
内容的提问来源于stack exchange,提问作者JohnnyBack
相关产品推荐
相关产品推荐

