はじめてのNode.jsとSQLite: 接続・操作ガイド

はい、承知いたしました。「はじめてのNode.jsとSQLite: 接続・操作ガイド」というタイトルで、約5000語の詳細な記事を作成します。


はじめてのNode.jsとSQLite: 接続・操作ガイド

はじめに

プログラミングの世界に足を踏み入れたばかりの方、あるいはサーバーサイド開発やデータベース操作に関心がある方にとって、Node.jsとSQLiteの組み合わせは非常に魅力的な選択肢となります。Node.jsはその軽量性と非同期I/O、JavaScriptという馴染みやすい言語でサーバーサイドアプリケーションを開発できる点から、多くの開発者に利用されています。一方、SQLiteは特別なサーバープロセスを必要とせず、単一のファイルとしてデータベースを管理できる、非常に軽量で組み込みやすいデータベースです。

この二つを組み合わせることで、セットアップが容易で、開発からデプロイまでスムーズに進められるアプリケーションを効率的に開発できます。小規模なユーティリティツール、デスクトップアプリケーションのバックエンド、プロトタイプの開発、あるいは学習目的など、さまざまなシーンでその真価を発揮します。

この記事では、Node.jsを使ってSQLiteデータベースに接続し、基本的なデータ操作(作成、読み込み、更新、削除 – CRUD)を行う方法を、初心者の方にも分かりやすいように詳細に解説します。非同期処理の扱いやエラーハンドリングといった重要なトピックにも触れ、実践的なコード例を豊富に提供します。

この記事を読むことで、以下のことができるようになります。

  • Node.js環境とSQLiteの基本を理解する
  • Node.jsプロジェクトにSQLiteライブラリをインストールする
  • SQLiteデータベースファイルを作成・接続する
  • テーブルを作成する
  • データの挿入、取得、更新、削除を行う
  • 非同期処理やエラーハンドリングを適切に行う
  • より実践的なコードを書くためのパターンを知る(Promise/async/awaitなど)

前提知識としては、JavaScriptの基本的な構文と、コマンドライン操作の基本的な理解があれば十分です。さあ、Node.jsとSQLiteの世界へ一緒に踏み出しましょう。

1. 前提知識と環境構築

1.1 前提知識

この記事を進めるにあたり、以下の知識があるとスムーズに理解できます。

  • JavaScriptの基本的な構文: 変数、データ型、関数、条件分岐、ループ処理など。
  • コマンドライン操作: ディレクトリ移動(cd)、ファイル作成(touchやエディタの使用)、コマンド実行など。
  • 非同期処理の概念: JavaScriptにおける非同期処理(コールバック、Promiseなど)の基本的な考え方。データベース操作はI/O処理であり、通常非同期で行われます。
  • SQLの基本的な概念: SELECT, INSERT, UPDATE, DELETEといった基本的なデータベース操作コマンドの存在を知っていると、SQLiteの操作が理解しやすくなります。この記事でも基本的なSQLについては触れますが、より深く理解したい場合は別途学習することをおすすめします。

1.2 Node.jsのインストール

Node.jsはJavaScriptをブラウザの外(サーバーサイドやコマンドラインツールなど)で実行するためのランタイム環境です。公式ウェブサイトからインストーラーをダウンロードする方法が最も手軽です。

  1. Node.js公式サイトへアクセス: https://nodejs.org/
  2. トップページに表示されている推奨版(LTS版)のインストーラーをダウンロードします。
  3. ダウンロードしたインストーラーを実行し、画面の指示に従ってインストールを完了させます。特別な設定変更は不要な場合が多いです。

インストールが完了したら、正しくインストールされたか確認しましょう。コマンドプロンプトまたはターミナルを開き、以下のコマンドを実行します。

bash
node -v
npm -v

それぞれNode.jsとnpm(Node.jsのパッケージマネージャー)のバージョンが表示されれば成功です。npmはNode.jsと一緒にインストールされます。

より柔軟にNode.jsのバージョンを管理したい場合は、nvm (Node Version Manager) のようなツールを利用するのも良い方法です。これにより、複数のNode.jsバージョンを切り替えて使用できます。

1.3 npm/yarnの基本

npm (Node Package Manager) は、Node.jsのパッケージ(ライブラリやツール)を管理するための標準的なツールです。この記事ではnpmを主に使用しますが、yarnという別のパッケージマネージャーも広く使われています。基本的な使い方は似ています。

  • パッケージのインストール: プロジェクトに必要なライブラリなどをインストールします。
    bash
    npm install パッケージ名
  • 依存関係の管理: プロジェクトのルートディレクトリに作成される package.json ファイルに、プロジェクトが依存するパッケージとそのバージョン情報が記録されます。これにより、他の開発者と同じ環境を簡単に再現できます。
  • スクリプトの実行: package.json に定義したスクリプトを実行できます(例: テストの実行、アプリケーションの起動など)。
    bash
    npm run スクリプト名

この記事では主に npm install を使用します。

1.4 エディタの準備

コードを書くためのテキストエディタが必要です。JavaScript開発に適したエディタとして、Visual Studio Code (VS Code) が非常におすすめです。無料で高機能、多くの拡張機能があり、Node.js開発を強力にサポートしてくれます。

もちろん、お好みのエディタ(Sublime Text, Atom, WebStormなど)を使用しても構いません。

2. SQLiteの基本

2.1 SQLiteの仕組み

SQLiteは、リレーショナルデータベース管理システム (RDBMS) の一種ですが、一般的なRDBMS(MySQL, PostgreSQL, SQL Serverなど)とは異なり、サーバープロセスを持ちません。データベース全体が単一のファイルとしてディスク上に保存されます。

この「サーバーレス」な設計により、以下のような特徴があります。

  • セットアップが不要: データベースサーバーのインストールや設定、起動・停止といった手間がありません。
  • 軽量: リソース消費が非常に少ないです。
  • 組み込みやすい: アプリケーションの一部として簡単に組み込めます。
  • 移植性が高い: データベースファイル自体をコピーするだけで、他の環境へ簡単に移動できます。

一方で、大規模な同時書き込みが多いシステムや、高い可用性が求められるシステムには向いていません。しかし、小規模なアプリケーション、組み込みシステム、オフラインで使用するアプリケーション、開発時のデータストアなど、多くの用途で非常に有用です。

2.2 SQLiteのデータ型

SQLiteは「厳密なデータ型」を持つ他のRDBMSとは少し異なり、「動的な型付け」に近い性質を持っています。値を保存する際にその値の型を内部的に判断しますが、カラム定義で指定できるデータ型は「Storage Class」と呼ばれ、以下の5つが基本です。

  • NULL: NULL値。
  • INTEGER: 符号付き整数。1, 2, 3, 4, 6, または8バイトで格納されます。
  • REAL: 浮動小数点数。8バイトのIEEE浮動小数点数として格納されます。
  • TEXT: 文字列。データベースエンコーディング(UTF-8, UTF-16など)を使って格納されます。
  • BLOB: バイナリデータ。入力されたそのままの形式で格納されます。

カラムを定義する際に INT, VARCHAR, DOUBLE, BOOLEAN, DATE などの型名を指定することもできますが、これらは「アフィニティ型」と呼ばれ、SQLiteは指定された型名に基づいて上記のStorage Classのいずれかにデータを格納しようとします。例えば INTINTEGERINTEGER Storage Class、VARCHARTEXTTEXT Storage Class、DOUBLEREALREAL Storage Classに対応します。この柔軟性は時に便利ですが、他のRDBMSから移行する際などは注意が必要です。

2.3 基本的なSQLコマンド

