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

如何使用Knex事务实现一对多关系的记录插入?

如何使用Knex事务实现一对多关系的记录插入?

我来帮你梳理下这个问题,你的思路方向完全是对的,但代码里有个关键的小问题需要调整,咱们一步步来解决:

首先说你现有代码里的坑:input.genres.forEach(async (g) => { ... })这里存在异步操作的时序问题——forEach不会等待内部的异步任务完成,这会导致事务可能在所有genre关联操作还没做完的时候就提前提交了,轻则出现部分关联记录缺失,重则数据完全不一致。

接下来给你修正后的实现方案,同时解释每个步骤的必要性:

核心思路

  1. 在事务内先插入书籍,拿到完整的书籍对象
  2. 对每个genre执行「查找或创建」操作,必须确保这些操作也在当前事务内,否则事务回滚时新建的genre不会被删除
  3. 批量插入书籍与genre的关联记录(比逐个插入更高效)
  4. 所有操作完成后返回书籍对象;任何一步出错,事务自动回滚,所有已插入的数据都会被撤销

修正后的代码

async create(input: {
    name: string;
    user: User;
    genres: string[];
}): Promise<Book | undefined> {
    try {
        return await knex.transaction(async (trx) => {
            // 1. 插入书籍并返回完整的书籍实体
            const [insertedBook] = await trx<Book>("books").insert(
                {
                    name: input.name,
                    user_id: input.user.id,
                },
                ["id", "name", "user_id", "created_at", "updated_at"]
            );

            // 2. 批量处理所有genre:确保每个操作都在事务内完成
            const genreIds = await Promise.all(
                input.genres.map(async (genreName) => {
                    // 注意:这里要把事务对象trx传给findOrCreate方法
                    const { id } = await this.gs.findOrCreate(genreName, trx);
                    return id;
                })
            );

            // 3. 批量生成关联记录并插入中间表,提升性能
            const bookGenreRelations = genreIds.map(genreId => ({
                book_id: insertedBook.id,
                genre_id: genreId,
            }));
            await trx<BookGenre>("book_genres").insert(bookGenreRelations);

            // (可选)如果需要返回包含genre信息的完整书籍,可以在这里做关联查询
            // const fullBook = await trx("books")
            //     .where("id", insertedBook.id)
            //     .leftJoin("book_genres", "books.id", "book_genres.book_id")
            //     .leftJoin("genres", "book_genres.genre_id", "genres.id")
            //     .select("books.*", trx.raw("array_agg(genres.name) as genres"))
            //     .groupBy("books.id")
            //     .first();

            return insertedBook;
        });
    } catch (error) {
        console.error("创建书籍及关联分类失败", error);
        return undefined;
    }
}

配套调整findOrCreate方法

你需要把findOrCreate改成支持传入事务对象的版本,这样所有操作都在同一个事务上下文里:

async findOrCreate(genreName: string, trx: Knex.Transaction): Promise<Genre> {
    // 先尝试查找已有分类
    let genre = await trx<Genre>("genres").where("name", genreName).first();
    
    if (!genre) {
        // 不存在则插入并返回新分类
        [genre] = await trx<Genre>("genres").insert({ name: genreName }, ["id", "name"]);
    }
    
    return genre;
}

关键注意点

  • 用Promise.all替代forEach处理异步操作:既保证所有genre操作完成后再继续,又能并行处理提升效率
  • 所有数据库操作必须使用事务对象trx:如果用全局的knex实例执行操作,那部分操作会脱离事务,回滚时不会被撤销
  • 批量插入关联记录:当genre数量较多时,批量插入比循环逐个插入能大幅减少数据库请求次数,提升性能

备注:内容来源于stack exchange,提问作者bambi

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.13 19:08:11