SwiftでSQLiteデータベースを扱う入門解説


SwiftでSQLiteデータベースを扱う入門解説

iOS、macOS、watchOS、tvOSなどのAppleプラットフォーム向けアプリケーション開発において、データの永続化は重要な要素の一つです。ユーザー設定、キャッシュデータ、オフラインで利用可能なコンテンツなど、様々な情報をアプリケーションの終了後も保持する必要があります。この目的のために、SQLiteは非常に強力かつ軽量なデータベースエンジンとして広く利用されています。

この記事では、Swiftを使ってSQLiteデータベースを操作する方法について、入門者向けに詳細かつ網羅的に解説します。SQLiteの基本的な概念から、SwiftからのC API呼び出し、データベースの接続、テーブル作成、データの挿入・取得・更新・削除(CRUD操作)、そしてエラーハンドリングやトランザクションといった応用的な内容まで、ステップバイステップで学んでいきます。

SQLite C APIを直接扱う方法は、少し低レベルですが、SQLiteの仕組みを深く理解するのに役立ちます。また、後述するラッパーライブラリ(FMDBやGRDBなど)も、結局はこのC APIをSwiftから扱いやすくラップしているにすぎません。まずは基礎としてC APIの呼び出し方から学ぶことで、より堅牢で効率的なデータベース処理を実装するための土台を築くことができます。

この記事を読むことで、あなたは以下のことを習得できます。

  • SQLiteデータベースの基本構造と概念
  • SwiftプロジェクトでSQLite C APIを呼び出すためのセットアップ方法
  • データベースファイルへの接続と切断
  • SQL文を実行してテーブルを作成する方法
  • プリペアドステートメントを使ってデータを安全かつ効率的に操作する方法(挿入、取得、更新、削除)
  • データベース操作におけるエラーの検出と適切な処理
  • トランザクションによる一連の操作の整合性の確保
  • SwiftコードからSQLite操作をより扱いやすくするための基本的なラッピング方法
  • より高レベルなラッパーライブラリの存在とメリット

約5000語というボリュームで、各トピックについて深く掘り下げて解説していきますので、じっくりと読み進めてみてください。

1. SQLiteとは何か、なぜSwiftで使うのか

SQLiteは、非常に軽量で組み込みに適したリレーショナルデータベース管理システム(RDBMS)です。その主な特徴は以下の通りです。

  • サーバーレス: 従来のRDBMSのように独立したサーバープロセスを必要としません。データベース全体が単一のファイルに格納されます。
  • 設定不要: インストールや設定が非常に簡単で、多くの環境にデフォルトで組み込まれています。
  • トランザクショナル: ACID特性(原子性、一貫性、分離性、永続性)をサポートしており、データの信頼性が高いです。
  • ゼロコンフィギュレーション: 外部の依存関係がほとんどなく、特別な設定なしですぐに利用できます。
  • 自己完結型: 外部ライブラリに依存することなく動作します。
  • パブリックドメイン: 商用・非商用を問わず、無料で自由に利用、配布、改変が可能です。

これらの特徴から、SQLiteはモバイルアプリケーション(iOS, Android)、デスクトップアプリケーション、組み込みシステムなどで広く利用されています。iOS SDKやmacOS SDKにも標準で組み込まれており、特別なライブラリを追加することなく利用できます。

Swiftアプリケーションでデータを永続化する方法はいくつかあります。例えば、UserDefaults、ファイルシステムへの書き込み、Core Data、そしてSQLiteなどです。

  • UserDefaults: 小規模なユーザー設定などのシンプルなデータに適しています。複雑な構造のデータや大量のデータには向きません。
  • ファイルシステム: アプリケーション独自のファイル形式でデータを保存できます。柔軟性が高いですが、データの検索や関係性の管理はすべて自分で実装する必要があります。
  • Core Data: Appleが提供する高レベルなオブジェクトグラフ管理フレームワークです。SQLiteをバックエンドとして利用することもできますが、Core Data自体はRDBMSのラッパーではなく、オブジェクトグラフの管理に特化しています。データモデルをObjective-C/Swiftオブジェクトとして扱い、複雑なリレーションシップなども管理しやすいですが、学習コストはやや高めです。
  • SQLite: リレーショナルデータベースとして、構造化されたデータを効率的に管理し、強力なクエリ(検索)機能を利用できます。SQL(Structured Query Language)を使ってデータを操作するため、SQLの知識が必要です。複雑なデータ構造や大量のデータを扱う場合に強力な選択肢となります。

SQLiteは、Core Dataほど抽象化されていませんが、直接SQLを扱うことで、より細かくパフォーマンスを制御したり、特定のクエリを実行したりすることが可能です。また、多くのプラットフォームで利用されているため、他の環境とのデータ連携も容易です。

SwiftからSQLiteを扱う方法は主に二つあります。

  1. SQLite C APIを直接呼び出す: SwiftのC言語連携機能を使って、SQLiteが提供するC APIを直接呼び出します。低レベルな操作になりますが、SQLiteの動作原理を深く理解できます。この記事の主要なテーマです。
  2. SQLiteラッパーライブラリを利用する: SQLite C APIの上に構築されたSwift/Objective-C製のライブラリを利用します。Swiftらしい構文でデータベース操作が可能になり、C APIを直接扱うよりも安全かつ簡単にコードを書けます。代表的なものにFMDB (Objective-CベースだがSwiftから利用可) やGRDB (Swiftネイティブ) があります。

最初はC APIを理解し、その上でラッパーライブラリを使うのがおすすめです。C APIの挙動を知っていると、ラッパーライブラリを使った際に何が行われているかを把握しやすくなります。

2. Swiftプロジェクトのセットアップ

SwiftプロジェクトでSQLite C APIを利用するには、いくつかのセットアップが必要です。Xcodeプロジェクトを作成し、SQLiteライブラリをリンクし、C APIをSwiftから呼び出せるようにするための設定を行います。

新しいXcodeプロジェクトを作成してください(例えば、iOS向けのSingle View App)。プロジェクト名は任意です(例: SwiftSQLiteSample)。

2.1 libsqlite3.tbd ライブラリのリンク

SQLiteはmacOSやiOSなどにシステムライブラリとして標準で搭載されています。Xcodeプロジェクトからこのライブラリを利用できるように設定します。

  1. プロジェクトナビゲーターでプロジェクトを選択します。
  2. プロジェクト設定画面が表示されるので、対象のターゲットを選択します。
  3. 「General」タブを開き、「Frameworks, Libraries, and Embedded Content」セクションまでスクロールします。
  4. リストの下にある + ボタンをクリックします。
  5. 表示されるウィンドウの検索バーに「sqlite3」と入力します。
  6. libsqlite3.tbd が表示されるので、これを選択して「Add」ボタンをクリックします。

これで、プロジェクトがSQLite C APIを含むシステムライブラリにリンクされました。

2.2 Bridging Header ファイルの作成