SQLiteも標準的なSQL(Structured Query Language)を使用して操作します。Node.jsから操作する際にも、これらのSQLコマンドを文字列として組み立ててライブラリに渡します。

  • データベースの作成/接続: SQLiteは指定したデータベースファイルが存在しない場合は自動的に作成します。
  • テーブルの作成: CREATE TABLE
    sql
    CREATE TABLE テーブル名 (
    カラム1名 データ型 [制約],
    カラム2名 データ型 [制約],
    ...
    );

    例:
    sql
    CREATE TABLE users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    age INTEGER
    );

    • PRIMARY KEY: 主キーとして設定。レコードを一意に識別するためのカラム。
    • AUTOINCREMENT: INTEGER PRIMARY KEY に設定すると、自動的にユニークな連番が割り振られます。
    • NOT NULL: そのカラムがNULL値を持つことを許可しない制約。
  • データの挿入: INSERT INTO
    sql
    INSERT INTO テーブル名 (カラム1, カラム2, ...) VALUES (値1, 値2, ...);

    例:
    sql
    INSERT INTO users (name, age) VALUES ('Alice', 30);
  • データの取得: SELECT
    sql
    SELECT カラム1, カラム2, ... FROM テーブル名 [WHERE 条件] [ORDER BY カラム名 [ASC|DESC]] [LIMIT 数] [OFFSET 数];

    例:
    sql
    SELECT * FROM users; -- 全てのカラムの全レコードを取得
    SELECT name FROM users WHERE age > 25; -- ageが25より大きいユーザーの名前を取得
    SELECT * FROM users ORDER BY age DESC LIMIT 10; -- ageで降順に並べ替えて最初の10件を取得
  • データの更新: UPDATE
    sql
    UPDATE テーブル名 SET カラム1 = 新しい値1, カラム2 = 新しい値2, ... [WHERE 条件];

    例:
    sql
    UPDATE users SET age = 31 WHERE name = 'Alice'; -- Aliceのageを31に更新
  • データの削除: DELETE FROM
    sql
    DELETE FROM テーブル名 [WHERE 条件];

    例:
    sql
    DELETE FROM users WHERE name = 'Alice'; -- Aliceのレコードを削除
    DELETE FROM users; -- テーブルの全レコードを削除 (テーブル自体は残る)

2.4 SQLiteクライアントツール

Node.jsからの操作だけでなく、手動でデータベースの内容を確認したり、SQLクエリを実行したりするために、SQLiteクライアントツールがあると便利です。

  • DB Browser for SQLite: GUIでSQLiteデータベースファイルを開き、テーブル構造の確認、データの閲覧・編集、SQLクエリの実行などが簡単に行えます。無料で利用でき、Windows, macOS, Linuxに対応しています。

これらのツールを使って、Node.jsスクリプトで作成・操作したデータベースファイルの中身を確認しながら学習を進めることをお勧めします。

3. Node.jsからSQLiteを扱うためのライブラリ

Node.jsからSQLiteデータベースを操作するためには、専用のライブラリ(パッケージ)が必要です。いくつかの選択肢がありますが、最も一般的で広く使われているのは sqlite3 です。

3.1 sqlite3 ライブラリ

sqlite3 は、Node.jsの慣習に沿った非同期APIを提供します。ネイティブコード(C++)で実装されているため、高速なデータベースアクセスが可能です。GitHubで活発に開発されており、多くのNode.jsプロジェクトで採用されています。

  • 特徴:

    • 非同期API: I/O処理をブロックしないため、Node.jsのイベントループを最大限に活用できます。
    • 軽量: 依存関係が少なく、インストールが容易です。
    • SQLインジェクション対策: プレースホルダをサポートしており、安全なクエリ実行が可能です。
    • トランザクション対応。
  • 注意点:

    • 非同期APIが基本のため、処理の流れを理解する必要があります(コールバック、Promiseなど)。
    • ネイティブモジュールを含むため、インストール時にコンパイルが必要になる場合があります(Node.jsのバージョンやOS環境によっては追加のビルドツールが必要なこともあります)。

3.2 他のライブラリとの比較 (better-sqlite3 など)

sqlite3 以外にもSQLiteを扱うライブラリは存在します。代表的なものに better-sqlite3 があります。

  • better-sqlite3:
    • 同期API: こちらは同期でデータベース操作を行います。これはNode.jsの一般的な非同期パターンとは異なりますが、スクリプトの実行フローが直線的になるため、シンプルなツールやスクリプトでは記述が容易になる場合があります。ただし、サーバーアプリケーションなど、多数の同時リクエストを処理する必要がある場面では、同期I/Oはイベントループをブロックするためパフォーマンス上の問題を引き起こす可能性があります。
    • 高速性: sqlite3 と比較して、特定の操作でより高速な場合があります。
    • シンプルなAPI。

なぜこの記事では sqlite3 を選ぶのか:

sqlite3 はNode.jsの標準的な非同期プログラミングパラダイムに合致しており、ウェブアプリケーションやAPIなど、非同期処理が求められる多くのNode.jsアプリケーション開発に適しています。初めてデータベース操作を学ぶ上で、Node.jsの非同期処理の扱い方を同時に学ぶことができるため、より実践的なスキルが身につくと考えられるからです。もちろん、better-sqlite3 も有効な選択肢ですが、この記事では非同期に焦点を当てて sqlite3 を詳しく解説します。

4. プロジェクトのセットアップ

それでは、Node.jsプロジェクトを作成し、sqlite3 ライブラリをインストールしましょう。

  1. プロジェクトディレクトリの作成:
    bash
    mkdir nodejs-sqlite-guide
    cd nodejs-sqlite-guide

  2. 新しいNode.jsプロジェクトの初期化:
    bash
    npm init -y

    -y オプションを付けると、対話式の質問に全てデフォルトで答えて package.json ファイルを生成します。プロジェクト名、バージョン、エントリポイントなどが設定されます。

  3. sqlite3 ライブラリのインストール:
    bash
    npm install sqlite3

    このコマンドを実行すると、sqlite3 ライブラリがダウンロードされ、プロジェクトの node_modules ディレクトリに保存されます。また、package.jsondependencies セクションに sqlite3 が追加されます。

これでプロジェクトのセットアップは完了です。次に、データベースへの接続方法を見ていきましょう。

5. SQLiteデータベースへの接続

Node.jsスクリプトからSQLiteを操作するためには、まずデータベースファイルに接続する必要があります。

sqlite3 ライブラリを使ってデータベースに接続するには、sqlite3.Database クラスのインスタンスを作成します。

“`javascript
// index.js または適切なファイル名で作成
const sqlite3 = require(‘sqlite3’).verbose(); // verbose() をつけると詳細なログが出力される(開発時推奨)

// データベースファイルのパスを指定
// ‘:memory:’ を指定するとメモリ上に一時的なデータベースを作成(ファイルは作成されない)
const dbFile = ‘./my_database.sqlite’; // プロジェクトディレクトリに ‘my_database.sqlite’ というファイルが作成される

// Database オブジェクトを作成
// 第一引数: データベースファイルのパス
// 第二引数 (省略可): オープンモード (例: sqlite3.OPEN_READONLY)
// 第三引数: コールバック関数 (エラーが発生した場合に呼ばれる)
const db = new sqlite3.Database(dbFile, (err) => {
if (err) {
// 接続エラーが発生した場合
console.error(‘データベース接続エラー:’, err.message);
} else {
// 接続成功
console.log(‘データベースに接続しました。’);
// ここからデータベース操作を開始する
// 例: db.run(‘CREATE TABLE …’, …);
}
});

// データベース接続が完了してから操作を行う必要があるため、非同期処理を意識する必要がある
// この時点ではまだ接続処理中かもしれない
console.log(‘データベース接続処理を開始しました…’);

// 注意:db.close() は通常、全てのデータベース操作が完了した後に呼び出す
// ここでは例として示しているだけ
// db.close((err) => {
// if (err) {
// console.error(‘データベース切断エラー:’, err.message);
// } else {
// console.log(‘データベース接続を閉じました。’);
// }
// });
“`

コード解説:

  • require('sqlite3').verbose(): sqlite3 ライブラリをインポートします。.verbose() をつけると、実行されるSQLステートメントがコンソールに出力されるようになり、デバッグに役立ちます。本番環境では通常外します。
  • const dbFile = './my_database.sqlite';: データベースファイルのパスを指定します。指定したファイルが存在しない場合は新しく作成されます。ファイルパスではなく :memory: を指定すると、データベースはディスクではなくメモリ上に作成され、スクリプトの実行終了とともに消滅します。テストや一時的な用途に便利です。
  • new sqlite3.Database(dbFile, (err) => { ... }): sqlite3.Database のインスタンスを作成します。コンストラクタの第三引数はコールバック関数です。データベースファイルを開く処理は非同期で行われるため、このコールバック関数はファイルオープンが成功または失敗した後に呼び出されます。
  • コールバック関数の err 引数: ファイルのオープンに失敗した場合(例: 権限がない、パスが不正など)にエラーオブジェクトが渡されます。
  • console.error('データベース接続エラー:', err.message);: エラーが発生した場合、エラーメッセージを出力します。
  • console.log('データベースに接続しました。');: 接続成功時にメッセージを出力します。
  • db.close((err) => { ... }): データベース接続を閉じるメソッドです。これも非同期処理です。通常は、すべてのデータベース操作が完了した後に呼び出します。

重要なポイント:

