iOSアプリ開発でSQLiteを使う FMDB

iOSアプリ開発で、SQLiteを使ってデータ管理する方法を調べてみました。SQLiteをそのまま使うよりは、SQLiteをラッパーしたFMDBライブラリを使うとより便利です。そちらもご紹介いたします。

iOSアプリ開発ではじめてSQLiteを使う

libsqlite3.tbdをプロジェクトに追加

libsqlite3.tbdをプロジェクトに追加します。

Xcodeのプロジェクトナビゲータでプロジェクト名を選択し、「Build Phases」タブに移動します。「Link Binary With Libraries」セクションで、+ボタンを押して、libsqlite3.tbdを追加します。

ソースコード

swift
//
//  ViewController.swift
//  SampleSqlite
//
//  Created by Toshihiko Arai on 2024/10/11.
//

import UIKit
import SQLite3

// 日本語文字を登録するために必要な定数
let SQLITE_TRANSIENT = unsafeBitCast(-1, to: sqlite3_destructor_type.self)

class ViewController: UIViewController {
    
    var db: OpaquePointer?

    override func viewDidLoad() {
        super.viewDidLoad()
        // データベースの準備
        if let databasePath = createDatabase() {
            openDatabase(path: databasePath)
            createTable()
            insertData(name: "空条承太郎", age: 40)
            insertData(name: "空条徐倫", age: 17)
            fetchData()
        }
    }

    // データベースのパスを取得して作成
    func createDatabase() -> String? {
        let fileURL = try? FileManager.default
            .url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
            .appendingPathComponent("sample.sqlite")
        return fileURL?.path
    }

    // データベースのオープン
    func openDatabase(path: String) {
        if sqlite3_open(path, &db) != SQLITE_OK {
            print("データベースを開くことができません")
        }
    }

    // テーブルの作成
    func createTable() {
        let createTableString = """
        CREATE TABLE IF NOT EXISTS Person(
        Id INTEGER PRIMARY KEY AUTOINCREMENT,
        Name CHAR(255),
        Age INTEGER);
        """
        var createTableStatement: OpaquePointer?
        if sqlite3_prepare_v2(db, createTableString, -1, &createTableStatement, nil) == SQLITE_OK {
            if sqlite3_step(createTableStatement) == SQLITE_DONE {
                print("テーブルの作成に成功しました")
            } else {
                print("テーブルの作成に失敗しました")
            }
        } else {
            print("テーブル作成の準備に失敗しました")
        }
        sqlite3_finalize(createTableStatement)
    }

    // データの挿入
    func insertData(name: String, age: Int32) {
        let insertStatementString = "INSERT INTO Person (Name, Age) VALUES (?, ?);"
        var insertStatement: OpaquePointer?
        if sqlite3_prepare_v2(db, insertStatementString, -1, &insertStatement, nil) == SQLITE_OK {

            sqlite3_bind_text(insertStatement, 1, name, -1, SQLITE_TRANSIENT)
            sqlite3_bind_int(insertStatement, 2, age)
            if sqlite3_step(insertStatement) == SQLITE_DONE {
                print("データの挿入に成功しました")
            } else {
                print("データの挿入に失敗しました")
            }
        } else {
            print("挿入ステートメントの準備に失敗しました")
        }
        sqlite3_finalize(insertStatement)
    }

    // データの取得
    func fetchData() {
        let queryStatementString = "SELECT * FROM Person;"
        var queryStatement: OpaquePointer?
        if sqlite3_prepare_v2(db, queryStatementString, -1, &queryStatement, nil) == SQLITE_OK {
            while sqlite3_step(queryStatement) == SQLITE_ROW {
                let id = sqlite3_column_int(queryStatement, 0)
                let name = String(cString: sqlite3_column_text(queryStatement, 1))
                let age = sqlite3_column_int(queryStatement, 2)
                print("Query Result:")
                print("ID: \(id), Name: \(name), Age: \(age)")
            }
        } else {
            print("クエリの準備に失敗しました")
        }
        sqlite3_finalize(queryStatement)
    }
}

SQLiteのデータ型

SQLiteには4つの基本的なストレージクラス(データ型)があります。

  • INTEGER: 整数値
  • REAL: 浮動小数点数
  • TEXT: テキスト(文字列)
  • BLOB: バイナリデータ

SQLiteではCHARやVARCHARといった型はSQL標準に基づく記述としてサポートされていますが、内部的にはすべてTEXT型に変換されます。CHAR(255)と書いても、実際にはTEXTとして処理されるので問題なく動作しますが、厳密な文字数制限が行われるわけではありません。

macOSからsqliteファイルへアクセスする

実機に保存されたファイルにmacOSからアクセスする場合、通常はアプリのサンドボックス環境により直接アクセスできませんが、以下の方法でファイルを取得することが可能です。

アプリにファイル共有機能を追加して、SQLiteファイルをiTunes経由でmacOSに転送することも可能です。これには以下の手順が必要です:

Info.plist
<key>UIFileSharingEnabled</key>
<true/>

この設定を行うと、アプリの「Documents」ディレクトリに保存されたファイルはiTunesを通じてファイル共有が可能になります。iTunesまたはFinder(macOS Catalina以降)を使用して、SQLiteファイルをコピーできます。

感想

SQLite3を使った場合、SQLITE_TRANSIENT などのテキストバインドを適切に行わないと、DBに登録できない現象に見舞われました。規模が大きくなるほど、ソースコードは読みにくくなり、管理しずらくなる恐れを感じますね。。そこで、SQLite3をラッパーして扱いやすくしてくれるライブラリ、FMDBを使ってみました。