SwiftコードからC言語の関数を呼び出すには、「Bridging Header」ファイルが必要です。このファイルに、Swiftから利用したいCヘッダーファイルをインポートするディレクティブ (#import または #include) を記述します。

  1. Xcodeのプロジェクトナビゲーターで、新しいファイルを作成します (File > New > File...)。
  2. テンプレートとして「Header File」を選択し、「Next」をクリックします。
  3. ファイル名をプロジェクト名に続けて「-Bridging-Header」とするのが一般的な命名規則です(例: SwiftSQLiteSample-Bridging-Header.h)。保存場所はプロジェクトのルートディレクトリなどが良いでしょう。「Create」をクリックします。
  4. ファイルを作成すると、Xcodeは「Would you like to configure an Objective-C Bridging Header?」というダイアログを表示する場合があります。「Enable Bridging Header」をクリックして設定を有効にします。もし表示されなかった場合は、手動でプロジェクト設定を行う必要があります(後述)。
  5. 作成した <ProjectName>-Bridging-Header.h ファイルを開き、以下の行を追加します。

“`c

include

“`

これで、SQLite C APIの関数や型がSwiftコードから利用できるようになります。

Bridging Headerが自動で設定されなかった場合、または設定を確認したい場合は、以下の手順を行います。

  1. プロジェクトナビゲーターでプロジェクトを選択します。
  2. プロジェクト設定画面が表示されるので、対象のターゲットを選択します。
  3. 「Build Settings」タブを選択します。
  4. 検索バーに「bridging header」と入力します。
  5. 「Swift Compiler – General」セクションにある「Objective-C Bridging Header」設定項目を探します。
  6. この項目に、作成したBridging Headerファイルのパス(プロジェクトルートからの相対パス)が正しく設定されていることを確認します(例: SwiftSQLiteSample/SwiftSQLiteSample-Bridging-Header.h)。パスが設定されていない場合は、ここに手動でパスを入力します。

これで、Swiftからsqlite3_*という形式のC API関数を呼び出す準備が整いました。

3. SQLiteの基本概念とSQL

SQLiteデータベースを操作するには、いくつかの基本的な概念と、操作のための言語であるSQL(Structured Query Language)の知識が必要です。

  • データベースファイル: SQLiteデータベース全体は通常、単一のファイルに格納されます(例えば、mydatabase.db)。このファイルの中に、すべてのデータ、テーブルスキーマ、インデックスなどが含まれます。
  • テーブル: データベース内のデータは、一つ以上のテーブルに organized されます。テーブルはスプレッドシートのような二次元の構造を持ち、行と列で構成されます。
  • カラム(列): テーブルの各列は「カラム」と呼ばれます。各カラムは特定の種類のデータを保持し、名前とデータ型を持ちます。SQLiteはいくつかのデータ型をサポートしますが、柔軟性が高いのが特徴です。主なデータ型は以下の通りです。
    • INTEGER: 符号付き整数。データのサイズに応じて1, 2, 3, 4, 6, 8バイトで格納されます。PRIMARY KEYとして使用される場合、自動的に行IDとして機能することが多いです。
    • REAL: 浮動小数点数。8バイトのIEEE浮動小数点数として格納されます。
    • TEXT: テキスト文字列。様々なエンコーディング(UTF-8, UTF-16など)をサポートします。
    • BLOB: バイナリラージオブジェクト。任意形式のバイナリデータ(画像やファイルの内容など)を格納できます。
    • NULL: 値が存在しないことを示します。
      SQLiteは動的な型付けをサポートしており、宣言されたデータ型と異なる型の値を挿入することも可能です。ただし、一貫性を保つためには宣言された型に従うのが一般的です。
  • 行(レコード): テーブルの各行は「レコード」と呼ばれます。一行は、テーブルのカラムに対応する一連の値を保持します。
  • 主キー (Primary Key): テーブル内の各行を一意に識別するための一つ以上のカラムの組み合わせです。通常、INTEGER PRIMARY KEYが使われ、各行に自動的にユニークな番号が割り振られます。
  • 外部キー (Foreign Key): あるテーブルのカラムが、別のテーブルの主キーを参照するものです。テーブル間の関係性を定義し、データの整合性を保つのに役立ちます。

SQL文

データベースを操作するためにSQL文を使用します。基本的な操作に対応するSQL文をいくつか紹介します。

  • CREATE TABLE: 新しいテーブルを作成します。
    sql
    CREATE TABLE users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    age INTEGER
    );

    この例では、usersという名前のテーブルを作成しています。idカラムは整数型で主キーとして自動増分されます。nameカラムはテキスト型でNULLを許容しません。ageカラムは整数型でNULLを許容します。
  • INSERT INTO: テーブルに新しい行を挿入します。
    sql
    INSERT INTO users (name, age) VALUES ('Alice', 30);
    INSERT INTO users (name) VALUES ('Bob'); -- ageはNULLになる
  • SELECT: テーブルからデータを取得(クエリ)します。
    sql
    SELECT id, name, age FROM users; -- 全てのカラムと行を取得
    SELECT name FROM users WHERE age > 25; -- ageが25より大きいユーザーの名前を取得
    SELECT COUNT(*) FROM users; -- テーブルの行数を取得

    WHERE句を使って条件を指定できます。
  • UPDATE: 既存の行のデータを更新します。
    sql
    UPDATE users SET age = 31 WHERE name = 'Alice'; -- Aliceのageを31に更新

    WHERE句を省略すると、全ての行が更新されます。
  • DELETE FROM: テーブルから行を削除します。
    sql
    DELETE FROM users WHERE age < 20; -- ageが20未満のユーザーを削除

    WHERE句を省略すると、テーブルの全ての行が削除されます(テーブル自体は残ります)。
  • DROP TABLE: テーブルを完全に削除します。
    sql
    DROP TABLE users;

SwiftからこれらのSQL文を文字列として構築し、SQLite C APIを介して実行します。

4. SQLiteデータベースへの接続と切断

データベース操作の最初のステップは、データベースファイルへの接続です。データベース接続は sqlite3* 型のポインタで管理されます。

まず、データベースファイルがどこに保存されるかを決めます。iOSアプリケーションでは、アプリケーションのDocumentsディレクトリやLibraryディレクトリにファイルを保存するのが一般的です。Documentsディレクトリはユーザーがアクセス可能な場所ですが、アプリケーションが生成するデータベースファイルなど、ユーザーに見せる必要のないものはLibraryディレクトリ(特にApplication Supportサブディレクトリ)に保存するのが良い慣習です。ここではDocumentsディレクトリに保存する例を示します。

“`swift
import Foundation
// Bridging Headerによってsqlite3が利用可能になっている

func getDocumentsDirectory() -> String {
let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
let documentsDirectory = paths[0]
return documentsDirectory
}

let dbFileName = “mydatabase.db”
let dbPath = “(getDocumentsDirectory())/(dbFileName)”
print(“Database path: (dbPath)”) // デバッグ用にパスを表示
“`

データベースファイルへの接続は sqlite3_open_v2 関数を使用します。切断は sqlite3_close 関数を使用します。

“`swift
var db: OpaquePointer? // sqlite3* 型に対応するSwiftの型

// データベースファイルへの接続
// sqlite3_open_v2(データベースファイルのパス, DBへのポインタのアドレス, 開き方フラグ, 拡張機能の名前)
// 開き方フラグ: SQLITE_OPEN_READWRITE (読み書き), SQLITE_OPEN_CREATE (ファイルが存在しない場合に作成), SQLITE_OPEN_FULLMUTEX (スレッド安全)
let flags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX
let rc = sqlite3_open_v2(dbPath, &db, flags, nil)

if rc == SQLITE_OK {
print(“Successfully opened database connection.”)
// ここでデータベース操作を行う
// …
// データベース接続の切断
sqlite3_close(db)
print(“Closed database connection.”)
} else {
// エラー処理
// sqlite3_errmsg() は最後の操作のエラーメッセージをC文字列で返す
let errMsg = sqlite3_errmsg(db)
let swiftErrMsg = String(cString: errMsg!) // C文字列をSwift文字列に変換
print(“Failed to open database: (swiftErrMsg)”)

// 接続失敗時にはdbポインタがNULLでない可能性もあるため、クローズを試みる
if db != nil {
    sqlite3_close(db)
}

}
“`

解説:

  • var db: OpaquePointer?: sqlite3* 型はCのポインタです。Swiftでは、Cのポインタは UnsafeMutablePointer<Type> または OpaquePointer で表現されます。sqlite3* は指している型が不明なポインタとして扱われるため、OpaquePointer? を使用するのが適切です。データベース接続が開かれるまで値はnilの可能性があるためOptional型にします。
  • sqlite3_open_v2(_:_:_:_: ): データベース接続を開くための関数です。
    • 第1引数: データベースファイルのパスをC文字列 (const char*) で渡します。SwiftのStringから.cString(using: .utf8)などで変換できますが、dbPathのように単にString型の変数を渡すだけでも、Swiftの文字列補間されたC文字列リテラルとして扱われるため、多くの場合これで動作します。ただし、確実性を求めるなら明示的な変換が推奨されます。
    • 第2引数: sqlite3* ポインタのアドレスを渡します(&db)。関数が成功すると、このアドレスに開かれたデータベース接続を指すポインタが書き込まれます。
    • 第3引数: 接続モードを指定するフラグです。
      • SQLITE_OPEN_READWRITE: 読み書きモードで開きます。
      • SQLITE_OPEN_CREATE: 指定されたファイルが存在しない場合に新しく作成します。
      • SQLITE_OPEN_FULLMUTEX: 接続オブジェクトに対して完全なマルチスレッド安全性を提供します。これはスレッド間で同じ接続オブジェクトを共有する場合に重要ですが、通常はスレッドごとに異なる接続オブジェクトを使用するのがより一般的でパフォーマンスも良いです。ここでは安全のため付けていますが、不要であれば省略も可能です。
      • 他にも SQLITE_OPEN_READONLY (読み取り専用) などがあります。
    • 第4引数: 拡張機能の名前を指定します。通常はnilで構いません。
  • 戻り値 rc: 関数の実行結果を示すリターンコードです。SQLITE_OK (0) は成功を示します。それ以外の値はエラーを示します。SQLite APIのほとんどの関数は、このようにリターンコードを返します。
  • SQLITE_OK: SQLite操作が成功したことを示す定数です。
  • sqlite3_errmsg(_: ): 指定されたデータベース接続 (db) で発生した最後の操作のエラーメッセージをC文字列 (const char*) として返します。エラーが発生した場合に詳しい原因を知るために使います。
  • String(cString: errMsg!): C文字列ポインタをSwiftのStringに変換します。errMsgはOptionalなのでアンラップが必要です。
  • sqlite3_close(_: ): データベース接続を閉じます。データベース操作が終わったら、必ずこの関数を呼び出してリソースを解放する必要があります。引数には sqlite3* ポインタを渡します。

このコードは、データベースファイルへの接続と切断の基本的な流れを示しています。実際には、これらの操作を専用のクラスや構造体の中にカプセル化すると、コードが整理されて扱いやすくなります(後述します)。

5. テーブルの作成

データベースにデータを格納するには、まずテーブルが必要です。テーブルの作成は CREATE TABLE SQL文を実行することで行います。SQL文の実行にはいくつかの方法がありますが、結果セットを返さない単純なSQL文(CREATE TABLE, INSERT, UPDATE, DELETE, DROP TABLEなど)の場合は sqlite3_exec 関数が便利です。

sqlite3_exec(_:_:_:_:_:) 関数は、指定されたSQL文を実行し、必要に応じてコールバック関数を呼び出すことができます。テーブル作成ではコールバックは不要です。

