如何用Knex实现GetOrCreate函数及解决异步查询循环问题
嘿,刚接触 Node.js、Promise 和 Knex 第一天就上手做这么实用的功能,已经超棒啦!我来一步步帮你理清思路,解决这些问题~
一、关于 first() 的疑问:必须加!
你提到两个查询最多返回 0 或 1 行,用 first() 绝对是最佳选择——它会自动给 SQL 加上 LIMIT 1,并且返回的 Promise 会直接解析为单个对象(比如 {id: 123})或者 undefined,比处理数组结果要简洁太多。修正后的查询应该是:
knex("ingredients").select('id').where('name', my_param).first() knex("synonyms").select('word_id').where('name', my_param).first()
二、实现 ingredientGetOrCreate 函数
用 async/await 来写这个函数会比嵌套 .then() 直观很多,完全符合你“先查后插”的需求:
async function ingredientGetOrCreate(my_param) { // 并行查询两张表,比串行查效率更高 const [ingredientRes, synonymRes] = await Promise.all([ knex("ingredients").select('id').where('name', my_param).first(), knex("synonyms").select('word_id').where('name', my_param).first() ]); // 优先返回已有结果 if (ingredientRes) return ingredientRes.id; if (synonymRes) return synonymRes.word_id; // 两张表都没数据,插入新食材并返回ID // 注意:returning('id') 是 Knex 获取插入后ID的标准写法,适配多数数据库 const [newIngredientId] = await knex("ingredients") .insert({ name: my_param.trim() }) // 建议trim避免空格导致重复创建 .returning('id'); return newIngredientId; }
这里用 Promise.all 同时发起两个查询,比先查一张再查另一张节省一半时间,非常适合你的场景。
三、解决循环中的异步陷阱
你原来的代码有两个核心问题:没正确等待异步操作完成,也没给关联表传入产品ID。修正后的版本如下:
knex("products") .select("id", "name", "desc") // 别忘了选desc字段! .then(async (products) => { // 遍历每个产品,用for...of配合await确保顺序处理(也可以用Promise.all并行) for (const product of products) { // 分割描述并去除空格,避免空字符串或重复值 const ingredients = product.desc.split(",").map(item => item.trim()).filter(Boolean); // 并行处理当前产品的所有食材 await Promise.all(ingredients.map(async (ingredientName) => { // 等待食材ID获取/创建完成 const ingredientId = await ingredientGetOrCreate(ingredientName); // 插入关联表,必须等待这个操作完成! await knex('ingredients_products').insert({ id_of_ingredient: ingredientId, id_of_product: product.id // 关键:关联当前产品的ID }); })); } console.log('所有产品关联完成!'); }) .catch(err => { // 一定要捕获错误,不然异步出错会静默消失 console.error('处理失败:', err); });
几个新手必注意点:
- 必须返回/等待Promise:
Promise.all需要接收一个 Promise 数组,所以.map()里的函数要加async,并且每个异步操作都要用await。 - 关联产品ID:你的原代码漏了给
ingredients_products传入产品ID,这会导致关联表数据无效,一定要补上。 - 处理空字符串:用
filter(Boolean)过滤分割后可能出现的空值,避免不必要的数据库操作。 - 错误捕获:异步代码的错误一定要用
.catch()捕获,不然程序出错了你根本找不到原因。
内容的提问来源于stack exchange,提问作者renathy
相关产品推荐
相关产品推荐

