NodeJS 异步SQLite语句执行 Package 函数的问题

ttcibm8c  于 2023-03-12  发布在  Node.js
关注(0)|答案(2)|浏览(130)

我在创建和填充表后立即执行SQL语句时遇到问题。
我认为记录不存在,因为创建查询没有及时完成。
如果我为clean标志(在createDatabaseFromSQL中)传递false,经过几次尝试后,
我得到了预期的结果:

$ node ./setup.js 
CREATE database
Executing: DROP TABLE IF EXISTS `accounts`
Executing: CREATE TABLE IF NOT EXISTS `accounts` ( `id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, `username` VARCHAR(50) NOT NULL, `password` VARCHAR(255) NOT NULL, `email` VARCHAR(100) NOT NULL )
Executing: INSERT INTO `accounts` (`username`, `password`, `email`) VALUES ('admin', 'admin', 'admin@admin.com'), ('test', 'test', 'test@test.com')
FETCH test account
Connected to the 'auth' database.
Connected to the 'auth' database.
true
Close the 'auth' database connection.
Close the 'auth' database connection.

如果强制清除,选择将失败,因为accounts表不存在。

$ node ./setup.js 
CREATE database
Executing: DROP TABLE IF EXISTS `accounts`
Executing: CREATE TABLE IF NOT EXISTS `accounts` ( `id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, `username` VARCHAR(50) NOT NULL, `password` VARCHAR(255) NOT NULL, `email` VARCHAR(100) NOT NULL )
Executing: INSERT INTO `accounts` (`username`, `password`, `email`) VALUES ('admin', 'admin', 'admin@admin.com'), ('test', 'test', 'test@test.com')
FETCH test account
Connected to the 'auth' database.
Connected to the 'auth' database.
node:internal/process/esm_loader:97
    internalBinding('errors').triggerUncaughtException(
                              ^

[Error: SQLITE_ERROR: no such table: accounts] {
  errno: 1,
  code: 'SQLITE_ERROR'
}

Node.js v18.14.0

文件
以下是相关文件。

设置. js

import { createDatabaseFromSql, executeQuery } from "./query-utils.js";

console.log("CREATE database");
await createDatabaseFromSql("auth", "./setup.sql", true);

console.log("FETCH test account");
const row = await executeQuery(
  "auth",
  "SELECT * FROM `accounts` where `username` = ?",
  ["test"]
);
console.log(row?.id === 2);

查询实用程序. js

import fs from "fs";
import sqlite3 from "sqlite3";

const SQL_DEBUG = true;

const loadSql = (filename, delimiter = ";") =>
  fs
    .readFileSync(filename)
    .toString()
    .replace(/(\r\n|\n|\r)/gm, " ")
    .replace(/\s+/g, " ")
    .split(delimiter)
    .map((statement) => statement.trim())
    .filter((statement) => statement.length);

const executeSerializedQueries = async (databaseName, callback) => {
  let db;
  try {
    db = new sqlite3.Database(`./${databaseName}.db`, (err) => {
      if (err) console.error(err.message);
      console.log(`Connected to the '${databaseName}' database.`);
    });
    db.serialize(() => {
      callback(db);
    });
  } catch (e) {
    throw Error(e);
  } finally {
    if (db) {
      db.close((err) => {
        if (err) console.error(err.message);
        console.log(`Close the '${databaseName}' database connection.`);
      });
    }
  }
};

const createDatabaseFromSql = async (databaseName, sqlFilename, clean) =>
  new Promise((resolve, reject) => {
    if (clean) {
      fs.rmSync(`./${databaseName}.db`, { force: true }); // Remove existing
    }
    try {
      executeSerializedQueries(databaseName, (db) => {
        loadSql(sqlFilename).forEach((statement) => {
          if (SQL_DEBUG) {
            console.log("Executing:", statement);
          }
          db.run(statement);
        });
        resolve();
      });
    } catch (e) {
      reject(e);
    }
  });

const executeQuery = async (databaseName, query, params = []) =>
  new Promise((resolve, reject) => {
    try {
      executeSerializedQueries(databaseName, (db) => {
        db.get(query, params, (error, row) => {
          if (error) reject(error);
          else resolve(row);
        });
      });
    } catch (e) {
      reject(e);
    }
  });

const executeQueryAll = async (databaseName, query, params = []) =>
  new Promise((resolve, reject) => {
    try {
      executeSerializedQueries(databaseName, (db) => {
        db.all(query, params, (error, rows) => {
          if (error) reject(error);
          else resolve(rows);
        });
      });
    } catch (e) {
      reject(e);
    }
  });

export {
  createDatabaseFromSql,
  executeSerializedQueries,
  executeQuery,
  executeQueryAll,
  loadSql,
};

设置. sql

DROP TABLE IF EXISTS `accounts`;

CREATE TABLE IF NOT EXISTS `accounts` (
  `id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
  `username` VARCHAR(50) NOT NULL,
  `password` VARCHAR(255) NOT NULL,
  `email` VARCHAR(100) NOT NULL
);

INSERT INTO `accounts` (`username`, `password`, `email`)
  VALUES ('admin', 'admin', 'admin@admin.com'),
    ('test', 'test', 'test@test.com');

软件包. json

{
  "dependencies": {
    "sqlite3": "^5.1.4"
  }
}
qacovj5a

qacovj5a1#

db.serialize很难在promise/async代码中使用,因为它不遵循“完成后运行此回调”的标准回调约定。它也只序列化sqlite的查询,不影响nodejs中的控制流,将立即继续使用更多代码。
基本上,db.serialize中的最终查询通常是在您希望解析承诺时,类似于:

function dbSerialize(db) {
  return new Promise((resolve, reject) => {
    const rejector = (err) => err && reject(err)
    db.serialize(() => {
      db.run(a, rejector)
      db.run(b, rejector)
      db.run(c, rejector)
      db.run(d, (err) => {
        if (err) return reject(err)
        resolve(true)
      })
    })
  })
}

当传递包含在其他地方构造的数据库命令的函数时,这也很难概括,如示例代码中的。
幸运的是,promise提供了自己的控制流,所以我们可以暂时不考虑serialize。

转换成承诺

下面是一个承诺的“打开数据库”函数

import sqlite3 from 'sqlite3'

function openDatabase(databaseName) {
  return new Promise((resolve, reject) => {
    const db = new sqlite3.Database(`./${databaseName}.db`, (err) => {
      if (err) return reject(err)
      console.log(`Connected to the '${databaseName}' database.`);
      resolve(db)
    })
  })
}

回调中的err被拒绝,value在回调中被解析,promise从函数返回。函数只有一个用途,即把回调操作转换为promise。
你可以在一个承诺中 Package 更大的回调链,但是承诺存在的原因是为了消除管理大型异步回调链的心理负担和边缘情况。同样的基本规则适用,来自 all 回调的err应该reject或被处理。然后回调链中的最后一个函数应该resolve一个值,如果你想等待所有事情完成。
这不是一个通用的解决方案,如OP,但一个简单的promised版本的安装程序,然后可以。

import { readFile, rm } from 'node:fs/promises'
import { promisify } from 'node:util'

// Now open can be awaited.
const db = await openDatabase('myfile')

// Use node built in to promisify sqlite3's regular callback functions
dbRun = promisify(db.run)
dbGet = promisify(db.get)
dbAll = promisify(db.all)
dbClose = promisify(db.close)

数据库API现在可以通过承诺函数访问,因此所有函数都不需要重复地用承诺来 Package 回调。
串行查询可以在for循环中等待(顺序类似于db.serialize),但nodejs的控制流在查询完成之前不会继续,所有回调错误都由promised函数处理。

try {
  const sql_statements = await loadFile(sqlFilename)
  for (const statement of sql_statements) {
    await dbRun(statement)
  }
  const rows = await dbGet("SELECT * FROM `accounts` where `username` = ?", ["test"])
  console.log(rows)
}
finally {
  if (db) {
    await dbClose()
    console.log(`Close the '${databaseName}' database connection.`);
  }
}
fquxozlt

fquxozlt2#

我最终选择了better-sqlite3。这个库不依赖于asyncPromise逻辑。

设置. js

import { createDatabaseFromSql, query } from "./src/app/js/query-utils.js";

console.log("CREATE database");
createDatabaseFromSql("auth", "./data/setup.sql", true);

console.log("FETCH test account");
const result = query("auth", "SELECT * FROM `accounts` where `username` = ?", [
  "test",
]);
console.log(result.id === 2);

查询实用程序. js

import fs from "fs";
import Database from "better-sqlite3";

const openDatabase = (databaseName) =>
  new Database(`${databaseName}.db`, { verbose: console.log });

const dropDatabase = (databaseName) => {
  fs.rmSync(`./${databaseName}.db`, { force: true });
};

const createDatabaseFromSql = (databaseName, sqlFilename, clean) => {
  if (clean) dropDatabase(databaseName);
  const db = openDatabase(databaseName);
  db.exec(fs.readFileSync(sqlFilename, "utf8"));
  db.close();
};

const query = (databaseName, query, params = []) => {
  let db = null;
  try {
    db = openDatabase(databaseName);
    return db.prepare(query).get(...params);
  } finally {
    if (db) {
      db.close();
    }
  }
};

const queryAll = (databaseName, query, params = []) => {
  let db = null;
  try {
    db = openDatabase(databaseName);
    return db.prepare(query).all(...params);
  } finally {
    if (db) {
      db.close();
    }
  }
};

export { createDatabaseFromSql, openDatabase, query, queryAll };

相关问题