“`swift
// データベース接続が開かれている状態を想定
// var db: OpaquePointer? は有効なポインタを保持しているとする

let createTableSQL = “””
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
age INTEGER
);
“””

// sqlite3_exec(DBポインタ, SQL文(C文字列), コールバック関数, コールバック関数の引数, エラーメッセージポインタのアドレス)
var errorMsg: UnsafeMutablePointer? = nil
let rc = sqlite3_exec(db, createTableSQL.cString(using: .utf8), nil, nil, &errorMsg)

if rc == SQLITE_OK {
print(“Successfully created table users.”)
} else {
// エラー処理
if let errorMsg = errorMsg {
let swiftErrMsg = String(cString: errorMsg)
print(“Failed to create table: (swiftErrMsg)”)
sqlite3_free(errorMsg) // sqlite3_execによって割り当てられたエラーメッセージを解放
} else {
print(“Failed to create table: Unknown error.”)
}
}
“`

解説:

  • let createTableSQL = """...""": SQL文をSwiftの複数行文字列リテラルで定義しています。CREATE TABLE IF NOT EXISTS は、指定された名前のテーブルがまだ存在しない場合にのみテーブルを作成するという意味です。これにより、アプリケーションの起動時などに繰り返しテーブル作成処理を呼び出してもエラーになりません。
  • createTableSQL.cString(using: .utf8): SwiftのStringをC文字列 (const char*) に変換しています。sqlite3_execはC文字列を期待するため、この変換が必要です。.cString(using: .utf8)はnull終端されたC文字列へのポインタを返します。このポインタは一時的なものなので、関数の呼び出し時に直接渡すのが安全です。
  • sqlite3_exec(_:_:_:_:_:): SQL文を実行します。
    • 第1引数: データベース接続を指す sqlite3* ポインタ。
    • 第2引数: 実行するSQL文を指すC文字列ポインタ。
    • 第3引数: コールバック関数へのポインタ。SELECT文などで結果セットを処理する場合に指定しますが、テーブル作成では不要なのでnilを渡します。
    • 第4引数: コールバック関数に渡す任意のデータへのポインタ。コールバック関数を指定しない場合はnilです。
    • 第5引数: エラーメッセージを受け取るC文字列ポインタのアドレス(UnsafeMutablePointer<CChar>?のアドレス)。sqlite3_execの実行中にエラーが発生した場合、SQLiteはこのポインタが指すメモリにエラーメッセージのC文字列を書き込みます。エラーメッセージが必要ない場合はnilを渡すことも可能です。
  • 戻り値 rc: 実行結果のリターンコード。SQLITE_OKなら成功。
  • エラーメッセージの取得と解放: sqlite3_execの第5引数にポインタのアドレスを渡した場合、エラー発生時にはそのポインタにエラーメッセージが格納されます。このメモリはSQLiteによって動的に割り当てられるため、メッセージを利用した後は sqlite3_free(_: ) 関数を呼び出して解放する必要があります。これはC言語のメモリ管理に相当する部分で、SwiftでC APIを直接扱う際の注意点です。

sqlite3_exec は簡単なSQL文の実行には便利ですが、SELECT文で結果を取得したり、ユーザーからの入力を安全にSQL文に含めたりする場合には向きません。特に、ユーザー入力を直接SQL文字列に結合すると、SQLインジェクション攻撃のリスクが生じます。これを避けるためには、プリペアドステートメントを使用するのが安全かつ効率的です。

6. プリペアドステートメント (Prepared Statements)

プリペアドステートメントは、SQL文のテンプレートをあらかじめコンパイルしておき、後からパラメータの値をバインドして複数回実行できる仕組みです。以下の利点があります。

  • セキュリティ: パラメータの値はSQL文の一部としてではなく、独立したデータとして扱われるため、SQLインジェクションを防ぐことができます。
  • パフォーマンス: SQL文の解析とコンパイルが一度だけ行われるため、同じSQL文を繰り返し実行する場合に高速です。
  • 利便性: 様々な型のデータを安全にバインドできます。

プリペアドステートメントの基本的な流れは以下のようになります。

  1. SQL文のテンプレートを準備し、sqlite3_prepare_v2 関数でコンパイルしてステートメントオブジェクト (sqlite3_stmt*) を取得する。
  2. ステートメントオブジェクトのパラメータに値をバインドする (sqlite3_bind_* 関数)。
  3. sqlite3_step 関数を呼び出してステートメントを実行する。SELECT文の場合は、sqlite3_stepを繰り返し呼び出すことで結果セットの各行を取得する。
  4. 結果セットのカラムから値を取得する (sqlite3_column_* 関数)。
  5. ステートメントをリセットするか、sqlite3_finalize 関数で解放する。

Swiftでプリペアドステートメントを扱う場合、ステートメントオブジェクトも OpaquePointer? 型で表現します。

6.1 データの挿入 (INSERT)

INSERT INTO 文を使ってデータを挿入する際にプリペアドステートメントを使ってみましょう。

“`swift
// データベース接続が開かれている状態を想定
// var db: OpaquePointer? は有効なポインタを保持しているとする

func insertUser(db: OpaquePointer?, name: String, age: Int?) {
let insertSQL = “INSERT INTO users (name, age) VALUES (?, ?);”

var statement: OpaquePointer? = nil

// SQL文をプリペアドステートメントとしてコンパイル
// sqlite3_prepare_v2(DBポインタ, SQL文(C文字列), SQL文のバイト数(-1で自動検出), ステートメントポインタのアドレス, 未使用のSQLのポインタ)
let rc = sqlite3_prepare_v2(db, insertSQL.cString(using: .utf8), -1, &statement, nil)

if rc == SQLITE_OK {
    // パラメータのバインド
    // パラメータは1から始まるインデックスで指定する
    // sqlite3_bind_text(ステートメント, パラメータインデックス, 値(C文字列), 値のバイト数(-1で自動検出), デストラクタ)
    sqlite3_bind_text(statement, 1, name.cString(using: .utf8), -1, nil)

    // 年齢 (Optional Integer) のバインド
    if let age = age {
        // sqlite3_bind_int(ステートメント, パラメータインデックス, 値)
        sqlite3_bind_int(statement, 2, Int32(age)) // SwiftのIntは通常64bitだが、SQLiteは32bit/64bit両方対応。ここではInt32に変換
    } else {
        // 値がnilの場合はNULLをバインド
        // sqlite3_bind_null(ステートメント, パラメータインデックス)
        sqlite3_bind_null(statement, 2)
    }

    // ステートメントの実行
    // sqlite3_step(ステートメント)
    let stepResult = sqlite3_step(statement)

    if stepResult == SQLITE_DONE {
        print("Successfully inserted user: \(name).")
    } else {
        // ステートメント実行時のエラー処理
        let errMsg = sqlite3_errmsg(db)
        let swiftErrMsg = String(cString: errMsg!)
        print("Failed to insert user: \(swiftErrMsg) (result code: \(stepResult))")
    }

} else {
    // プリペアドステートメント作成時のエラー処理
    let errMsg = sqlite3_errmsg(db)
    let swiftErrMsg = String(cString: errMsg!)
    print("Failed to prepare statement: \(swiftErrMsg) (result code: \(rc))")
}

// ステートメントの解放
// sqlite3_finalize(ステートメント)
sqlite3_finalize(statement)

}

// 使用例
// Предполагается, что dbが有効なデータベース接続ポインタである
// insertUser(db: db, name: “Alice”, age: 30)
// insertUser(db: db, name: “Bob”, age: nil) // 年齢不明のユーザー
“`

解説:

  • let insertSQL = "INSERT INTO users (name, age) VALUES (?, ?);": SQL文のテンプレートです。値を入れたい場所に ? をプレースホルダーとして指定します。パラメータは ? の代わりに :name, @age, $variable のような名前付きパラメータを使用することもできますが、? が最も一般的で簡単です。
  • sqlite3_prepare_v2(_:_:_:_:_:): SQL文を解析し、ステートメントオブジェクトを作成します。
    • 第1引数: データベース接続ポインタ。
    • 第2引数: SQL文のC文字列。
    • 第3引数: SQL文のバイト数。-1を指定すると、最初のnull終端文字までを自動的に解析します。
    • 第4引数: コンパイルされたステートメントオブジェクトを格納するための sqlite3_stmt* ポインタのアドレス。
    • 第5引数: 指定されたSQL文字列の中に、解析されずに残った部分がある場合に、その先頭を指すポインタを受け取るアドレス。通常は不要なのでnilです。
  • 戻り値 rc: SQLITE_OK なら成功。それ以外はエラー。
  • sqlite3_bind_text(_:_:_:_:_:), sqlite3_bind_int(_:_:_:), sqlite3_bind_null(_:_: ): パラメータに値をバインドする関数です。
    • 第1引数: ステートメントオブジェクトポインタ。
    • 第2引数: バインドするパラメータのインデックス。? プレースホルダーはSQL文に出現する順に1から始まります。
    • 続く引数は、バインドする値とその型によって異なります。_textは文字列、_intは整数、_nullはNULLです。他の型 (_real for REAL, _blob for BLOB) に対応する関数もあります。
    • sqlite3_bind_textの最後の引数(デストラクタ):バインドしたC文字列データが解放される際に呼び出される関数を指定できます。nilを指定すると、SQLiteは必要に応じて内部で文字列のコピーを作成します。もし自分で管理しているメモリへのポインタを渡す場合は、SQLITE_STATICを指定するとSQLiteはデータのコピーを作成せず、ポインタが有効な間はそのメモリを直接参照します。またはSQLITE_TRANSIENTを指定するとSQLiteはデータのコピーを作成します。通常はnilまたはSQLITE_TRANSIENTで安全です。
  • sqlite3_step(_: ): プリペアドステートメントを実行します。INSERT, UPDATE, DELETE 文のようなデータ変更操作の場合、成功すれば SQLITE_DONE を返します。SELECT文の場合は、結果セットの次の行に進むたびに SQLITE_ROW を返します(詳細は後述)。エラーが発生した場合はエラーコードを返します。
  • SQLITE_DONE: sqlite3_stepが、操作が完了し、これ以上結果(行)がないことを示した成功コードです。INSERT, UPDATE, DELETE の場合は通常これを返します。
  • sqlite3_finalize(_: ): ステートメントオブジェクトを解放し、関連するリソースをクリーンアップします。プリペアドステートメントの使用が終わったら、必ず呼び出す必要があります。

