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

使用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,所以必须拆分流程:

  1. 先确保关联的子实体(ProductTag)都存在,避免关联时出现外键错误
  2. 处理主实体的Upsert逻辑
  3. 单独维护多对多中间表,保证关联关系与传入数据一致

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.08 15:07:53