データベースの接続は非同期で行われるため、new sqlite3.Database(...) の直後にデータベース操作のコードを書いても、接続が完了する前に実行されてしまう可能性があります。データベース操作は、接続成功のコールバック関数の中、あるいは接続完了を待つ別の方法(Promiseなど)を使って行う必要があります。

6. テーブルの作成

データベースにデータを保存するためには、まずテーブルを作成する必要があります。テーブル作成は db.run() メソッドを使用します。

db.run(sql, [param, ...], [callback]) メソッドは、結果セットを返さないSQLコマンド(CREATE TABLE, INSERT, UPDATE, DELETE など)を実行するために使用します。

以下の例では、users という名前のテーブルを作成します。

“`javascript
const sqlite3 = require(‘sqlite3’).verbose();
const dbFile = ‘./my_database.sqlite’;

const db = new sqlite3.Database(dbFile, (err) => {
if (err) {
console.error(‘データベース接続エラー:’, err.message);
return; // エラーが発生したら処理を終了
}
console.log(‘データベースに接続しました。’);

// — ここからテーブル作成処理 —
const createTableSql = CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
age INTEGER
)
;

db.run(createTableSql, (err) => {
if (err) {
console.error(‘テーブル作成エラー:’, err.message);
} else {
console.log(‘テーブル “users” が作成されたか、既に存在します。’);
// テーブル作成が成功したら、次の操作(例: データ挿入)に進む
// 例えば、ここにデータ挿入の関数呼び出しなどを記述する
}

// 全ての操作が完了したらデータベース接続を閉じる(ここでは例としてすぐに閉じている)
// 実際には、すべてのCRUD操作などが終わった後に閉じるべき
db.close((err) => {
  if (err) {
    console.error('データベース切断エラー:', err.message);
  } else {
    console.log('データベース接続を閉じました。');
  }
});

});
// — テーブル作成処理ここまで —
});
“`

コード解説:

  • CREATE TABLE IF NOT EXISTS users (...): SQLコマンドです。IF NOT EXISTS を付けることで、もし users テーブルが既に存在してもエラーにならず、安全にスクリプトを実行できます。
  • id INTEGER PRIMARY KEY AUTOINCREMENT: id カラムを整数型、主キーとし、新しいレコードが追加されるたびに自動的にユニークな整数値が割り振られるように設定しています。
  • name TEXT NOT NULL: name カラムをテキスト型とし、NULL値を許容しないように設定しています。
  • age INTEGER: age カラムを整数型としています。NULLも許容されます。
  • db.run(createTableSql, (err) => { ... }): createTableSql というSQLコマンドを実行します。第二引数はコールバック関数です。SQLの実行が成功または失敗した後に呼び出されます。
  • コールバック関数の err 引数: SQL実行中にエラーが発生した場合にエラーオブジェクトが渡されます。
  • エラーハンドリング: エラーが発生した場合、その詳細をコンソールに出力します。
  • 成功時の処理: テーブル作成が成功した場合のメッセージを出力します。通常、次のデータベース操作はこの成功コールバックの中で開始します。

注意点:

db.run() のコールバック関数は、SQLコマンド自体の実行が完了した時点で呼ばれます。例えば INSERT の場合、挿入処理が完了した時点で呼ばれるという意味です。非同期処理のチェーンを管理するために、このコールバックのネストが深くなる(いわゆる「コールバック地獄」)傾向があります。これを避けるための方法については後述します(Promise/async/await)。

7. データの挿入 (INSERT)

テーブルが作成できたら、いよいよデータを挿入してみましょう。データの挿入も db.run() メソッドを使用します。SQLコマンドは INSERT INTO です。

SQLインジェクション対策:

SQLクエリの中に直接ユーザーからの入力を埋め込むと、SQLインジェクションというセキュリティ脆弱性の原因になります。これを防ぐために、sqlite3 ライブラリは「プレースホルダ」の使用を強く推奨しています。プレースホルダを使うと、SQLコマンドの構造と挿入する値を分離できます。ライブラリが値を適切にエスケープするため、安全にデータを扱えます。

sqlite3 で使えるプレースホルダにはいくつか種類があります。

  • ?: 匿名プレースホルダ。渡す値の配列の順序に対応します。
  • $name, @name, :name: 名前付きプレースホルダ。オブジェクトのキー名に対応します。

ここでは匿名プレースホルダ ? を使った例を示します。

“`javascript
const sqlite3 = require(‘sqlite3’).verbose();
const dbFile = ‘./my_database.sqlite’;

const db = new sqlite3.Database(dbFile, (err) => {
if (err) {
console.error(‘データベース接続エラー:’, err.message);
return;
}
console.log(‘データベースに接続しました。’);

// テーブル作成(前述のコードを省略)
const createTableSql = ...; // 省略
db.run(createTableSql, (err) => {
if (err) {
console.error(‘テーブル作成エラー:’, err.message);
db.close(); return;
}
console.log(‘テーブル “users” が作成されたか、既に存在します。’);

// --- ここからデータ挿入処理 ---
const insertSql = `INSERT INTO users (name, age) VALUES (?, ?)`;
const user1 = ['Bob', 25];
const user2 = ['Charlie', 35];

// 単一データの挿入
db.run(insertSql, user1, function(err) { // function() を使うと this.lastID, this.changes にアクセスできる
  if (err) {
    console.error('データ挿入エラー (Bob):', err.message);
  } else {
    console.log(`データが挿入されました (Bob, ID: ${this.lastID})`);
    // 挿入成功後、さらに別の操作(例: 別のデータを挿入)を行う
    db.run(insertSql, user2, function(err) {
      if (err) {
        console.error('データ挿入エラー (Charlie):', err.message);
      } else {
        console.log(`データが挿入されました (Charlie, ID: ${this.lastID})`);
      }

      // 全ての挿入が終わったら接続を閉じる
      db.close((err) => {
        if (err) {
          console.error('データベース切断エラー:', err.message);
        } else {
          console.log('データベース接続を閉じました。');
        }
      });
    });
  }
});
// --- データ挿入処理ここまで ---

});
});
“`

コード解説:

  • const insertSql =INSERT INTO users (name, age) VALUES (?, ?):: ? がプレースホルダです。挿入したい値の数だけ ? を記述します。
  • const user1 = ['Bob', 25];: プレースホルダ ? に対応する値を配列で用意します。配列の順番が ? の出現順と一致している必要があります。
  • db.run(insertSql, user1, function(err) { ... }): insertSql を実行し、user1 の配列をプレースホルダの値として渡します。コールバック関数は function(err) の形式で定義しています。通常の矢印関数 (err) => ではなく function キーワードを使うことで、コールバック関数内で this オブジェクトにアクセスできるようになります。
  • this.lastID: 最後に挿入された行の AUTOINCREMENT されたIDを取得できます(INTEGER PRIMARY KEY AUTOINCREMENT が設定されている場合)。
  • this.changes: 最後に実行された INSERT, UPDATE, DELETE コマンドによって変更された行数を取得できます。
  • 非同期処理のネスト: 例では、Bobの挿入成功コールバックの中でCharlieの挿入を行っています。このように非同期操作を順番に実行するには、コールバックをネストしていく必要があります。これが「コールバック地獄」の典型例であり、コードが読みにくくなる原因となります。

複数データの挿入とトランザクション

複数のデータをまとめて挿入する場合、一つずつ db.run() を非同期で実行すると、挿入順が保証されなかったり、エラー発生時に一部だけ挿入されたりする可能性があります。このような場合、トランザクションを使用すると、複数の操作を一つのまとまり(アトミックな単位)として実行できます。トランザクション内の全ての操作が成功すればコミットされ、一つでも失敗すればロールバック(全て取り消し)されます。

sqlite3 でトランザクションを使用するには、いくつかの方法がありますが、簡単な方法の一つは db.serialize() メソッドを使うことです。db.serialize() のコールバック関数の中で実行された db.run(), db.get(), db.all(), db.each() といったメソッドは、同期的に(順番に)実行されるようになります。