6.2 データの取得 (SELECT)

SELECT 文を使ってテーブルからデータを取得する場合もプリペアドステートメントを使用するのが一般的です。SELECT の場合は、sqlite3_step を繰り返し呼び出して結果セットの各行を取得し、sqlite3_column_* 関数を使って行のカラムから値を取り出します。

“`swift
// データベース接続が開かれている状態を想定
// var db: OpaquePointer? は有効なポインタを保持しているとする

struct User {
let id: Int
let name: String
let age: Int?
}

func queryUsers(db: OpaquePointer?) -> [User] {
let querySQL = “SELECT id, name, age FROM users WHERE age > ? OR age IS NULL ORDER BY name;”

var statement: OpaquePointer? = nil
var users = [User]()

// プリペアドステートメントのコンパイル
let rc = sqlite3_prepare_v2(db, querySQL.cString(using: .utf8), -1, &statement, nil)

if rc == SQLITE_OK {
    // パラメータのバインド (例: age > 25 のユーザーを検索)
    sqlite3_bind_int(statement, 1, Int32(25))

    // 結果セットの取得 (行ごとにループ)
    // sqlite3_step() が SQLITE_ROW を返す間、次の行がある
    while sqlite3_step(statement) == SQLITE_ROW {
        // 各カラムの値を取得
        // sqlite3_column_int(ステートメント, カラムインデックス) - カラムインデックスは0から始まる
        let id = Int(sqlite3_column_int64(statement, 0)) // id (INTEGER)

        // sqlite3_column_text(ステートメント, カラムインデックス)
        let namePointer = sqlite3_column_text(statement, 1) // name (TEXT)
        let name = String(cString: namePointer!) // 非Optional Stringに変換

        // sqlite3_column_int(ステートメント, カラムインデックス) または sqlite3_column_int664
        let ageValue = sqlite3_column_int(statement, 2) // age (INTEGER)
        // NULLかどうかをチェック
        let isAgeNull = (sqlite3_column_type(statement, 2) == SQLITE_NULL)
        let age: Int? = isAgeNull ? nil : Int(ageValue)

        // 取得したデータでUserオブジェクトを作成し、配列に追加
        users.append(User(id: id, name: name, age: age))
    }

    // ループを抜けた後、stepの結果がSQLITE_DONEまたはエラーコードかを確認
    let finalStepResult = sqlite3_step(statement) // ループ条件で既にstepされているが、念のため確認
    if finalStepResult != SQLITE_DONE && finalStepResult != SQLITE_ROW {
        // エラーが発生した場合
        let errMsg = sqlite3_errmsg(db)
        let swiftErrMsg = String(cString: errMsg!)
        print("Error after fetching rows: \(swiftErrMsg) (result code: \(finalStepResult))")
        // ここで適切なエラーハンドリングを行う必要がある
    }


} else {
    // プリペアドステートメント作成時のエラー処理
    let errMsg = sqlite3_errmsg(db)
    let swiftErrMsg = String(cString: errMsg!)
    print("Failed to prepare query statement: \(swiftErrMsg) (result code: \(rc))")
}

// ステートメントの解放
sqlite3_finalize(statement)

return users

}

// 使用例
// Предполагается, что dbが有効なデータベース接続ポインタである
// let userList = queryUsers(db: db)
// print(“Found (userList.count) users:”)
// for user in userList {
// print(” ID: (user.id), Name: (user.name), Age: (user.age ?? -1)”)
// }
“`

解説:

  • struct User { ... }: 取得したデータを保持するためのSwiftの構造体を定義しています。
  • let querySQL = "SELECT id, name, age FROM users WHERE age > ? OR age IS NULL ORDER BY name;": クエリSQL文です。WHERE句で条件を指定し、ORDER BY句で結果の順序を指定しています。ここでも?をプレースホルダーとして使用しています。age IS NULLは、ageがNULLである行を選択するための構文です。
  • sqlite3_prepare_v2(...): クエリSQL文をコンパイルします。
  • sqlite3_bind_int(statement, 1, Int32(25)): 最初の?プレースホルダーに整数値25をバインドしています。
  • while sqlite3_step(statement) == SQLITE_ROW: sqlite3_step 関数を繰り返し呼び出します。
    • SQLITE_ROW: プリペアドステートメントが SELECT 文であり、結果セットに次の行がある場合に返されます。この場合、その行のカラムの値を取得できます。
    • SQLITE_DONE: 結果セットの全ての行の処理が完了した場合、または非 SELECT 文の実行が完了した場合に返されます。ループはこのコードで終了します。
    • エラーコード: 実行中にエラーが発生した場合に返されます。ループは終了し、エラーハンドリングが必要です。
  • sqlite3_column_int64(_:_: ), sqlite3_column_text(_:_: ), sqlite3_column_type(_:_: ): 現在処理中の行のカラムから値を取得する関数です。
    • 第1引数: ステートメントオブジェクトポインタ。
    • 第2引数: 取得するカラムのインデックス。SELECT文のSELECT句に記述されたカラムの順序で、0から始まります。
    • _int64: INTEGER型のカラムから64bit整数として値を取得します。
    • _text: TEXT型のカラムからC文字列ポインタとして値を取得します。返されるポインタはSQLiteの内部メモリを指しており、sqlite3_step または sqlite3_finalize が次に呼び出されるまでのみ有効です。取得したC文字列ポインタからSwiftのStringを作成しておけば安全です。
    • _type: カラムに格納されている値のデータ型を示すコードを返します (SQLITE_INTEGER, SQLITE_REAL, SQLITE_TEXT, SQLITE_BLOB, SQLITE_NULL)。これにより、NULL値かどうかを正確に判定できます。
  • カラムのインデックスは、SELECT句で指定したカラムの順序と一致します。上記の例では、idがインデックス0、nameがインデックス1、ageがインデックス2です。
  • sqlite3_finalize(_: ): 結果セットの処理が完了したら、ステートメントオブジェクトを解放します。

6.3 データの更新 (UPDATE)

UPDATE 文を使ったデータ更新も、プリペアドステートメントで行います。

“`swift
// データベース接続が開かれている状態を想定
// var db: OpaquePointer? は有効なポインタを保持しているとする

func updateUserAge(db: OpaquePointer?, name: String, newAge: Int) {
let updateSQL = “UPDATE users SET age = ? WHERE name = ?;”

var statement: OpaquePointer? = nil

let rc = sqlite3_prepare_v2(db, updateSQL.cString(using: .utf8), -1, &statement, nil)

if rc == SQLITE_OK {
    // パラメータのバインド
    // age = ? (パラメータインデックス 1) に新しい年齢をバインド
    sqlite3_bind_int(statement, 1, Int32(newAge))
    // WHERE name = ? (パラメータインデックス 2) に名前をバインド
    sqlite3_bind_text(statement, 2, name.cString(using: .utf8), -1, nil)

    // ステートメントの実行
    let stepResult = sqlite3_step(statement)

    if stepResult == SQLITE_DONE {
        print("Successfully updated age for user: \(name).")
        // 更新された行数を知りたい場合は sqlite3_changes(db) を呼び出す
        let changes = sqlite3_changes(db)
        print("Number of rows updated: \(changes)")
    } else {
        let errMsg = sqlite3_errmsg(db)
        let swiftErrMsg = String(cString: errMsg!)
        print("Failed to update user age: \(swiftErrMsg) (result code: \(stepResult))")
    }

} else {
    let errMsg = sqlite3_errmsg(db)
    let swiftErrMsg = String(cString: errMsg!)
    print("Failed to prepare update statement: \(swiftErrMsg) (result code: \(rc))")
}

sqlite3_finalize(statement)

}

// 使用例
// updateUserAge(db: db, name: “Alice”, newAge: 32)
“`

解説:

  • let updateSQL = "UPDATE users SET age = ? WHERE name = ?;": UPDATE 文のテンプレートです。SET句とWHERE句にそれぞれプレースホルダー?を使用しています。
  • sqlite3_bind_int(statement, 1, Int32(newAge))sqlite3_bind_text(statement, 2, ...): ?の出現順に従って、最初の? (age) にnewAgeを、二番目の? (name) にnameをバインドしています。
  • sqlite3_changes(_: ): 最後のINSERT, UPDATE, DELETE 文によって変更されたデータベース行の数を返します。データベース接続ポインタを引数に取ります。

