Better-sqlite3 class Database

来自泡泡学习笔记
跳到导航 跳到搜索

Database(path, [options])

创建一个新的数据库连接。如果数据库文件不存在,它将被创建。这是同步的,这意味着您可以立即开始执行查询。您可以通过将":memory:"作为第一个参数来创建一个内存数据库。您可以通过传递一个空字符串(或省略所有参数)来创建一个临时数据库。

内存数据库也可以通过传递由.serialize()返回的缓冲区来创建,而不是传递一个字符串作为第一个参数。


可以接受各种选项:

  • options.readonly:以只读模式打开数据库连接(默认值:false)。
  • options.fileMustExist:如果数据库不存在,将抛出错误而不是创建新文件。此选项对于内存、临时或只读数据库连接将被忽略(默认值:false)。
  • options.timeout:在锁定数据库上执行查询时等待的毫秒数,然后抛出SQLITE_BUSY错误(默认值:5000)。
  • options.verbose:提供一个函数,该函数将在每次执行数据库连接的SQL字符串时被调用(默认值:null)。
  • options.nativeBinding:如果您使用复杂的构建系统,可能会遇到问题,因为它可能无法找到其本地C++插件(better_sqlite3.node)。如果您遇到类似于以下的错误,您可以通过使用此选项提供better_sqlite3.node的文件路径(相对于当前工作目录)来解决它。


const Database = require('better-sqlite3');
const db = new Database('foobar.db', { verbose: console.log });


.prepare(string) -> Statement

从给定的SQL字符串创建一个新的已准备好的Statement


const stmt = db.prepare('SELECT name, age FROM cats');


.transaction(function) -> function

创建一个始终在事务中运行的函数。当函数被调用时,它将开始一个新的事务。当函数返回时,事务将被提交。如果抛出异常,事务将被回滚(并且异常将像往常一样传播)。


const insert = db.prepare('INSERT INTO cats (name, age) VALUES (@name, @age)');

const insertMany = db.transaction((cats) => {
  for (const cat of cats) insert.run(cat);
});

insertMany([
  { name: 'Joey', age: 2 },
  { name: 'Sally', age: 4 },
  { name: 'Junior', age: 1 },
]);


事务函数可以从其他事务函数中调用。在这种情况下,内部事务将成为保存点

const newExpense = db.prepare('INSERT INTO expenses (note, dollars) VALUES (?, ?)');

const adopt = db.transaction((cats) => {
  newExpense.run('adoption fees', 20);
  insertMany(cats); // nested transaction
});


事务还具有deferredimmediateexclusive版本。

insertMany(cats); // uses "BEGIN"
insertMany.deferred(cats); // uses "BEGIN DEFERRED"
insertMany.immediate(cats); // uses "BEGIN IMMEDIATE"
insertMany.exclusive(cats); // uses "BEGIN EXCLUSIVE"


传递给事务函数的任何参数都将被转发给包装函数,而从包装函数返回的任何值也将从事务函数返回。包装函数还将访问与事务函数相同的this绑定。


注意事项

如果您希望手动管理事务,可以使用常规预处理语句(使用BEGINCOMMIT等)。然而,手动管理的事务不应该与此.transaction()方法管理的事务混合在一起。换句话说,在使用原始COMMITROLLBACK语句的情况下,不应在事务函数中使用。


事务函数不支持异步函数。技术上讲,异步函数总是在第一个await之后返回,这意味着事务将在任何异步代码执行之前就已经提交了。此外,由于SQLite3序列化所有事务,因此通常将事务保持打开跨越事件循环刻度线是一个非常糟糕的想法。


重要的是要知道,SQLite3有时可能会在我们没有要求的情况下回滚事务。这可能发生在ON CONFLICT子句、RAISE()触发器函数或某些错误(如SQLITE_FULLSQLITE_BUSY)中。换句话说,如果您在事务中捕获到SQLite3错误,您必须意识到您执行的任何进一步SQL可能不在同一事务中。通常,在这种情况下的最佳做法是简单地重新抛出错误,退出事务函数。


try {
  ...
} catch (err) {
  if (!db.inTransaction) throw err; // (transaction was forcefully rolled back)
  ...
}


.pragma(string, [options]) -> results