“`javascript
const sqlite3 = require(‘sqlite3’).verbose();
const dbFile = ‘./my_database.sqlite’;

const db = new sqlite3.Database(dbFile, (err) => {
if (err) {
console.error(‘データベース接続エラー:’, err.message);
return;
}
console.log(‘データベースに接続しました。’);

const createTableSql = ...; // 省略
db.run(createTableSql, (err) => {
if (err) {
console.error(‘テーブル作成エラー:’, err.message);
db.close(); return;
}
console.log(‘テーブル “users” が作成されたか、既に存在します。’);

// --- ここから複数データ挿入 (トランザクション風) ---
const insertSql = `INSERT INTO users (name, age) VALUES (?, ?)`;
const usersToInsert = [
  ['David', 40],
  ['Eve', 22],
  ['Frank', 50]
];

// db.serialize() を使うと、その中のDB操作が順番に実行される
db.serialize(() => {
  // BEGIN TRANSACTION
  db.run('BEGIN TRANSACTION;');

  usersToInsert.forEach(user => {
    db.run(insertSql, user, function(err) {
      if (err) {
        console.error(`データ挿入エラー (${user[0]}):`, err.message);
        // エラーが発生したらロールバックを試みる
        db.run('ROLLBACK;');
        console.log('トランザクションをロールバックしました。');
        // 以降の処理を中断するなど、適切なエラー処理を行う
        // 注意: このコールバック内での return は forEach の反復を止めるだけで、serialize全体を止めるわけではない
        // より堅牢なエラーハンドリングは async/await と try...catch が適している
      } else {
        console.log(`データ挿入成功 (${user[0]}, ID: ${this.lastID})`);
      }
    });
  });

  // COMMIT TRANSACTION (全ての挿入が成功したと仮定してここでコミット)
  // 実際のアプリケーションでは、エラーが発生しなかったことを確認してからコミットする必要がある
  // serialize 内の最後の db.run のコールバックなどで COMMIT するのが一般的
  // ここでは説明のために簡略化
  db.run('COMMIT;', (err) => {
    if (err) {
      console.error('コミットエラー:', err.message);
      // コミット失敗時もロールバックを検討
      db.run('ROLLBACK;');
      console.log('コミット失敗によりロールバックしました。');
    } else {
      console.log('トランザクションをコミットしました。');
    }
  });

  // serialize ブロックの最後に接続を閉じる
  db.close((err) => {
    if (err) {
      console.error('データベース切断エラー:', err.message);
    } else {
      console.log('データベース接続を閉じました。');
    }
  });
});
// --- 複数データ挿入ここまで ---

});
});
“`

コード解説:

  • db.serialize(() => { ... }): このブロック内のデータベース操作は順番に実行されます。ただし、各 db.run() 自体は非同期処理なので、forEach ループはすぐに完了し、各 db.run のコールバックが順次呼ばれます。
  • db.run('BEGIN TRANSACTION;');: トランザクションを開始するSQLコマンドを実行します。
  • db.run(insertSql, user, function(err) { ... }): ループ内で各ユーザーデータを挿入します。
  • db.run('ROLLBACK;');: エラーが発生した場合、ROLLBACK コマンドを実行してトランザクション開始以降の全ての変更を取り消します。
  • db.run('COMMIT;');: 全ての操作が成功したと見なせる場合に COMMIT コマンドを実行し、トランザクション内の変更を確定させます。

注意点:

db.serialize() は実行順序を保証しますが、個々の非同期操作の完了を「待つ」わけではありません。例えば、forEach ループの直後に COMMIT を書くと、まだ挿入処理が完了していないのにコミットされてしまう可能性があります。したがって、すべての挿入操作が成功したことを確認してから COMMIT を実行するためには、最後の db.run のコールバック関数内で COMMIT を実行するなどの工夫が必要です。これはコールバックベースの非同期処理の複雑さを示す例です。

8. データの取得 (SELECT)

データベースからデータを取得するには、SELECT コマンドを使用します。sqlite3 ライブラリは、データの取得用にいくつかのメソッドを提供しています。

  • db.get(sql, [param, ...], [callback]): クエリ結果の最初の1行を取得する場合に使用します。
  • db.all(sql, [param, ...], [callback]): クエリ結果の全ての行を配列として取得する場合に使用します。
  • db.each(sql, [param, ...], row_callback, [complete_callback]): クエリ結果の各行を処理する場合に使用します。行ごとに row_callback が呼ばれ、全ての行の処理が完了した後に complete_callback が呼ばれます。大量のデータを扱う場合にメモリ効率が良いですが、各行の処理が非同期になる可能性があり注意が必要です。

これらのメソッドも非同期であり、結果はコールバック関数で受け取ります。

8.1 db.get() で単一行を取得

“`javascript
const sqlite3 = require(‘sqlite3’).verbose();
const dbFile = ‘./my_database.sqlite’;

const db = new sqlite3.Database(dbFile, (err) => {
if (err) {
console.error(‘データベース接続エラー:’, err.message);
return;
}
console.log(‘データベースに接続しました。’);

// データ挿入などが完了している前提
// … 以前のテーブル作成・データ挿入コード …

// — ここからデータ取得 (単一行) —
const selectSql = SELECT id, name, age FROM users WHERE name = ?;
const userName = ‘Bob’;

db.get(selectSql, [userName], (err, row) => {
if (err) {
console.error(‘データ取得エラー:’, err.message);
} else if (row) {
// データが見つかった場合、rowはオブジェクト
console.log(取得したデータ: ID=${row.id}, Name=${row.name}, Age=${row.age});
} else {
// データが見つからなかった場合、rowは undefined
console.log(指定された名前 (${userName}) のユーザーは見つかりませんでした。);
}

// 接続を閉じる
db.close((err) => {
  if (err) {
    console.error('データベース切断エラー:', err.message);
  } else {
    console.log('データベース接続を閉じました。');
  }
});

});
// — データ取得ここまで —
});
“`

コード解説:

  • const selectSql =SELECT id, name, age FROM users WHERE name = ?:: SELECT コマンドで id, name, age カラムを取得します。WHERE name = ? で条件を指定し、プレースホルダを使用しています。
  • const userName = 'Bob';: プレースホルダ ? に渡す値を定義します。
  • db.get(selectSql, [userName], (err, row) => { ... }): db.get() メソッドを実行します。第二引数にプレースホルダに対応する値の配列、第三引数にコールバック関数を渡します。
  • コールバック関数の引数 errrow:
    • err: SQL実行中にエラーが発生した場合にエラーオブジェクトが渡されます。
    • row: クエリの結果が1行見つかった場合、その行のデータがオブジェクトとして渡されます(カラム名がプロパティ名になります)。データが見つからなかった場合は undefined となります。
  • データの存在チェック: if (row) でデータが見つかったかどうかを確認します。

8.2 db.all() で複数行を取得

“`javascript
const sqlite3 = require(‘sqlite3’).verbose();
const dbFile = ‘./my_database.sqlite’;

const db = new sqlite3.Database(dbFile, (err) => {
if (err) {
console.error(‘データベース接続エラー:’, err.message);
return;
}
console.log(‘データベースに接続しました。’);

// データ挿入などが完了している前提
// … 以前のテーブル作成・データ挿入コード …

// — ここからデータ取得 (複数行) —
const selectAllSql = SELECT id, name, age FROM users WHERE age >= ? ORDER BY age ASC;
const minAge = 25;

db.all(selectAllSql, [minAge], (err, rows) => {
if (err) {
console.error(‘データ取得エラー:’, err.message);
} else if (rows.length > 0) {
// データが見つかった場合、rowsはオブジェクトの配列
console.log(取得したデータ (${rows.length}件):);
rows.forEach(row => {
console.log(- ID=${row.id}, Name=${row.name}, Age=${row.age});
});
} else {
// データが見つからなかった場合、rowsは空の配列 []
console.log(指定された条件 (${minAge}歳以上) のユーザーは見つかりませんでした。);
}

// 接続を閉じる
db.close((err) => {
  if (err) {
    console.error('データベース切断エラー:', err.message);
  } else {
    console.log('データベース接続を閉じました。');
  }
});

});
// — データ取得ここまで —
});
“`

コード解説:

  • const selectAllSql =SELECT id, name, age FROM users WHERE age >= ? ORDER BY age ASC:: age が指定された値以上のユーザーを、age の昇順で取得するSQLコマンドです。
  • db.all(selectAllSql, [minAge], (err, rows) => { ... }): db.all() メソッドを実行します。
  • コールバック関数の引数 errrows:
    • err: SQL実行中にエラーが発生した場合。
    • rows: クエリの結果がオブジェクトの配列として渡されます。結果がない場合は空の配列 [] となります。
  • rows.forEach(...): 取得した各行をループ処理してコンソールに出力しています。

8.3 db.each() で行ごとに処理

db.each() は、取得件数が多い場合に、メモリを一度に大量に消費するのを避けるために使用できます。ただし、コールバックベースであることに加え、行処理のコールバックが非同期的に呼ばれる可能性がある(ドキュメントでは「順不同」と記載されているが、実際は順番に呼ばれることが多い)ため、扱いが少し複雑になることがあります。