6.4 データの削除 (DELETE)

DELETE FROM 文を使ったデータ削除も同様にプリペアドステートメントで行います。

“`swift
// データベース接続が開かれている状態を想定
// var db: OpaquePointer? は有効なポインタを保持しているとする

func deleteUser(db: OpaquePointer?, name: String) {
let deleteSQL = “DELETE FROM users WHERE name = ?;”

var statement: OpaquePointer? = nil

let rc = sqlite3_prepare_v2(db, deleteSQL.cString(using: .utf8), -1, &statement, nil)

if rc == SQLITE_OK {
    // パラメータのバインド
    // WHERE name = ? (パラメータインデックス 1) に名前をバインド
    sqlite3_bind_text(statement, 1, name.cString(using: .utf8), -1, nil)

    // ステートメントの実行
    let stepResult = sqlite3_step(statement)

    if stepResult == SQLITE_DONE {
        print("Successfully deleted user: \(name).")
        let changes = sqlite3_changes(db)
        print("Number of rows deleted: \(changes)")
    } else {
        let errMsg = sqlite3_errmsg(db)
        let swiftErrMsg = String(cString: errMsg!)
        print("Failed to delete user: \(swiftErrMsg) (result code: \(stepResult))")
    }

} else {
    let errMsg = sqlite3_errmsg(db)
    let swiftErrMsg = String(cString: errMsg!)
    print("Failed to prepare delete statement: \(swiftErrMsg) (result code: \(rc))")
}

sqlite3_finalize(statement)

}

// 使用例
// deleteUser(db: db, name: “Bob”)
“`

解説:

  • let deleteSQL = "DELETE FROM users WHERE name = ?;": DELETE 文のテンプレートです。
  • sqlite3_bind_text(statement, 1, ...): WHERE句の?に名前をバインドしています。
  • sqlite3_changes(_: ): 削除された行数を確認するために使用できます。

7. エラーハンドリングの詳細

SQLite C APIは、多くの関数がエラーコードを返します。これらのコードを適切にチェックし、エラー発生時に原因を特定して対応することが堅牢なアプリケーションを構築する上で非常に重要です。

  • リターンコード: ほとんどのSQLite関数は int 型のリターンコードを返します。SQLITE_OK (値は0) は成功を示します。それ以外の非ゼロの値はエラーを示します。SQLiteには多くの定義済みエラーコード定数があります(SQLITE_ERROR, SQLITE_BUSY, SQLITE_LOCKED, SQLITE_CORRUPT, SQLITE_CONSTRAINTなど)。これらの値を確認することで、どのような種類のエラーが発生したかを特定できます。
  • sqlite3_errmsg(_: ): これは、指定されたデータベース接続オブジェクト (sqlite3*) に対して最後に実行された操作で発生したエラーの詳細なメッセージをC文字列として返します。エラーコードだけでは原因が特定しにくい場合でも、このメッセージは具体的な情報を提供してくれます。ただし、この関数が返すメッセージはあくまで補助的な情報であり、エラーハンドリングの主要なロジックはリターンコードに基づいて行うべきです。

エラーハンドリングの基本パターンは以下のようになります。

“`swift
let rc = some_sqlite_function(…)

if rc != SQLITE_OK {
// エラーが発生した場合
let errMsg = sqlite3_errmsg(db) // データベース接続ポインタを渡す
let swiftErrMsg = String(cString: errMsg!)
print(“SQLite error (Code (rc)): (swiftErrMsg)”)

// ここでエラーコード (rc) に応じた具体的な処理を行う
// 例:
// if rc == SQLITE_BUSY {
//     // データベースがロックされている場合の処理 (リトライなど)
// } else if rc == SQLITE_CONSTRAINT {
//     // 制約違反(例: ユニーク制約に違反)
// } else {
//     // その他のエラー
// }

// 必要に応じて例外をスローしたり、エラー値を返したりする
// throw SomeDatabaseError.sqliteError(code: rc, message: swiftErrMsg)

} else {
// 成功した場合の処理
}
“`

プリペアドステートメントの場合、sqlite3_prepare_v2 のリターンコードと、その後の sqlite3_step のリターンコードの両方をチェックする必要があります。sqlite3_prepare_v2 はステートメントのコンパイルエラー(SQL構文間違いなど)を示し、sqlite3_step は実行時のエラー(制約違反、I/Oエラーなど)を示します。

また、sqlite3_exec 関数でエラーメッセージポインタを渡した場合、エラー発生時にはそのポインタ経由でメッセージを取得できますが、このメッセージは sqlite3_free で解放する必要があります。sqlite3_errmsg が返すポインタはSQLiteが管理しているため、通常解放する必要はありません。

SwiftのResult型やthrowsキーワードを使って、よりSwiftらしいエラーハンドリングを実装することも可能です。後述のデータベースヘルパークラスの実装例でこれを示します。

8. トランザクション

トランザクションは、複数のデータベース操作(INSERT, UPDATE, DELETEなど)を一つの論理的な単位としてまとめる仕組みです。トランザクション内の全ての操作が成功した場合にのみ、その変更がデータベースに永続的に反映されます(コミット)。途中の操作で一つでも失敗した場合、トランザクション開始前の状態に戻されます(ロールバック)。これにより、データベースの整合性を保つことができます。

SQLiteでトランザクションを開始するには BEGIN TRANSACTION または BEGIN を実行します。トランザクションを終了するには COMMIT または END TRANSACTION で変更を確定するか、ROLLBACK で変更を取り消します。

これらのコマンドはSQL文なので、sqlite3_exec またはプリペアドステートメントで実行できます。単純なコマンドなので sqlite3_exec が適しています。

“`swift
// データベース接続が開かれている状態を想定
// var db: OpaquePointer? は有効なポインタを保持しているとする

func performBatchInsert(db: OpaquePointer?, users: [(name: String, age: Int?)]) {
// トランザクション開始
let beginSQL = “BEGIN TRANSACTION;”
var rc = sqlite3_exec(db, beginSQL.cString(using: .utf8), nil, nil, nil)
if rc != SQLITE_OK {
print(“Failed to begin transaction.”)
return
}

let insertSQL = "INSERT INTO users (name, age) VALUES (?, ?);"
var statement: OpaquePointer? = nil

// プリペアドステートメントのコンパイル (トランザクション内で複数回実行するため)
rc = sqlite3_prepare_v2(db, insertSQL.cString(using: .utf8), -1, &statement, nil)
if rc != SQLITE_OK {
    print("Failed to prepare insert statement.")
    // prepareが失敗した場合でも、もしbeginが成功していたらrollbackを試みるべき
    sqlite3_exec(db, "ROLLBACK;", nil, nil, nil)
    return
}

var success = true
for user in users {
    // パラメータのバインド
    sqlite3_bind_text(statement, 1, user.name.cString(using: .utf8), -1, nil)
    if let age = user.age {
        sqlite3_bind_int(statement, 2, Int32(age))
    } else {
        sqlite3_bind_null(statement, 2)
    }

    // ステートメントの実行
    let stepResult = sqlite3_step(statement)

    if stepResult != SQLITE_DONE {
        // 個別のINSERT操作が失敗した場合
        let errMsg = sqlite3_errmsg(db)
        let swiftErrMsg = String(cString: errMsg!)
        print("Failed to insert user \(user.name): \(swiftErrMsg) (result code: \(stepResult))")
        success = false
        // エラーが発生したらループを中断し、ロールバックに進む
        break
    }

    // ステートメントをリセットして次のバインドと実行に備える
    // sqlite3_reset() はバインドされた値をクリアし、ステートメントを最初の状態に戻す
    sqlite3_reset(statement)
    // sqlite3_clear_bindings() はバインドされた値だけをクリアする
    // sqlite3_reset() はステップ位置もリセットするため、こちらを使う
}

// ステートメントの解放
sqlite3_finalize(statement)

// トランザクションの終了 (全て成功ならCOMMIT、一つでも失敗ならROLLBACK)
let finishSQL = success ? "COMMIT;" : "ROLLBACK;"
rc = sqlite3_exec(db, finishSQL.cString(using: .utf8), nil, nil, nil)

if rc == SQLITE_OK {
    print("Transaction finished: \(success ? "COMMIT" : "ROLLBACK").")
} else {
    let action = success ? "COMMIT" : "ROLLBACK"
    print("Failed to \(action) transaction.")
    // トランザクション終了自体のエラーは深刻な場合が多い
}

}

// 使用例
// let newUsers = [(“Charlie”, 25), (“David”, nil), (“Eve”, 29)]
// performBatchInsert(db: db, users: newUsers)
“`