执行给定的PRAGMA并返回其结果。默认情况下,返回值将是一个结果行的数组。每一行由一个对象表示,其键对应于列名。

由于大多数PRAGMA语句只返回一个值,因此提供了simple选项以简化操作。当simpletrue时,只有第一行的第一列将被返回。


db.pragma('cache_size = 32000');
console.log(db.pragma('cache_size', { simple: true })); // => 32000


如果PRAGMA执行失败,将抛出一个Error。


使用此方法而不是普通的预编译语句执行PRAGMA会更好,因为此方法会规范化一些可能否则遇到的奇怪行为。


.backup(destination, [options]) -> promise

初始化数据库的备份,返回一个promise,当备份完成时该promise将被解析。如果备份失败,promise将被拒绝并带有一个Error。您可以通过将attached选项设置为所需附加数据库的名称来选择性地备份附加数据库。备份文件只是一个常规的SQLite3数据库文件。它可以通过new Database()像任何SQLite3数据库一样打开。


db.backup(`backup-${Date.now()}.db`)
  .then(() => {
    console.log('backup complete!');
  })
  .catch((err) => {
    console.log('backup failed:', err);
  });


在备份进行期间,您可以继续使用数据库。如果在同一时间对数据库进行备份时,不同的连接正在更改数据库,这些更改将自动反映在备份中。然而,如果在备份期间,不同的连接正在更改数据库,备份将被强制重新启动。因此,建议仅在执行在线备份时,如果只有一个连接负责更改数据库,才负责更改数据库。


您可以通过设置progress选项为回调函数来监控备份的进度。该函数将在每次备份进展时被调用,提供两个属性的对象:

  • .totalPages:在进度报告时的源数据库(以及完成备份时的)的总页数。
  • .remainingPages:在备份完成之前必须传输的页数。


默认情况下,每轮事件循环后将传输100个页面。但是,您可以随时通过从progress回调返回数字来更改此设置。您甚至可以返回0以有效地暂停整个备份。总的来说,目标是最大化吞吐量,同时减少暂停时间。如果传输大小非常低,暂停时间将很低,但可能需要一段时间才能完成备份。另一方面,如果设置过高,暂停时间将更大,但备份可能会更快完成。在大多数情况下,100已被证明是一种强大的折衷方案,但最佳设置取决于您的计算机规格和程序的性质。不要在不测量更改效果的情况下更改此设置。您不应该假设您的更改将产生预期的效果,除非您为此特定情况进行了测量。


如果备份成功,返回的promise将包含与提供给progress回调相同的对象的属性,但.remainingPages将为0。如果progress回调抛出异常,备份将被中止。这通常由于意外错误发生,但也可以使用这种行为来有意取消备份操作。如果父数据库连接关闭,所有挂起的备份都将自动中止。

let paused = false;
db.backup(`backup-${Date.now()}.db`, {
  progress({ totalPages: t, remainingPages: r }) {
    console.log(`progress: ${((t - r) / t * 100).toFixed(1)}%`);
    return paused ? 0 : 200;
  }
});


.serialize([options]) -> Buffer

返回包含数据库序列化内容的缓冲区。您可以通过将attached选项设置为所需附加数据库的名称来选择性地序列化附加数据库。


返回的缓冲区可以写入磁盘以创建常规SQLite3数据库文件,或直接作为内存数据库打开,将其传递给new Database()

const buffer = db.serialize();
db.close();
db = new Database(buffer);


.function(name, [options], function) -> this

注册一个用户定义的function,以便它可以用于SQL语句。

db.function('add2', (a, b) => a + b);

db.prepare('SELECT add2(?, ?)').pluck().get(12, 4); // => 16
db.prepare('SELECT add2(?, ?)').pluck().get('foo', 'bar'); // => "foobar"
db.prepare('SELECT add2(?, ?, ?)').pluck().get(12, 4, 18); // => Error: wrong number of arguments


默认情况下,用户定义的函数具有严格的参数数量(由function.length确定)。您可以注册具有相同名称的不同参数数量的多个函数,导致SQLite3根据传递到它的参数数量执行不同的函数。如果注册了两个具有相同名称和相同参数数量的函数,则第二个注册将覆盖第一个。


如果options.varargstrue,注册的函数可以接受任意数量的参数。