“`javascript
const sqlite3 = require(‘sqlite3’).verbose();
const dbFile = ‘./my_database.sqlite’;

const db = new sqlite3.Database(dbFile, (err) => {
if (err) {
console.error(‘データベース接続エラー:’, err.message);
return;
}
console.log(‘データベースに接続しました。’);

// データ挿入などが完了している前提
// … 以前のテーブル作成・データ挿入コード …

// — ここからデータ取得 (each) —
const selectSql = SELECT id, name, age FROM users;
let rowCount = 0;

db.each(selectSql, (err, row) => { // 各行が見つかるたびに呼ばれるコールバック
if (err) {
console.error(‘行処理エラー:’, err.message);
// each 内でのエラーは complete_callback を呼ばずに終了することがあるため注意
} else {
rowCount++;
console.log(処理中の行 (${rowCount}): ID=${row.id}, Name=${row.name}, Age=${row.age});
// ここで行ごとの処理を行う
}
}, (err, numRows) => { // 全ての行の処理が完了した後に呼ばれるコールバック
if (err) {
console.error(‘最終処理エラー:’, err.message);
} else {
console.log(--- 全${numRows}行の処理が完了しました ---);
}

// 接続を閉じる
db.close((err) => {
  if (err) {
    console.error('データベース切断エラー:', err.message);
  } else {
    console.log('データベース接続を閉じました。');
  }
});

});
// — データ取得ここまで —
});
“`

コード解説:

  • db.each(selectSql, row_callback, complete_callback): 第一引数にSQL、第二引数に各行を処理するコールバック、第三引数に全ての行の処理が完了した後に呼ばれるコールバックを渡します。
  • row_callback (err, row): 各行のデータ (row) とエラー (err) を受け取ります。
  • complete_callback (err, numRows): 全ての行を処理し終えた後、あるいは途中でエラーが発生した場合に呼ばれます。err には最終的なエラー(なければnull)、numRows には処理された行数(エラーの場合は不正確なことがある)が渡されます。

db.each() は、データを全てメモリにロードすることなく処理できるため、大規模なデータセットを扱う場合に有効です。ただし、各行の処理内で非同期操作を行う場合は、その完了を待つ仕組みを別途用意する必要があります。

9. データの更新 (UPDATE)

既存のレコードのデータを変更するには UPDATE コマンドを使用します。データの更新も db.run() メソッドを使用します。

“`javascript
const sqlite3 = require(‘sqlite3’).verbose();
const dbFile = ‘./my_database.sqlite’;

const db = new sqlite3.Database(dbFile, (err) => {
if (err) {
console.error(‘データベース接続エラー:’, err.message);
return;
}
console.log(‘データベースに接続しました。’);

// データ挿入などが完了している前提
// … 以前のテーブル作成・データ挿入コード …

// — ここからデータ更新処理 —
const updateSql = UPDATE users SET age = ? WHERE name = ?;
const newAge = 32;
const userNameToUpdate = ‘Bob’;

db.run(updateSql, [newAge, userNameToUpdate], function(err) {
if (err) {
console.error(‘データ更新エラー:’, err.message);
} else {
// this.changes で更新された行数が取得できる
if (this.changes > 0) {
console.log(${this.changes}件のレコードが更新されました (${userNameToUpdate}のageを${newAge}に)。);
} else {
console.log(指定された名前 (${userNameToUpdate}) のユーザーは見つからず、更新されませんでした。);
}
}

// 接続を閉じる
db.close((err) => {
  if (err) {
    console.error('データベース切断エラー:', err.message);
  } else {
    console.log('データベース接続を閉じました。');
  }
});

});
// — データ更新処理ここまで —
});
“`

コード解説:

  • const updateSql =UPDATE users SET age = ? WHERE name = ?:: age カラムの値を更新し、name カラムが指定された値に一致するレコードを対象とするSQLコマンドです。プレースホルダを2つ使用しています。
  • db.run(updateSql, [newAge, userNameToUpdate], function(err) { ... }): db.run() メソッドを実行します。プレースホルダに対応する値は配列で渡します。順序が重要です(最初の ?newAge、二番目の ?userNameToUpdate)。
  • コールバック関数は function(err) の形式で定義し、this.changes で更新された行数を取得しています。

10. データの削除 (DELETE)

不要になったレコードを削除するには DELETE FROM コマンドを使用します。データの削除も db.run() メソッドを使用します。

“`javascript
const sqlite3 = require(‘sqlite3’).verbose();
const dbFile = ‘./my_database.sqlite’;

const db = new sqlite3.Database(dbFile, (err) => {
if (err) {
console.error(‘データベース接続エラー:’, err.message);
return;
}
console.log(‘データベースに接続しました。’);

// データ挿入などが完了している前提
// … 以前のテーブル作成・データ挿入コード …

// — ここからデータ削除処理 —
const deleteSql = DELETE FROM users WHERE name = ?;
const userNameToDelete = ‘Charlie’;

db.run(deleteSql, [userNameToDelete], function(err) {
if (err) {
console.error(‘データ削除エラー:’, err.message);
} else {
// this.changes で削除された行数が取得できる
if (this.changes > 0) {
console.log(${this.changes}件のレコードが削除されました (${userNameToDelete})。);
} else {
console.log(指定された名前 (${userNameToDelete}) のユーザーは見つからず、削除されませんでした。);
}
}

// 接続を閉じる
db.close((err) => {
  if (err) {
    console.error('データベース切断エラー:', err.message);
  } else {
    console.log('データベース接続を閉じました。');
  }
});

});
// — データ削除処理ここまで —
});
“`

コード解説:

  • const deleteSql =DELETE FROM users WHERE name = ?:: name カラムが指定された値に一致するレコードを削除するSQLコマンドです。
  • db.run(deleteSql, [userNameToDelete], function(err) { ... }): db.run() メソッドを実行します。プレースホルダに対応する値を配列で渡します。
  • コールバック関数は function(err) の形式で定義し、this.changes で削除された行数を取得しています。

WHERE 句を省略して DELETE FROM users; とすると、テーブル内の全てのレコードが削除されますが、テーブル構造自体は残ります。テーブル構造も含めて完全に削除したい場合は DROP TABLE users; コマンドを使用します(これも db.run() で実行します)。

11. エラーハンドリング

これまでのコード例でも簡単なエラーハンドリング(if (err) でエラーメッセージを出力)を含めてきましたが、実際のアプリケーションではより堅牢なエラー処理が必要です。データベース操作で発生しうるエラーには以下のようなものがあります。

  • 接続エラー: データベースファイルが見つからない、権限がない、パスが不正など。new sqlite3.Database() のコールバックで捕捉します。
  • SQL構文エラー: SQLコマンドの記述が間違っている。db.run(), db.get(), db.all(), db.each() などのコールバックで捕捉します。
  • 制約違反エラー: NOT NULL制約に違反するNULL値を挿入しようとした、UNIQUE制約に違反した値を挿入しようとしたなど。これも各操作メソッドのコールバックで捕捉します。
  • その他の実行時エラー: データベースファイルが破損している、ディスク容量不足など。

sqlite3 の各メソッドのコールバック関数には、通常第一引数に Error オブジェクトが渡されます。エラーが発生しなかった場合は null が渡されます。したがって、コールバックの先頭で if (err) をチェックし、エラーがあれば適切な処理(ログ出力、ユーザーへの通知、アプリケーションの終了、ロールバックなど)を行うのが基本です。

javascript
db.run(sql, params, function(err) {
if (err) {
console.error(`SQL実行エラー: ${err.message}`);
// エラーの種類に応じてさらに詳細な処理を分けることも可能 (err.code などを見る)
// 例: ROLLBACK する、アプリケーションを終了するなど
// 重要な操作の場合は、エラーを上位に伝播させる
} else {
// 成功時の処理
console.log(`操作成功。変更された行数: ${this.changes}`);
}
});

特に一連のデータベース操作をまとめて行う場合(例: トランザクション)、途中でエラーが発生したらそれ以降の操作を中断し、ロールバックを行うといった処理が必要になります。コールバックのネストが深くなると、エラーハンドリングのためのコードが複雑になりがちです。

12. Promise/async/await を利用したコード

sqlite3 ライブラリはデフォルトでコールバックベースの非同期APIを提供しています。Node.jsの非同期処理パターンとしては、近年Promiseや async/await が主流になっています。これらを利用することで、非同期処理のコードをより直感的で読みやすくすることができます。

sqlite3 ライブラリ自体はPromiseを直接返しませんが、自分でPromiseを返すラッパー関数を作成するか、既存のラッパーライブラリを利用することで、async/await を使って記述できるようになります。

12.1 sqlite3 をPromise化するラッパーの作成