解説:

  • BEGIN TRANSACTION;: トランザクションを開始します。
  • COMMIT;: トランザクション内の全ての操作を確定し、変更を永続化します。
  • ROLLBACK;: トランザクション内の全ての操作を取り消し、開始前の状態に戻します。
  • バッチ処理(複数のINSERTなど)を行う際にトランザクションを使用すると、個別のINSERTを繰り返すよりもはるかに高速になることが多いです。SQLiteはデフォルトで各SQL文を自動的にトランザクションとして扱いますが、複数の文を一つの明示的なトランザクションにまとめることで、ディスクへの書き込み回数を減らすことができます。
  • トランザクション処理中は、エラーが発生した場合に必ず ROLLBACK を実行してデータベースの不整合を防ぐことが重要です。
  • sqlite3_reset(_: ): プリペアドステートメントを再利用する際に使用します。sqlite3_step によって進んだ実行状態をリセットし、再度 sqlite3_bind_* で値をバインドして sqlite3_step で実行できるようにします。これにより、SQL文を再コンパイルすることなく複数回実行できます。これはバッチ処理やループ内でのクエリ実行で特に重要です。

9. データベース操作を構造化する

C APIを直接扱うコードは、ポインタが多く、エラーチェックが煩雑になりがちです。Swiftコードをより安全で扱いやすくするために、データベース操作をカプセル化したクラスや構造体を作成することを強く推奨します。これにより、データベースの接続・切断管理、ステートメントの準備・解放、エラー処理などを一箇所にまとめ、利用側からはSwiftらしいインターフェースでデータベースを操作できるようになります。

簡単なデータベースヘルパークラスの例を以下に示します。この例では、データベースファイルのパス管理、接続/切断、SQL実行機能のみを含んでいます。実際のアプリケーションでは、CRUD操作やトランザクション処理などをラップするメソッドを追加していくことになります。