如果options.directOnlytrue,注册的函数只能在顶级SQL中使用,不能用于VIEWsTRIGGERs或诸如CHECK constraintsDEFAULT clauses等模式结构。


如果您的函数是确定性的,您可以将options.deterministic设置为true,这可能会在某些情况下提高性能。


db.function('void', { deterministic: true, varargs: true }, () => {});

db.prepare("SELECT void()").pluck().get(); // => null
db.prepare("SELECT void(?, ?)").pluck().get(55, 19); // => null


.aggregate(name, options) -> this

注册一个用户自定义的聚合函数。


db.aggregate('addAll', {
  start: 0,
  step: (total, nextValue) => total + nextValue,
});

db.prepare('SELECT addAll(dollars) FROM expenses').pluck().get(); // => 92


step() 函数将在每次传递给聚合的行上调用一次,并使用其返回值作为新的聚合值。这与 Array#reduce() 的工作方式类似。


如果 options.start 是一个函数,它将在每个聚合开始时调用,并使用其返回值作为初始聚合值。如果 options.start 不是函数,它将原样用作初始聚合值(如上例所示)。如果没有提供,初始聚合值将为 null


您还可以提供一个 result() 函数来转换最终的聚合值:

db.aggregate('getAverage', {
  start: () => [],
  step: (array, nextValue) => {
    array.push(nextValue);
  },
  result: array => array.reduce(sum) / array.length,
});

db.prepare('SELECT getAverage(dollars) FROM expenses').pluck().get(); // => 20.2


如上所示,您可以使用任意 JavaScript 对象作为您的聚合上下文,只要最后由 result() 返回的有效 SQLite3 值有效。如果 step() 不返回任何内容(undefined),则聚合值不会被替换(在使用返回 undefined 当需要 null 时返回 undefined 的函数时要小心)。


就像常规的用户自定义函数一样,用户可以定义的聚合可以接受多个参数。此外,options.varargsoptions.directOnlyoptions.deterministic 也接受。


如果您提供了一个 inverse() 函数,聚合可以用作窗口函数。其中 step() 用于向当前窗口添加一行,inverse() 用于从当前窗口中删除一行。在使用窗口函数时,result() 可能会被多次调用。

db.aggregate('addAll', {
  start: 0,
  step: (total, nextValue) => total + nextValue,
  inverse: (total, droppedValue) => total - droppedValue,
  result: total => Math.round(total),
});

db.prepare(`
  SELECT timestamp, dollars, addAll(dollars) OVER day as dayTotal
  FROM expenses
  WINDOW day AS (PARTITION BY date(timestamp))
  ORDER BY timestamp
`).all();


.table(name, definition) -> this

注册一个虚拟表。虚拟表可以像真实表一样查询,但它们的结果不在数据库文件中;相反,它们是通过JavaScript中的生成器函数实时计算的。

const fs = require('fs');

db.table('filesystem_directory', {
  columns: ['filename', 'data'],
  rows: function* () {
    for (const filename of fs.readdirSync(process.cwd())) {
      const data = fs.readFileSync(filename);
      yield { filename, data };
    }
  },
});

const files = db.prepare('SELECT * FROM filesystem_directory').all();
// => [{ filename, data }, { filename, data }]


要生成虚拟表中的一行,你可以生成一个对象,其键对应列名,或者生成一个数组,其元素表示按声明顺序排列的列。每个虚拟表都必须通过columns选项声明其列。

虚拟表可以像表值函数一样使用;与常规表不同,您可以向它们传递参数。

db.table('regex_matches', {
  columns: ['match', 'capture'],
  rows: function* (pattern, text) {
    const regex = new RegExp(pattern, 'g');
    let match;

    while (match = regex.exec(text)) {
      yield [match[0], match[1]];
    }
  },
});

const stmt = db.prepare("SELECT * FROM regex('\\$(\\d+)', ?)");

stmt.all('Desks cost $500 and chairs cost $27');
// => [{ match: '$500', capture: '500' }, { match: '$27', capture: '27' }]


默认情况下,虚拟表接受的参数数量由function.length推断,参数会自动命名为$1$2等。但是,您可以通过可选的parameters选项显式提供参数列表。

db.table('regex_matches', {
  columns: ['match', 'capture'],
  parameters: ['pattern', 'text'],
  rows: function* (pattern, text) {
    ...
  },
});