ここでは、主要な sqlite3 メソッド(db.run, db.get, db.all, db.each, db.close)をPromise化する簡単なラッパー関数を作成してみます。

“`javascript
const sqlite3 = require(‘sqlite3’).verbose();

// DatabaseオブジェクトをPromise対応にするクラス
class Database {
constructor(file) {
this.db = new sqlite3.Database(file);
}

// db.run() をPromise化
run(sql, params = []) {
return new Promise((resolve, reject) => {
// function() {} を使うことで this.lastID や this.changes にアクセス可能
this.db.run(sql, params, function(err) {
if (err) {
reject(err);
} else {
// 挿入/更新/削除操作の場合、成功情報 (lastID, changes) を返す
resolve({ lastID: this.lastID, changes: this.changes });
}
});
});
}

// db.get() をPromise化
get(sql, params = []) {
return new Promise((resolve, reject) => {
this.db.get(sql, params, (err, row) => {
if (err) {
reject(err);
} else {
resolve(row); // 結果の単一行または undefined を返す
}
});
});
}

// db.all() をPromise化
all(sql, params = []) {
return new Promise((resolve, reject) => {
this.db.all(sql, params, (err, rows) => {
if (err) {
reject(err);
} else {
resolve(rows); // 結果の全行 (配列) を返す (見つからなければ空配列)
}
});
});
}

// db.each() をPromise化 (少し特殊なラッパー)
// 各行の処理はコールバックで行い、完了をPromiseで待つ
each(sql, params = [], callback) {
return new Promise((resolve, reject) => {
// Eachメソッドは完了コールバックに合計行数を渡す
this.db.each(sql, params, callback, (err, numRows) => {
if (err) {
reject(err);
} else {
resolve(numRows); // 処理した行数を返す
}
});
});
}

// db.close() をPromise化
close() {
return new Promise((resolve, reject) => {
this.db.close((err) => {
if (err) {
reject(err);
} else {
resolve(); // 成功時は引数なし
}
});
});
}

// Database接続自体もPromiseで待つようにする (new Database() のコンストラクタは同期だが、内部的なファイルオープンは非同期)
// 実際には Database コンストラクタのコールバックで接続状態を確認するのが標準だが、
// async/await の流れに合わせるために open() のようなメソッドを用意するパターンも
open(mode) {
return new Promise((resolve, reject) => {
this.db = new sqlite3.Database(this.db.filename, mode, (err) => {
if (err) {
this.db = null; // エラー時は参照をクリア
reject(err);
} else {
resolve(this); // 成功時は自分自身 (Databaseインスタンス) を返す
}
});
});
}
// または、new Promise() の中で new sqlite3.Database() を行う方が素直かもしれない
static open(file, mode) {
return new Promise((resolve, reject) => {
const db = new sqlite3.Database(file, mode, (err) => {
if (err) {
reject(err);
} else {
resolve(db); // 成功時はsqlite3.Databaseインスタンスを返す
}
});
});
}
}

// この Database クラスを使ってコードを書く

async function main() {
const dbFile = ‘./my_async_database.sqlite’;
let db; // Database インスタンスを保持する変数

try {
// データベースに接続 (Promise対応のstatic openを使用)
// db = new Database(dbFile); // コンストラクタは同期
// await db.open(); // 接続完了を待つ

// あるいは、より一般的な Promise ラッパーのパターン
db = await Database.open(dbFile);
console.log('データベースに接続しました (async/await)。');

// テーブル作成 (IF NOT EXISTS を使うことで何度実行しても安全)
const createTableSql = `
  CREATE TABLE IF NOT EXISTS users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    age INTEGER
  )
`;
await db.run(createTableSql); // await で処理完了を待つ
console.log('テーブル "users" が作成されたか、既に存在します。');

// データ挿入
const insertSql = `INSERT INTO users (name, age) VALUES (?, ?)`;
const usersToInsert = [
  ['Grace', 28],
  ['Henry', 45]
];

// トランザクション内で複数の挿入を行う (async/await と run)
// serialize() と組み合わせることで、SQLの実行順序も保証しつつ async/await を使える
await db.run('BEGIN TRANSACTION;'); // トランザクション開始
console.log('トランザクションを開始しました。');

for (const user of usersToInsert) {
    try {
        const result = await db.run(insertSql, user);
        console.log(`データ挿入成功 (${user[0]}, ID: ${result.lastID})`);
    } catch (insertErr) {
        console.error(`データ挿入エラー (${user[0]}):`, insertErr.message);
        // 挿入エラーが発生したらロールバックしてループを抜ける
        await db.run('ROLLBACK;');
        console.log('トランザクションをロールバックしました。');
        throw insertErr; // エラーを外側に再度投げる
    }
}

await db.run('COMMIT;'); // トランザクションコミット
console.log('トランザクションをコミットしました。');


// 全データ取得
const selectAllSql = `SELECT id, name, age FROM users`;
const allUsers = await db.all(selectAllSql); // await で結果を待つ
console.log('\n--- 全ユーザー情報 ---');
if (allUsers.length > 0) {
    allUsers.forEach(user => {
        console.log(`ID=${user.id}, Name=${user.name}, Age=${user.age}`);
    });
} else {
    console.log('ユーザーはまだいません。');
}
console.log('--- 取得完了 ---');

// 特定データの取得
const userName = 'Grace';
const selectSingleSql = `SELECT id, name, age FROM users WHERE name = ?`;
const singleUser = await db.get(selectSingleSql, [userName]); // await で結果を待つ
console.log(`\n--- "${userName}" の情報 ---`);
if (singleUser) {
    console.log(`ID=${singleUser.id}, Name=${singleUser.name}, Age=${singleUser.age}`);
} else {
    console.log(`"${userName}" は見つかりませんでした。`);
}
 console.log('--- 取得完了 ---');


// データ更新
const updateSql = `UPDATE users SET age = ? WHERE name = ?`;
const newAge = 29;
const userNameToUpdate = 'Grace';
const updateResult = await db.run(updateSql, [newAge, userNameToUpdate]); // await で結果を待つ
console.log(`\n--- データ更新 ---`);
console.log(`${updateResult.changes}件のレコードが更新されました (${userNameToUpdate}のageを${newAge}に)。`);
console.log('--- 更新完了 ---');


// データ削除
const deleteSql = `DELETE FROM users WHERE name = ?`;
const userNameToDelete = 'Henry';
const deleteResult = await db.run(deleteSql, [userNameToDelete]); // await で結果を待つ
console.log(`\n--- データ削除 ---`);
console.log(`${deleteResult.changes}件のレコードが削除されました (${userNameToDelete})。`);
console.log('--- 削除完了 ---');

} catch (error) {
console.error(‘エラーが発生しました:’, error.message);
// エラー発生時は接続が開いていれば閉じる
if (db) {
try {
await db.close();
console.log(‘エラー発生によりデータベース接続を閉じました。’);
} catch (closeErr) {
console.error(‘エラー発生時のデータベース切断エラー:’, closeErr.message);
}
}
} finally {
// エラーの有無に関わらず接続を閉じる(try…catchブロックでエラーハンドリングしている場合は finally は必須ではないが、確実性を高める)
// ただし、この finally は try…catch のエラーが発生した後に呼ばれるので、上記のエラーハンドリング内での close() で十分
// エラーが発生しなかった場合の close() が必要
if (db) {
try {
await db.close(); // 成功した場合のみ実行される想定
console.log(‘データベース接続を閉じました (正常終了)。’);
} catch (closeErr) {
console.error(‘正常終了時のデータベース切断エラー:’, closeErr.message);
}
}
}
}

main(); // async function を実行
“`

