sqliteで同期的に複数行を操作する(node.js)

JavaScript

javascriptもsqliteも非同期で動作するのが標準のため、同期的に書くのに割と苦労しました。
複数行をInsert後にselect * で取得しても中途半端な結果が返ってきたり、コールバックでうまく処理したつもりでもDBで処理中のデータが返ってきたり、、、

この書き方が好ましいのか分かりませんが、備忘録。

クエリを1つにまとめてしまう方法

ダメな例(array.forEach)
let ids = [1,2,3]
async function deleteBooks(ids){
    let stmt = db.prepare(`DELETE FROM books WHERE id = $id`);
    ids.forEach(id =>{
        await stmt.run({$id: id});
    })
    stmt.finalize();
}
ダメな例(array.map)

async function deleteBooks(ids){
    let stmt = db.prepare(`DELETE FROM books WHERE id = $id`);
    await Promise.all(ids.map(async id => await stmt.run({$id: id})))
    stmt.finalize();

}
ダメな例2(for of)

async function deleteBooks(ids){
    let stmt = db.prepare(`DELETE FROM books WHERE id = $id`);
    for (const id of ids) {
        await stmt.run({$id: id});
    }

    stmt.finalize();
}

どれも動きそうですがdeleteBooks呼び出し元でawaitしてもDB処理の全てが終わる前にメソッドを抜けてしまい、コールバック処理がDB処理中に作動してしまいました。

DELETEの場合はidの配列しか渡す必要がないため、簡単に処理するには以下のように1クエリに纏めてしまうのが簡単です。

function deleteBooks(ids){
    db.run(`DELETE FROM books WHERE id in (${ids})`)
}

これ処理的には非同期なんでしょうけど、ローカルアプリでデータ数十行消す程度ならコールバック処理でSelect *しても問題なく削除済みのデータが取得されました。多数のコネクションを作ってアクセスするのなら避けた方がいいかも?
なおDELETEではなくINSERTする場合も同様の書き方でいけます。
VALUES (?), (?), (?)でゴリゴリ書けば通ります。個人的に読みにくいので好きじゃないですが。

db.serializeで同期的に処理する方法

sqliteではdb.serialize()の直下で実行するクエリはdb.serialize内では同期的に処理されます。
db.serialize()外は非同期処理となるため、注意しないと順序がややこしくなります。

db.serialize(() => {
    db.run(query1, params);
    db.run(query2, params);
    db.run(query3, params);
})
// 上から順番に処理される。

db.serialize(() => {
    db.run(query1, params);
    db.run(query2, params);
    db.run(query3, params);
})
db.run(query4, params);
// serializeとquery4は非同期実行のため、query4がどのタイミングで実行されるか分からない。

なのでserialize内でforで回せば同期的に処理されます。
試してないですがmapで回しても同期的に処理されるはずです。ただ実行順序は保証されないと思います。

let query = `INSERT OR REPLACE INTO books (id, title) VALUES($id, $title)`;
db.serialize(() => {
    for(let book of books){
        db.run(query, {$id: book.id, $title: book.title});
    }
    // db.close();  
    // もしcloseするならserialize内で行わないと処理中にcloseが走る可能性がある。
})

これもdb.serialize自体は非同期メソッドなので、コールバックの処理のさせ方に気を使わないと処理中の値が返ります。
Promiseを利用して適切なタイミングで処理を返しましょう。
上記の例だと適当にserializeの最後に意味のないクエリを処理させて、そのコールバックでresolveさせるとdb処理終了のタイミングで狙ったコールバックを発火できます。

function saveBooks(books){
    return new Promise(resolve =>{
        db.serialize(() => {
            for(let book of books){
                db.run(query, {$id: book.id, $title: book.title});
            }
            db.run(`SELECT 0`, () => {

                resolve();
            });
        })
    })
}

async function doSomething(){
    let result = await saveBooks(books);
    callback(result);
}

コメント