FMDBを使った方法

FMDBは、SQLite3よりも簡潔で読みやすいコードを提供し、SQLiteの操作をラップする便利な機能を持っています。

FMDBのインストール

PodでFMDBライブラリをインストールします。

Podfile
platform :ios, '12.0'

target 'SampleSqlite' do
  use_frameworks!

  # FMDBライブラリを追加
  pod 'FMDB'
end

Podfileを編集したら、以下のコマンドを実行してFMDBをインストールします。

zsh
pod install

ソースコード

以下は、先ほどのSQLite操作をFMDBを使って書き換えたコードです。

swift
//
//  ViewController.swift
//  SampleSqlite
//
//  Created by Toshihiko Arai on 2024/10/11.
//

import UIKit
import FMDB

class ViewController: UIViewController {
    
    var database: FMDatabase?

    override func viewDidLoad() {
        super.viewDidLoad()
        // データベースの準備
        if let databasePath = createDatabase() {
            openDatabase(path: databasePath)
            createTable()
            insertData(name: "空条承太郎", age: 40)
            insertData(name: "空条徐倫", age: 17)
            fetchData()
        }
    }

    // データベースのパスを取得して作成
    func createDatabase() -> String? {
        let fileURL = try? FileManager.default
            .url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
            .appendingPathComponent("sample.sqlite")
        return fileURL?.path
    }
    
    // データベースをオープン
    func openDatabase(path: String) {
        database = FMDatabase(path: path)
        if database?.open() == true {
            print("データベースを開くことができました")
        } else {
            print("データベースを開くことができません")
        }
    }

    // テーブルの作成
    func createTable() {
        let createTableSQL = """
        CREATE TABLE IF NOT EXISTS Person(
        Id INTEGER PRIMARY KEY AUTOINCREMENT,
        Name TEXT,
        Age INTEGER);
        """
        do {
            try database?.executeUpdate(createTableSQL, values: nil)
            print("テーブルの作成に成功しました")
        } catch {
            print("テーブルの作成に失敗しました: \(error.localizedDescription)")
        }
    }

    // データの挿入
    func insertData(name: String, age: Int) {
        let insertSQL = "INSERT INTO Person (Name, Age) VALUES (?, ?);"
        do {
            try database?.executeUpdate(insertSQL, values: [name, age])
            print("データの挿入に成功しました")
        } catch {
            print("データの挿入に失敗しました: \(error.localizedDescription)")
        }
    }

    // データの取得
    func fetchData() {
        let querySQL = "SELECT * FROM Person;"
        do {
            let resultSet: FMResultSet? = try database?.executeQuery(querySQL, values: nil)
            while resultSet?.next() == true {
                let id = resultSet?.int(forColumn: "Id") ?? 0
                let name = resultSet?.string(forColumn: "Name") ?? ""
                let age = resultSet?.int(forColumn: "Age") ?? 0
                print("Query Result: ID: \(id), Name: \(name), Age: \(age)")
            }
        } catch {
            print("データの取得に失敗しました: \(error.localizedDescription)")
        }
    }
}

感想

FMDBを使うと、だいぶスッキリしたソースコードになりましたね。シンプルなラッパー関数のようですし、SQLiteを使うならFMDB経由で操作したほうが良さそうです。

GitHub - ccgus/fmdb: A Cocoa / Objective-C wrapper around SQLite

Sandboxエラーが出て、ビルドできない場合

FMDBを使ってビルドしようとした時に、以下のようなエラーが出ました。

Sandbox: rsync.samba(4918) deny(1) file-write-create /Users/xxxxx/Library/Developer/Xcode/DerivedData/SampleSqlite-cpgxooycbitrhxbehgazdzkiumbh/Build/Products/Debug-iphoneos/SampleSqlite.app/Frameworks/FMDB.framework/FMDB_Privacy.bundle

対処方法

このビルドエラーを対処する方法ですが、Build Settingsの ENABLE_USER_SCRIPT_SANDBOXINGNo に変更することで解決できました。

ENABLE_USER_SCRIPT_SANDBOXINGをNoに変更したことでビルドできた理由は、アプリや依存するライブラリがスクリプトやファイルアクセスに関するサンドボックス制限に引っかかっていたためです。特に、rsyncやFMDBなどのライブラリが、ファイルシステムにアクセスしようとしてサンドボックス制限により拒否されていた可能性があるようです。

ENABLE_USER_SCRIPT_SANDBOXING とは

ENABLE_USER_SCRIPT_SANDBOXINGはXcodeプロジェクトのビルド設定オプションの1つで、ユーザースクリプトのサンドボックス化を有効または無効にする設定です。このオプションは、特にmacOSアプリやXcodeの一部の機能に関連して使用されます。

  • 有効(Yes)に設定: このオプションが有効な場合、アプリケーション内で実行されるユーザースクリプト(特定のスクリプトやJavaScriptコードなど)はサンドボックス化され、アプリのサンドボックス外にアクセスする権限が制限されます。セキュリティの観点から、アプリケーションが外部に悪影響を与えないようにするための保護機能が働きます。
  • 無効(No)に設定: 無効にすると、ユーザースクリプトのサンドボックス制限が解除され、スクリプトがより自由に外部のリソースやファイルシステムにアクセスできるようになります。これにより、ファイルの書き込みや読み取りなどの操作が可能になりますが、セキュリティリスクも増加します。

関連記事

最後までご覧いただきありがとうございます!

▼ 記事に関するご質問やお仕事のご相談は以下よりお願いいたします。
お問い合わせフォーム

関連記事