コード解説:

  • class Database { ... }: sqlite3.Database のインスタンスをラップし、各メソッドをPromiseを返すように再定義しています。
  • 各ラッパーメソッド(run, get, all, each, close)は new Promise((resolve, reject) => { ... }) を返します。オリジナルの db.method() を実行し、そのコールバック内でエラーがあれば reject を、成功すれば resolve を呼び出しています。
  • db.run のラッパーでは、this.lastIDthis.changes を含むオブジェクトを解決 (resolve) することで、呼び出し元でこれらの情報にアクセスできるようにしています。
  • Database.open はstaticメソッドとして、データベース接続自体もPromiseで待てるようにしています。new sqlite3.Database(...) のコールバックで成功/失敗を判断し、Promiseを解決/拒否します。
  • async function main() { ... }: async キーワードを関数の前につけることで、その中で await キーワードを使えるようになります。
  • try { ... } catch (error) { ... }: await で呼び出したPromiseが拒否された場合(エラーが発生した場合)、catch ブロックに処理が移ります。これにより、コールバック地獄を避けつつ、同期的なコードに近いスタイルで非同期処理とエラーハンドリングを記述できます。
  • 各データベース操作の前に await を付けることで、その操作が完了するまで次の行の実行を待つようになります。これにより、処理の流れが非常に分かりやすくなります。
  • トランザクション処理 (BEGIN TRANSACTION, COMMIT, ROLLBACK) も await db.run() で順番に実行しています。挿入ループの中でエラーハンドリングを行い、エラー時には ROLLBACK を実行し、さらに外側にエラーを投げ直すことで、catch ブロックでまとめてエラーを処理できるようにしています。
  • finally { ... }: try...catch ブロックの終了後に必ず実行されるブロックです。ここでは、正常終了した場合のデータベース切断処理を記述するのに使用できます。ただし、上記の例のように catch ブロック内で close を呼び出している場合は、二重に閉じないように注意が必要です。より一般的なパターンは、main 関数の最後(エラーなく try ブロックが終了した後)で db.close() を呼び出すか、または finally ブロック内で db オブジェクトが存在するか確認して閉じることです。上記の例では、try ブロックの最後に db.close() があり、catch ブロック内でも db.close() があるため、どちらかで閉じられます。

この async/await を使ったスタイルは、現代のNode.js開発で広く採用されており、非同期処理の複雑さを大幅に軽減してくれます。自分でラッパーを書くのが面倒な場合は、sqlite3-promise のような既存のライブラリを利用することも可能です。

13. 実践的なサンプルアプリケーション (簡易TODOリストCLI)

これまでに学んだことを活かして、簡単なコマンドラインベースのTODOリスト管理ツールを作成してみましょう。

機能:

  • TODOを追加する
  • TODO一覧を表示する
  • TODOを完了済みにする
  • TODOを削除する

必要なファイルの準備:

  • package.json (すでに npm init -y で作成済み)
  • index.js (メインスクリプト)
  • database.js (データベース操作をまとめたモジュール)

npm install sqlite3 は実行済みとします。

database.js ファイルの作成:

まず、データベース操作をPromiseベースで行えるようにするモジュールを作成します。先ほど作成したPromiseラッパーをベースにします。

“`javascript
// database.js
const sqlite3 = require(‘sqlite3’).verbose();

// sqlite3.Database を Promise 対応にする
class Database {
constructor(file) {
this.db = new sqlite3.Database(file);
}

run(sql, params = []) {
return new Promise((resolve, reject) => {
this.db.run(sql, params, function(err) {
if (err) {
reject(err);
} else {
resolve({ lastID: this.lastID, changes: this.changes });
}
});
});
}

get(sql, params = []) {
return new Promise((resolve, reject) => {
this.db.get(sql, params, (err, row) => {
if (err) {
reject(err);
} else {
resolve(row);
}
});
});
}

all(sql, params = []) {
return new Promise((resolve, reject) => {
this.db.all(sql, params, (err, rows) => {
if (err) {
reject(err);
} else {
resolve(rows);
}
});
});
}

close() {
return new Promise((resolve, reject) => {
this.db.close((err) => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
}

// データベース接続を Promise で待つ static メソッド
static open(file, mode = sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE) {
return new Promise((resolve, reject) => {
const db = new sqlite3.Database(file, mode, (err) => {
if (err) {
reject(err);
} else {
resolve(new Database(file)); // ラッパーインスタンスを返す
}
});
});
}

// テーブル初期化メソッド
async init() {
const createTableSql = CREATE TABLE IF NOT EXISTS todos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
task TEXT NOT NULL,
completed BOOLEAN DEFAULT 0
)
;
await this.run(createTableSql);
console.log(‘テーブル “todos” が作成されたか、既に存在します。’);
}
}

module.exports = Database; // 作成した Database クラスをエクスポート
“`

index.js ファイルの作成:

このファイルでコマンドライン引数を解析し、database.js の機能を使ってTODOを管理します。簡単なコマンドライン引数解析には process.argv を使用します。

“`javascript
// index.js
const Database = require(‘./database’); // database.js をインポート
const path = require(‘path’);

const dbPath = path.resolve(__dirname, ‘todos.sqlite’); // データベースファイルのパス
let db = null; // データベースインスタンスを保持する変数

async function main() {
const args = process.argv.slice(2); // コマンドライン引数を取得 (最初の2つは node と スクリプトパス)
const command = args[0]; // 最初の引数をコマンドとする

// データベースに接続
try {
    // Database.open は Promise を返すように修正した
    db = await Database.open(dbPath);
    console.log('データベースに接続しました。');

    // テーブルが存在しない場合は作成
    await db.init();

    // コマンドに応じて処理を分岐
    switch (command) {
        case 'add':
            const task = args.slice(1).join(' '); // 2番目以降の引数をタスク内容とする
            if (!task) {
                console.error('エラー: 追加するタスク内容を指定してください。');
                break;
            }
            await addTodo(task);
            break;

        case 'list':
            await listTodos();
            break;

        case 'complete':
            const completeId = parseInt(args[1], 10); // 2番目の引数をIDとする
            if (isNaN(completeId)) {
                 console.error('エラー: 完了済みにするTODOのIDを指定してください。');
                 break;
            }
            await completeTodo(completeId);
            break;

        case 'delete':
             const deleteId = parseInt(args[1], 10); // 2番目の引数をIDとする
             if (isNaN(deleteId)) {
                 console.error('エラー: 削除するTODOのIDを指定してください。');
                 break;
             }
            await deleteTodo(deleteId);
            break;

        case 'help':
        default:
            showHelp();
            break;
    }

} catch (error) {
    console.error('アプリケーション実行中にエラーが発生しました:', error.message);
} finally {
    // データベース接続を閉じる
    if (db) {
        try {
            await db.close();
            console.log('データベース接続を閉じました。');
        } catch (closeErr) {
             console.error('データベース切断エラー:', closeErr.message);
        }
    }
}

}

// TODOを追加する関数
async function addTodo(task) {
const insertSql = INSERT INTO todos (task) VALUES (?);
try {
const result = await db.run(insertSql, [task]);
console.log(TODOを追加しました (ID: ${result.lastID}): "${task}");
} catch (err) {
console.error(‘TODO追加エラー:’, err.message);
}
}

// TODO一覧を表示する関数
async function listTodos() {
const selectSql = SELECT id, task, completed FROM todos ORDER BY id ASC;
try {
const todos = await db.all(selectSql);
console.log(‘\n— TODOリスト —‘);
if (todos.length === 0) {
console.log(‘TODOはまだありません。’);
} else {
todos.forEach(todo => {
const status = todo.completed ? ‘[完了]’ : ‘[未完了]’;
console.log(${todo.id}. ${status} ${todo.task});
});
}
console.log(‘—————–‘);
} catch (err) {
console.error(‘TODO一覧取得エラー:’, err.message);
}
}

// TODOを完了済みにする関数
async function completeTodo(id) {
const updateSql = UPDATE todos SET completed = 1 WHERE id = ?;
try {
const result = await db.run(updateSql, [id]);
if (result.changes > 0) {
console.log(TODO (ID: ${id}) を完了済みにしました。);
} else {
console.warn(警告: TODO (ID: ${id}) は見つかりませんでした。);
}
} catch (err) {
console.error(‘TODO完了エラー:’, err.message);
}
}

// TODOを削除する関数
async function deleteTodo(id) {
const deleteSql = DELETE FROM todos WHERE id = ?;
try {
const result = await db.run(deleteSql, [id]);
if (result.changes > 0) {
console.log(TODO (ID: ${id}) を削除しました。);
} else {
console.warn(警告: TODO (ID: ${id}) は見つかりませんでした。);
}
} catch (err) {
console.error(‘TODO削除エラー:’, err.message);
}
}

// ヘルプメッセージ表示関数
function showHelp() {
console.log(`
TODOリストCLIツール

使い方:
node index.js add <タスク内容> – 新しいTODOを追加します
node index.js list – TODO一覧を表示します
node index.js complete – 指定したIDのTODOを完了済みにします
node index.js delete – 指定したIDのTODOを削除します
node index.js help – このヘルプメッセージを表示します
`);
}

main(); // アプリケーション起動
“`

実行方法:

プロジェクトのルートディレクトリで以下のコマンドを実行します。

  • 新しいTODOを追加:
    bash
    node index.js add 買い物に行く
    node index.js add 読書をする
  • TODO一覧を表示:
    bash
    node index.js list
  • TODOを完了済みにする(例: IDが1のTODO):
    bash
    node index.js complete 1
  • TODOを削除する(例: IDが2のTODO):
    bash
    node index.js delete 2
  • ヘルプを表示:
    bash
    node index.js help