“`swift
import Foundation
// import sqlite3 (Bridging Headerによってインポートされる)

enum DatabaseError: Error {
case openDatabaseError(message: String)
case prepareStatementError(message: String)
case stepStatementError(message: String)
case bindParameterError(message: String) // バインド関数自体のエラーは少ないが、念のため
case sqlExecutionError(message: String) // sqlite3_exec のエラー
case unknownError(message: String = “An unknown database error occurred.”)

// SQLiteのリターンコードからDatabaseErrorを生成するヘルパー
static func fromResultCode(_ rc: Int32, db: OpaquePointer?, statement: OpaquePointer? = nil) -> DatabaseError {
    let errMsg: String
    if let db = db, let cMsg = sqlite3_errmsg(db) {
        errMsg = String(cString: cMsg)
    } else if let statement = statement, let dbFromStmt = sqlite3_db_handle(statement), let cMsg = sqlite3_errmsg(dbFromStmt) {
         errMsg = String(cString: cMsg)
    }
    else {
        errMsg = "Unknown error message for code \(rc)."
    }

    switch rc {
    case SQLITE_OK, SQLITE_ROW, SQLITE_DONE:
        // 成功を示すコードはエラーではない
        return .unknownError(message: "Called from a success code \(rc).") // これは実際には発生しないはず
    case SQLITE_CANTOPEN, SQLITE_IOERR, SQLITE_CORRUPT, SQLITE_NOTADB:
        return .openDatabaseError(message: "Open/Access Error (\(rc)): \(errMsg)")
    case SQLITE_CONSTRAINT:
         return .stepStatementError(message: "Constraint Error (\(rc)): \(errMsg)") // 通常step時に発生
    case SQLITE_MISUSE:
        return .unknownError(message: "Misuse Error (\(rc)): \(errMsg)") // APIの誤った使い方
    default:
        // prepare, step, exec など、どこで発生したかによって適切なケースに分類するのが理想だが、
        // シンプルにするため、特定のコード以外は汎用的なエラーとして扱う
        if statement != nil {
             return .stepStatementError(message: "Statement Error (\(rc)): \(errMsg)")
        } else if db != nil {
             return .sqlExecutionError(message: "Exec Error (\(rc)): \(errMsg)")
        } else {
             return .unknownError(message: "SQLite Error (\(rc)): \(errMsg)")
        }
    }
}

}

class DatabaseHelper {
private let dbPath: String
private var db: OpaquePointer? // データベース接続ポインタ

init(databaseFileName: String) {
    // アプリケーションのDocumentsディレクトリのパスを取得
    let fileURL = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
        .appendingPathComponent(databaseFileName)
    self.dbPath = fileURL.path // URLからパス文字列を取得

    // 初期化時にデータベースを開くこともできるが、ここでは明示的なopen/closeメソッドを提供する
    print("Database path: \(dbPath)")
}

deinit {
    // オブジェクトが解放されるときにデータベース接続を閉じる
    closeDatabase()
}

// データベース接続を開く
func openDatabase() throws {
    guard db == nil else {
        print("Database is already open.")
        return // 既に開いている場合は何もしない
    }

    let flags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX // フルミューテックスは通常不要だが、ここでは例として含める

    let rc = sqlite3_open_v2(dbPath, &db, flags, nil)

    guard rc == SQLITE_OK else {
        // 接続失敗
        // エラーメッセージを取得する前にdbポインタがnilでないことを確認
        let dbForMsg = db // sqlite3_errmsgはnilを受け取っても良いが、確実にポインタを渡す
        let error = DatabaseError.fromResultCode(rc, db: dbForMsg)

        // 失敗した場合、dbポインタを必ずnilに戻すか、クローズを試みる
        if db != nil {
             sqlite3_close(db) // 開こうとして失敗してもポインタが割り当てられている場合がある
             db = nil // ポインタをクリア
        } else {
             // dbポインタ自体が取得できなかった場合 (rc != SQLITE_OKだがdbがnil)
        }

        throw error
    }

    // 接続成功
    print("Successfully opened database.")
}

// データベース接続を閉じる
func closeDatabase() {
    guard db != nil else {
        // 既に閉じているか、開かれていない場合は何もしない
        return
    }

    let rc = sqlite3_close(db)

    if rc == SQLITE_OK {
        print("Successfully closed database.")
        db = nil // ポインタをクリア
    } else {
        // クローズ失敗 (稀だが発生しうる - 例: 未終了のステートメントがある)
        // ここでのエラー処理は難しい場合がある。ログ出力などが主。
        let errMsg = sqlite3_errmsg(db) // クローズ失敗の場合でもメッセージ取得できるか要確認
        let swiftErrMsg = String(cString: errMsg!)
        print("Failed to close database (\(rc)): \(swiftErrMsg)")

        // 未終了のステートメントがある場合など、sqlite3_close() は SQLITE_BUSY を返す
        // その場合は、保留中のステートメントを解放してから再度クローズを試みる必要がある
        // 簡潔にするため、ここではログ出力のみとする
    }
}

// MARK: - SQL Execution

// 結果セットを返さないSQL文 (CREATE TABLE, INSERT, UPDATE, DELETE など) を実行
// Simple fire-and-forget execution
func executeSQL(_ sql: String) throws {
    guard let db = db else {
        throw DatabaseError.unknownError(message: "Database connection is not open.")
    }

    // sqlite3_exec はプリペアドステートメントではないため、単純なクエリに限定
    // SQLインジェクションのリスクがあるため、ユーザー入力を含むSQLには使用しないこと!
    // このメソッドは主にテーブル作成など、固定のSQLに使用することを想定
    let rc = sqlite3_exec(db, sql.cString(using: .utf8), nil, nil, nil) // エラーメッセージポインタは省略

    guard rc == SQLITE_OK else {
        throw DatabaseError.fromResultCode(rc, db: db)
    }

    print("Successfully executed SQL: \(sql)")
}

// プリペアドステートメントを準備するヘルパー (内部利用向け)
private func prepareStatement(_ sql: String) throws -> OpaquePointer? {
     guard let db = db else {
        throw DatabaseError.unknownError(message: "Database connection is not open.")
    }

    var statement: OpaquePointer? = nil
    let rc = sqlite3_prepare_v2(db, sql.cString(using: .utf8), -1, &statement, nil)

    guard rc == SQLITE_OK else {
        sqlite3_finalize(statement) // prepare失敗時でもstatementがnilでない場合があるため解放を試みる
        throw DatabaseError.fromResultCode(rc, db: db)
    }

    return statement
}

// プリペアドステートメントを実行し、結果セットが必要な場合は処理する
// このメソッドはSELECT用。INSERT/UPDATE/DELETE用は別に用意するか、コールバックでラップするなど
func executeQuery<T>(_ querySQL: String,
                    parameters: [Any?] = [], // バインドするパラメータの配列
                    rowMapper: (OpaquePointer?) -> T) throws -> [T] // 結果行をSwiftオブジェクトにマッピングするクロージャ
{
    guard let db = db else {
        throw DatabaseError.unknownError(message: "Database connection is not open.")
    }

    var statement: OpaquePointer? = nil
    var results = [T]()

    // プリペアドステートメントの準備
    let rc = sqlite3_prepare_v2(db, querySQL.cString(using: .utf8), -1, &statement, nil)

    guard rc == SQLITE_OK else {
        sqlite3_finalize(statement)
        throw DatabaseError.fromResultCode(rc, db: db)
    }

    // パラメータのバインド
    for (index, parameter) in parameters.enumerated() {
        // パラメータインデックスは1から始まるため +1 する
        let bindResult = bindParameter(statement: statement, index: Int32(index + 1), value: parameter)
        guard bindResult == SQLITE_OK else {
            sqlite3_finalize(statement)
            throw DatabaseError.fromResultCode(bindResult, statement: statement) // バインドエラー
        }
    }

    // 結果セットの取得と処理
    while sqlite3_step(statement) == SQLITE_ROW {
        // rowMapper クロージャを使って現在の行からSwiftオブジェクトを生成
        let rowObject = rowMapper(statement)
        results.append(rowObject)
    }

    // ステートメントの最終ステップの結果を確認 (SQLITE_DONE またはエラー)
    let finalStepResult = sqlite3_step(statement) // ループ条件で既にstepされているが、念のため最終結果を取得
     guard finalStepResult == SQLITE_DONE || finalStepResult == SQLITE_ROW else { // SQLITE_ROW はループ中で処理済み
         // step中にエラーが発生した場合
         sqlite3_finalize(statement)
         throw DatabaseError.fromResultCode(finalStepResult, statement: statement)
     }


    // ステートメントの解放
    sqlite3_finalize(statement)

    return results
}


// INSERT, UPDATE, DELETE など、結果セットを返さないプリペアドステートメントを実行
 func executeUpdate(_ sql: String, parameters: [Any?] = []) throws -> Int32 {
     guard let db = db else {
         throw DatabaseError.unknownError(message: "Database connection is not open.")
     }

     var statement: OpaquePointer? = nil

     let rc = sqlite3_prepare_v2(db, sql.cString(using: .utf8), -1, &statement, nil)

     guard rc == SQLITE_OK else {
         sqlite3_finalize(statement)
         throw DatabaseError.fromResultCode(rc, db: db)
     }

     // パラメータのバインド
     for (index, parameter) in parameters.enumerated() {
         let bindResult = bindParameter(statement: statement, index: Int32(index + 1), value: parameter)
         guard bindResult == SQLITE_OK else {
              sqlite3_finalize(statement)
             throw DatabaseError.fromResultCode(bindResult, statement: statement)
         }
     }

     // ステートメントの実行
     let stepResult = sqlite3_step(statement)

     // INSERT/UPDATE/DELETE は通常 SQLITE_DONE を返す
     guard stepResult == SQLITE_DONE else {
         sqlite3_finalize(statement)
         throw DatabaseError.fromResultCode(stepResult, statement: statement)
     }

     // ステートメントの解放
     sqlite3_finalize(statement)

     // 影響を受けた行数を返す (sqlite3_changes() は直前の UPDATE, INSERT, DELETE の影響行数を返す)
     return sqlite3_changes(db)
 }


// パラメータをプリペアドステートメントにバインドする内部ヘルパー
private func bindParameter(statement: OpaquePointer?, index: Int32, value: Any?) -> Int32 {
    switch value {
    case let text as String:
        return sqlite3_bind_text(statement, index, text.cString(using: .utf8), -1, SQLITE_TRANSIENT) // SQLITE_TRANSIENT はコピーを作成
    case let intValue as Int:
        return sqlite3_bind_int64(statement, index, Int64(intValue)) // Intは通常64bitなのでint64でバインド
    case let doubleValue as Double:
        return sqlite3_bind_double(statement, index, doubleValue)
    case is NSNull, .none: // nil または NSNull
        return sqlite3_bind_null(statement, index)
    case let data as Data:
         // BLOBとしてバインド
         let bytes: [UInt8] = data.withUnsafeBytes { buffer in
             Array(buffer.bindMemory(to: UInt8.self))
         }
        return sqlite3_bind_blob(statement, index, bytes, Int32(bytes.count), SQLITE_TRANSIENT)
    default:
        print("Unsupported type for binding: \(type(of: value))")
        // 未対応の型はNULLとして扱うか、エラーとするか設計次第
        return sqlite3_bind_null(statement, index) // またはエラーコードを返す
    }
}

// MARK: - Column Value Retrieval Helpers (QueryメソッドのrowMapperクロージャ内で使用)

// ステートメントから指定したカラムのINTEGER値を取得
func getInt(statement: OpaquePointer?, columnIndex: Int32) -> Int {
    return Int(sqlite3_column_int64(statement, columnIndex))
}

// ステートメントから指定したカラムのTEXT値を取得
func getText(statement: OpaquePointer?, columnIndex: Int32) -> String? {
    guard let textPointer = sqlite3_column_text(statement, columnIndex) else {
        return nil // NULLの場合
    }
    return String(cString: textPointer)
}

// ステートメントから指定したカラムのREAL値を取得
func getDouble(statement: OpaquePointer?, columnIndex: Int32) -> Double {
    return sqlite3_column_double(statement, columnIndex)
}

// ステートメントから指定したカラムのBLOB値を取得
func getData(statement: OpaquePointer?, columnIndex: Int32) -> Data? {
    guard let blobPointer = sqlite3_column_blob(statement, columnIndex) else {
        return nil // NULLの場合
    }
    let count = sqlite3_column_bytes(statement, columnIndex) // BLOBのバイト数を取得
    return Data(bytes: blobPointer, count: Int(count))
}

// ステートメントから指定したカラムの値がNULLかどうかをチェック
func isNull(statement: OpaquePointer?, columnIndex: Int32) -> Bool {
    return sqlite3_column_type(statement, columnIndex) == SQLITE_NULL
}

}

// MARK: – 使用例

/*
// DatabaseHelperのインスタンス化
let dbHelper = DatabaseHelper(databaseFileName: “users.db”)

do {
// データベース接続を開く
try dbHelper.openDatabase()

// テーブル作成
let createTableSQL = """
CREATE TABLE IF NOT EXISTS users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    age INTEGER,
    profile_image BLOB
);
"""
try dbHelper.executeSQL(createTableSQL)

// データの挿入 (プリペアドステートメント)
let insertSQL = "INSERT INTO users (name, age, profile_image) VALUES (?, ?, ?);"

// 画像データ (例)
let sampleImageData = UIImage(systemName: "person.fill")?.pngData()

try dbHelper.executeUpdate(insertSQL, parameters: ["Alice", 30, sampleImageData]) // age: 30 (Int), profile_image: (Data)
try dbHelper.executeUpdate(insertSQL, parameters: ["Bob", nil, nil]) // age: nil, profile_image: nil

// データの取得 (プリペアドステートメント)
let querySQL = "SELECT id, name, age, profile_image FROM users WHERE age > ? OR age IS NULL;"
let minAge = 25 // 検索条件

// rowMapper クロージャで結果行を User オブジェクトにマッピング
struct User {
    let id: Int
    let name: String
    let age: Int?
    let profileImage: Data?
}

let userList = try dbHelper.executeQuery(querySQL, parameters: [minAge]) { statement in
    let id = dbHelper.getInt(statement: statement, columnIndex: 0)
    let name = dbHelper.getText(statement: statement, columnIndex: 1) ?? "N/A" // 名前はNOT NULLなのでnilにはならないはずだが安全のため
    let age = dbHelper.isNull(statement: statement, columnIndex: 2) ? nil : dbHelper.getInt(statement: statement, columnIndex: 2)
    let profileImage = dbHelper.getData(statement: statement, columnIndex: 3)

    return User(id: id, name: name, age: age, profileImage: profileImage)
}

print("\nQuery Results:")
for user in userList {
    print("  ID: \(user.id), Name: \(user.name), Age: \(user.age ?? -1), Has Image: \(user.profileImage != nil)")
}

// データの更新 (プリペアドステートメント)
let updateSQL = "UPDATE users SET age = ? WHERE name = ?;"
let updatedRows = try dbHelper.executeUpdate(updateSQL, parameters: [32, "Alice"]) // Aliceの年齢を32に更新
print("\nUpdated \(updatedRows) row(s) for Alice.")

// データの削除 (プリペアドステートメント)
let deleteSQL = "DELETE FROM users WHERE name = ?;"
let deletedRows = try dbHelper.executeUpdate(deleteSQL, parameters: ["Bob"]) // Bobを削除
print("Deleted \(deletedRows) row(s) for Bob.")

// 再度クエリして確認
let remainingUsers = try dbHelper.executeQuery("SELECT id, name, age FROM users;", parameters: []) { statement in
     let id = dbHelper.getInt(statement: statement, columnIndex: 0)
     let name = dbHelper.getText(statement: statement, columnIndex: 1) ?? "N/A"
     let age = dbHelper.isNull(statement: statement, columnIndex: 2) ? nil : dbHelper.getInt(statement: statement, columnIndex: 2)
     return User(id: id, name: name, age: age, profileImage: nil) // 画像カラムは取得しないクエリなのでnilを設定
}

print("\nRemaining Users:")
for user in remainingUsers {
     print("  ID: \(user.id), Name: \(user.name), Age: \(user.age ?? -1)")
}

} catch {
// データベース操作中に発生したエラーをキャッチ
print(“Database operation failed: (error)”)
}

// アプリケーション終了時または不要になったときにデータベース接続を閉じる (deinitでも行われるが明示的に呼んでも良い)
// dbHelper.closeDatabase() // deinitで自動的に呼ばれることが多いが、必要に応じて手動で
*/

“`