在虚拟表中,参数实际上是隐藏列,它们可以在查询结果集中选择,就像其他列一样。这就是为什么有时给它们显式命名是有益的。

当查询虚拟表时,任何省略的参数都将为undefined。您可以利用这种行为实现必需的参数和默认参数值。

db.table('sequence', {
  columns: ['value'],
  parameters: ['length', 'start'],
  rows: function* (length, start = 0) {
    if (length === undefined) {
      throw new TypeError('missing required parameter "length"');
    }

    const end = start + length;
    for (let n = start; n < end; ++n) {
      yield { value: n };
    }
  },
});

db.prepare('SELECT * FROM sequence(10)').pluck().all();
// => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]


注意,在使用类似于start = 0的默认参数值(如上所示)时,函数的.length属性不包括可选参数,因此在这种情况下需要显式声明parameters


通常,当你注册虚拟表时,虚拟表会自动存在,无需运行CREATE VIRTUAL TABLE语句。然而,如果你提供一个工厂函数作为第二个参数(一个返回虚拟表定义的函数),那么不会自动创建虚拟表。相反,你可以运行多个类似的CREATE VIRTUAL TABLE语句来创建多个虚拟表,每个都有自己的模块参数。这就像是定义了一个可以实例化的虚拟表“类”,可以通过运行CREATE VIRTUAL TABLE语句来实例化它。

const fs = require('fs');

db.table('csv', (filename) => {
  const firstLine = getFirstLineOfFile(filename);
  return {
    columns: firstLine.split(','),
    rows: function* () {
      // This is just an example. Real CSV files are more complicated to parse.
      const contents = fs.readFileSync(filename, 'utf8');
      for (const line of contents.split('\n')) {
        yield line.split(',');
      }
    },
  };
});

db.exec('CREATE VIRTUAL TABLE my_data USING csv(my_data.csv)');
const allData = db.prepare('SELECT * FROM my_data').all();


工厂函数将在每次运行相应的CREATE VIRTUAL TABLE语句时被调用。工厂函数的参数对应于在CREATE VIRTUAL TABLE语句中传递的模块参数;始终是一个由任意字符串组成的逗号分隔列表。您负责解析和解释这些模块参数。请注意,SQLite3不允许在模块参数中使用绑定参数。

就像用户自定义函数和用户自定义聚合一样,虚拟表支持options.directOnly,以防止表在VIEWsTRIGGERs或诸如CHECK约束DEFAULT子句等模式结构中使用。

一些扩展可以为具有写能力的虚拟表提供虚拟表,但db.table()只能创建只读虚拟表,主要用于支持表值函数。


以下是关于better-sqlite3库中db.loadExtension()db.exec()db.close()方法的说明:

db.loadExtension(*path*, [*entryPoint*]) -> *this*

该方法用于加载一个已编译的SQLite3扩展,并将其应用于当前数据库连接。

您有责任确保所加载的扩展与SQLite3的兼容版本进行了编译/链接。请记住,better-sqlite3会定期使用较新的SQLite3版本。


示例代码:

db.loadExtension('./my-extensions/compress.so');


db.exec(*string*) -> *this*

该方法用于执行给定的SQL字符串。与预处理语句不同,它可以执行包含多个SQL语句的字符串。相比于使用预处理语句,此函数的性能较差且安全性较低。仅当需要从外部源(通常是文件)执行SQL时,才应使用此方法。如果发生错误,执行将停止,并且不会执行进一步的语句。您必须手动回滚更改。


示例代码:

const migration = fs.readFileSync('migrate-schema.sql', 'utf8');
db.exec(migration);


db.close() -> *this*

该方法用于关闭数据库连接。调用此方法后,无法创建或执行任何语句。


示例代码:

process.on('exit', () => db.close());
process.on('SIGHUP', () => process.exit(128 + 1));
process.on('SIGINT', () => process.exit(128 + 2));
process.on('SIGTERM', () => process.exit(128 + 15));


属性

.open -> boolean - 表示数据库连接当前是否打开。

.inTransaction -> boolean - 表示数据库连接当前是否处于打开的事务中。

.name -> string - 用于打开数据库连接的字符串。

.memory -> boolean - 表示数据库是内存数据库还是临时数据库。

.readonly -> boolean - 表示数据库连接是否以只读模式创建。