このサンプルは非常にシンプルですが、Node.jsからSQLiteを操作する基本的な流れと、Promise/async/await を使った非同期処理の記述方法を理解するのに役立ちます。database.js モジュールでデータベース操作の責務を分離し、index.js でコマンドライン引数の解析と適切なデータベース操作関数の呼び出しを行う、という構造になっています。

14. 高度な操作と考慮事項

14.1 トランザクションのより詳細な制御

db.serialize() は操作の順序を保証しますが、厳密なトランザクション制御(BEGIN, COMMIT, ROLLBACK)は手動でSQLコマンドとして実行する必要があります。async/await 環境では、以下のように try...catch ブロックと組み合わせてトランザクションを制御するのが一般的です。

“`javascript
// async/await を使用したトランザクション処理の例
async function performTransaction(db) {
try {
await db.run(‘BEGIN TRANSACTION;’); // トランザクション開始
console.log(‘トランザクション開始’);

// ここにトランザクションに含めたい一連の操作を記述
const result1 = await db.run('INSERT INTO users (name, age) VALUES (?, ?)', ['New User 1', 20]);
console.log('Insert 1 done:', result1);

// 意図的にエラーを発生させる場合
// throw new Error("Something went wrong!");

const result2 = await db.run('UPDATE users SET age = ? WHERE name = ?', [21, 'New User 1']);
console.log('Update 1 done:', result2);

await db.run('COMMIT;'); // 全て成功したらコミット
console.log('トランザクションコミット');

} catch (error) {
console.error(‘トランザクション中にエラー:’, error.message);
await db.run(‘ROLLBACK;’); // エラーが発生したらロールバック
console.log(‘トランザクションロールバック’);
throw error; // 必要に応じてエラーをさらに上位に投げる
}
}

// 使用例 (main 関数や他の async function 内で)
// await performTransaction(db);
“`

このように try...catch を使うことで、例外発生時に自動的にロールバックを行う堅牢なトランザクション処理を記述できます。

14.2 パフォーマンスチューニング

SQLiteは軽量ですが、大量のデータを扱う場合や複雑なクエリを実行する場合、パフォーマンスが問題になることがあります。

  • インデックスの利用: WHERE 句や ORDER BY 句で頻繁に使用されるカラムにはインデックスを作成すると、データの検索や並べ替えが高速化されます。
    sql
    CREATE INDEX idx_users_name ON users (name);
    CREATE INDEX idx_users_age ON users (age);

    インデックスの作成や削除は db.run() で実行できます。ただし、インデックスはデータの挿入・更新・削除時にオーバーヘッドが発生するため、必要最小限にとどめるべきです。
  • クエリの最適化: EXPLAIN QUERY PLAN 文を使って、SQLiteがどのようにクエリを実行するかを確認できます。これにより、遅いクエリの原因を特定できます。
  • ジャーナルモード: SQLiteはデフォルトでロールバックジャーナルモードを使用しますが、ライトアヘッドログ (WAL: Write-Ahead Logging) モードの方が並列性が高く、書き込みが多い場合にパフォーマンスが向上する可能性があります。
    sql
    PRAGMA journal_mode=WAL;

    これも db.run() で設定できます。
  • バキューム: VACUUM コマンドはデータベースファイルを再構成し、削除された領域を解放してファイルサイズを小さくします。これにより、断片化が解消され、読み取り性能が向上する場合があります。ただし、VACUUM中はデータベース全体がロックされるため、実行タイミングに注意が必要です。
    sql
    PRAGMA vacuum;

14.3 スキーマの変更 (ALTER TABLE)

既存のテーブル構造を変更するには ALTER TABLE コマンドを使用します。SQLiteの ALTER TABLE は他のRDBMSと比較して機能が限定的です(カラムの追加、カラム名の変更、テーブル名の変更は可能ですが、カラムの削除やデータ型の変更は直接サポートされていません)。

  • カラムの追加:
    sql
    ALTER TABLE users ADD COLUMN email TEXT;
  • カラム名の変更:
    sql
    ALTER TABLE users RENAME COLUMN age TO user_age;
  • テーブル名の変更:
    sql
    ALTER TABLE users RENAME TO user_profiles;

カラムの削除やデータ型の変更が必要な場合は、一時テーブルを作成してデータを移行し、古いテーブルを削除してから新しいテーブルをリネームする、という手順を踏む必要があります。

14.4 データベースのバックアップとリストア

SQLiteデータベースは単一のファイルなので、バックアップは単純にファイルをコピーするだけで可能です。リストアは、コピーしたファイルを元の場所に戻すだけです。ただし、アプリケーションがデータベースファイルを開いている間は、完全なバックアップが取れない可能性があるため、アプリケーションを停止してからコピーするか、SQLiteのオンラインバックアップAPIを使用するのが望ましいです。

sqlite3 ライブラリは、オンラインバックアップAPIを直接提供していませんが、ネイティブAPI経由でアクセスすることは可能です。しかし、Node.jsのラッパーとしてはあまり一般的ではないため、通常は外部ツール(sqlite3 コマンドラインツールなど)を使用するか、ファイルコピーで対応することが多いです。

14.5 より高レベルな抽象化 (ORマッパー)

SQLクエリを文字列として組み立てるのは、特に複雑なクエリやテーブルのリレーションが増えると煩雑になります。オブジェクト指向のコードとリレーショナルデータベースの概念のギャップを埋めるために、ORM (Object-Relational Mapper) があります。

Node.jsでSQLiteを扱うための一般的なORMライブラリには以下のようなものがあります。

  • Sequelize: 多くのデータベース(PostgreSQL, MySQL, SQLite, SQL Serverなど)に対応した強力なORMです。マイグレーション機能なども備えています。
  • TypeORM: TypeScript/JavaScript向けのORMで、Entityクラスを使ってデータベーススキーマを定義し、オブジェクトとしてデータを操作できます。
  • Prisma: モダンなデータベースツールキットで、スキーマ定義からORMクライアントコード生成、マイグレーションまでをサポートします。SQLiteもサポートしています。

これらのORMを使うと、SQLを書く代わりにJavaScript/TypeScriptのコードでデータベース操作を記述できるようになります。学習コストはかかりますが、大規模なアプリケーション開発では生産性とメンテナンス性が向上します。

15. まとめ

この記事では、Node.jsを使ってSQLiteデータベースに接続し、基本的なCRUD操作を行う方法を詳細に解説しました。

  • Node.jsとSQLiteの組み合わせは、軽量でセットアップが容易なアプリケーション開発に適しています。
  • sqlite3 ライブラリを使用することで、Node.jsからSQLiteを操作できます。
  • データベース接続、テーブル作成、データの挿入、取得、更新、削除といった基本的な操作方法を学びました。
  • SQLインジェクションを防ぐために、必ずプレースホルダを使用すべきであることを理解しました。
  • sqlite3 は非同期APIを提供するため、コールバックまたはPromise/async/await を使って処理フローを制御する必要があることを学びました。
  • Promise/async/await を使うことで、非同期コードをより読みやすく、エラーハンドリングもしやすくなることを、Promiseラッパーの作成とサンプルアプリケーションを通して確認しました。
  • トランザクション、パフォーマンス、スキーマ変更、ORマッパーといったより高度なトピックにも触れました。

このガイドが、Node.jsとSQLiteを使ったデータベース操作を始めるための一助となれば幸いです。

学習の次のステップ:

  • より複雑なSQL: JOINを使った複数テーブルからのデータ取得、集計関数(COUNT, SUM, AVGなど)、サブクエリなどを学習する。
  • エラーハンドリングの深化: エラーの種類に応じたより詳細な処理や、ロギングの実装。
  • ORマッパーの利用: SequelizeやTypeORM、PrismaなどのORMを使ってみることで、より抽象化されたデータベース操作を学ぶ。
  • 他のデータベース: MySQL, PostgreSQL, MongoDBなど、他のデータベースとNode.jsを連携させる方法を学ぶ。
  • 実際のアプリケーション開発: WebアプリケーションのバックエンドやRESTful APIなど、より実践的なアプリケーション開発に挑戦し、学んだ知識を応用する。

SQLiteはシンプルながらも強力なデータベースであり、Node.jsとの組み合わせはアイデアを素早く形にするのに最適です。ぜひ、この記事で得た知識を活かして、様々なアプリケーション開発に挑戦してみてください。


コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

上部へスクロール