解説:

  • DatabaseError Enum: カスタムエラー型を定義することで、どのような種類のデータベースエラーが発生したかをより具体的に表現できます。fromResultCode ヘルパー関数は、SQLiteのリターンコードから適切な DatabaseError ケースにマッピングします。
  • DatabaseHelper クラス:
    • dbPath: データベースファイルのパスを保持します。Documentsディレクトリ内にファイルを作成しています。
    • db: データベース接続ポインタ OpaquePointer? を保持します。
    • init(databaseFileName: ): 初期化時にデータベースファイルのパスを決定します。
    • deinit: オブジェクトが解放される際に closeDatabase を呼び出し、リソースをクリーンアップします。
    • openDatabase(): sqlite3_open_v2 をラップし、接続を開きます。エラー発生時はthrowsで例外をスローします。成功時はdbポインタをセットします。
    • closeDatabase(): sqlite3_close をラップし、接続を閉じます。成功時はdbポインタをnilにクリアします。
    • executeSQL(_: ): sqlite3_exec をラップした単純なSQL実行メソッド。主にCREATE TABLEなど、パラメータを含まない固定のSQLに使用します。エラー発生時はthrowsします。
    • prepareStatement(_: ) (private): sqlite3_prepare_v2 をラップし、ステートメントを準備します。内部で利用することを想定しています。エラーをthrowsします。
    • executeQuery<T>(_:parameters:rowMapper: ): SELECT クエリを実行するためのジェネリックメソッドです。
      • querySQL: クエリ文字列。
      • parameters: バインドするパラメータの配列(Any? 型)。
      • rowMapper: 結果セットの各行をSwiftの任意の型Tのオブジェクトにマッピングするためのクロージャです。引数として現在のステートメントポインタを受け取り、その行のカラムから値を読み取り、T型のオブジェクトを返す責任を持ちます。これにより、データベースから取得したデータをSwiftの構造体やクラスのインスタンスとして簡単に扱えます。
      • メソッド内部では、ステートメントの準備、パラメータのバインド、sqlite3_step による行の取得ループ、rowMapper の呼び出し、そしてステートメントの解放を行います。各ステップでエラーチェックを行い、例外としてスローします。
    • executeUpdate(_:parameters: ): INSERT, UPDATE, DELETE など、結果セットを返さないプリペアドステートメントを実行するためのメソッドです。パラメータのバインドと sqlite3_step の実行を行い、成功した場合は sqlite3_changes で影響を受けた行数を返します。
    • bindParameter(statement:index:value: ) (private): 様々なSwiftの型(String, Int, Double, Data, nil)をSQLiteの対応する型にバインドするための内部ヘルパー関数です。これにより、利用側はC APIの具体的なバインド関数名を意識することなく、Swiftの値をそのまま渡すことができます。
    • getInt, getText, getDouble, getData, isNull (Helpers): executeQuery メソッドの rowMapper クロージャ内で結果のカラム値を取得する際に使用するヘルパーメソッド群です。C APIのカラム取得関数をラップし、Swiftの適切な型で値を返します。isNull はNULL値かどうかを判定するのに便利です。

この DatabaseHelper クラスは基本的なラップ機能を提供しますが、実際のアプリケーションではさらに機能を追加していくことになるでしょう。例えば、トランザクションをラップするメソッド、特定のテーブルに対するCRUD操作をメソッドとして提供する、マイグレーション機能を追加するなどです。

10. より高レベルなラッパーライブラリの紹介

SQLite C APIを直接扱うことは、SQLiteの仕組みを理解する上で非常に勉強になりますが、Swiftコードを書く上でいくつかの課題があります。

  • C API特有の煩雑さ: ポインタの管理、C文字列とSwift文字列間の変換、手動でのリソース解放 (sqlite3_finalize, sqlite3_free など) が必要になり、ボイラープレートコードが多くなりがちです。
  • エラーハンドリングの複雑さ: 各API関数のリターンコードをチェックし、適切にエラーメッセージを取得し、リソースをクリーンアップするロジックを手動で記述する必要があります。
  • Swiftらしさの欠如: Optional型やResult型、async/awaitなどのSwiftの現代的な機能と連携させにくい場合があります。
  • 型安全性: カラムからの値取得はカラムインデックスを指定して行いますが、コンパイル時にはカラムの名前や型がチェックされないため、実行時に型変換エラーが発生する可能性があります。

これらの課題を解決し、Swiftでより安全かつ効率的にSQLiteデータベースを扱うために、多くの優れたラッパーライブラリが存在します。代表的なものに以下の二つがあります。

  • FMDB: Objective-Cで書かれたSQLiteラッパーですが、Swiftからも問題なく利用できます。シンプルで実績があり、広く使われています。Objective-Cベースのため、Swiftらしい構文には完全にはなりませんが、C APIよりははるかに扱いやすいです。PodやCarthageで導入できます。
  • GRDB.swift: Swiftでゼロから書かれたSQLiteラッパーです。Swiftらしい構文、強力な型安全性、Functional Programmingの要素を取り入れたクエリ構築、非同期処理サポート、Codable対応など、現代的なSwift開発に適した多くの機能を提供します。Swift Package Managerで簡単に導入できます。

これらのライブラリを利用することで、C APIを直接扱う必要がなくなり、より少ないコードでデータベース操作を記述できます。例えば、GRDBではSwiftの構造体を定義し、それに従ってテーブルを作成したり、構造体のインスタンスを直接保存・取得したりすることが可能です。

“`swift
// GRDB.swift を使用した場合の例 (コードは簡略化しています)
import GRDB

struct User: Codable, FetchableRecord, MutablePersistableRecord {
var id: Int62? // Optional because it’s auto-incrementing
var name: String
var age: Int?

mutating func didInsert(with rowID: Int64, for column: String?) {
    id = Int64(rowID)
}

}

// データベースファイルの準備 (例: ドキュメントディレクトリ)
let fileManager = FileManager.default
let dbPath = try! fileManager
.url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
.appendingPathComponent(“users.sqlite”)
.path

// データベース接続
let dbQueue = try! DatabaseQueue(path: dbPath)

// スキーマ定義 (マイグレーション)
try! dbQueue.write { db in
try db.create(table: “user”, ifNotExists: true) { t in
t.autoIncrementedPrimaryKey(“id”)
t.column(“name”, .text).notNull()
t.column(“age”, .integer)
}
}

// データの挿入
var alice = User(name: “Alice”, age: 30)
try! dbQueue.write { db in
try alice.insert(db) // IDが自動的にセットされる
}

var bob = User(name: “Bob”, age: nil)
try! dbQueue.write { db in
try bob.insert(db)
}

// データの取得
let users: [User] = try! dbQueue.read { db in
try User.filter(Column(“age”) > 25 || Column(“age”).isNull).order(Column(“name”)).fetchAll(db)
}
print(users)

// データの更新
try! dbQueue.write { db in
try db.execute(sql: “UPDATE user SET age = ? WHERE name = ?”, arguments: [32, “Alice”])
}

// データの削除
try! dbQueue.write { db in
try db.execute(sql: “DELETE FROM user WHERE name = ?”, arguments: [“Bob”])
}
``
GRDBを使うと、Swiftの
Codable`プロトコルを利用して、Swiftの構造体とデータベースの行を簡単に相互変換できます。また、型安全なクエリビルダや、Reactive ProgrammingのためのCombine連携なども提供されています。

C APIを学ぶことはSQLiteの基礎を理解する上で価値がありますが、実際のアプリケーション開発では、このような高レベルなラッパーライブラリを利用する方が生産性、安全性、メンテナンス性の観点から推奨されます。この記事でC APIの基礎を学んだ知識は、ラッパーライブラリを使った際に何が行われているのかを理解する助けになるでしょう。

11. まとめ

この記事では、SwiftでSQLiteデータベースを扱うための基本的な知識と実践的な方法について、SQLite C APIを中心に詳細に解説しました。

  • SQLiteは軽量で組み込みに適したデータベースエンジンであり、iOS/macOS開発におけるデータの永続化に適しています。
  • SwiftからSQLiteを利用するには、プロジェクトにlibsqlite3.tbdをリンクし、Bridging Headerファイルでsqlite3.hをインポートする必要があります。
  • データベース操作は sqlite3* ポインタを介して行われ、sqlite3_open_v2 で接続、sqlite3_close で切断します。
  • テーブルの作成や単純なSQL実行には sqlite3_exec が利用できますが、結果セットの取得や安全なパラメータの扱いのためにはプリペアドステートメント (sqlite3_prepare_v2, sqlite3_bind_*, sqlite3_step, sqlite3_column_*, sqlite3_finalize) を使用するのが一般的です。
  • エラーハンドリングは、各API関数のリターンコードを確認し、必要に応じて sqlite3_errmsg で詳細なメッセージを取得することが重要です。
  • 複数の操作をまとめて実行し、データの整合性を保つためにトランザクション (BEGIN, COMMIT, ROLLBACK) を利用できます。
  • C APIの直接利用は低レベルで煩雑なため、データベース操作をカプセル化する独自のヘルパークラスを作成するか、FMDBやGRDBのような既存のSwift/Objective-Cラッパーライブラリを利用するのが実用的です。

この記事で学んだC APIの知識は、SQLiteの内部動作を理解する上で貴重な基礎となります。実際のプロジェクトでは、この基礎知識を活かしつつ、GRDBなどのモダンなラッパーライブラリを利用して、より効率的でメンテナンス性の高いデータベース実装を目指すのが良いでしょう。

データベースはアプリケーションの根幹をなす重要な部分です。堅牢で効率的なデータベース処理を実装することで、アプリケーションの信頼性とパフォーマンスを向上させることができます。この記事が、あなたのSwift開発におけるSQLiteデータベース活用の第一歩となれば幸いです。


コメントする

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

上部へスクロール