Swift 4 で SQLite を使う

とりあえず右も左も分からない状況から初めてみる。

当初SQLiteのテーブルの管理が面倒と思い、SQLPluginなどのプラグインでデータベースを管理できればいいかなと思ったけれど、無知がゆえXcode 8以降では Alcatraz とかのPluginをXcode上で使うには未署名のものをどうのこうのとWarningがでたので取り合えずは後回しに。

Pluginを使う場合の大まかな流れはAlcatrazなどのパッケージマネージャー(これもPlugin)をインストール。update_xcode_updatesコマンド(別途gemでインストールが必要)を使ってPluginを使えるようにする必要があるようです。個人的なメモ。

事前準備で無駄に時間かかった・・・(経験値不足)

  1. SQLiteのブラウザを用意する
  2. アプリ経由でテーブルを作成してみる
  3. DB Browser for SQLiteでテーブルを変更してみる
  4. 作成、挿入、参照をコードで実装してみる

1. SQLiteのブラウザを用意する

SQLPro for SQLite (App Storeで¥2600)がいいかなと思ったけど、試しに使って見たらなんとなくしっくりこなかった。ので堅実にフリーのDB Browser for SQLiteに。

SQLPro for SQLite
SQLPro for SQLite ダウンロード(App Store)

DB Browser for SQLite
DB Browser for SQLite ダウンロード

ダウンロード後dmgファイルを開いてデータベースのマークをDrag & Dropでフォルダに入れる。

2. アプリ経由でテーブルを作成してみる

とりあえず適当なSingle View Appを作って SQLite3 をインポートするようにする。そのあと、テーブル作成用のSQL文を指定して実行する。

import UIKit
import SQLite3
class ViewController: UIViewController {
    var db: OpaquePointer?
    var dbfile: String = "sample.db"
    
    override func viewDidLoad() {
        super.viewDidLoad()

        let fileURL = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false).appendingPathComponent(self.dbfile)
        if sqlite3_open(fileURL.path, &db) != SQLITE_OK {
            print("Error: database file open error.")
        }
        
        if sqlite3_exec(db, "CREATE TABLE IF NOT EXISTS foobar (id INTEGER PRIMARY KEY AUTOINCREMENT, guid TEXT)", nil, nil, nil) != SQLITE_OK {
            print("Error: SQL execution error.")
        }
    }
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
}

データベースファイル(sample.db)はエミュレータ配下の以下の様な場所に作成される。

/Users/foobar/Library/Developer/CoreSimulator/Devices/
   1CCE826B-5B4E-4548-8185-E4DF9B3B0090/data/Containers/Data/
   Application/43E40AAB-1F9E-4893-A5EC-B6AA795CFD9E/Documents

3. DB Browser for SQLiteでテーブルを変更してみる

DB Browser for SQLiteでアプリ実行時に生成されたファイルを指定して開く。

まずはFinderでLibraryを表示できる様にする。 Finderでユーザのディレクトリを表示した状態で → を選択。 表示...

ファイルを開いたらテーブルを選択したのち、Modify Tableでテーブルを編集する。

4. 作成、挿入、参照をコードで実装してみる

SQLite用の主要な操作用のクラス。

class SQLite {
    var op: OpaquePointer?
    var state: OpaquePointer?
    var databaseFile: String
    init(){
        self.op = nil
        self.state = nil
        self.databaseFile = ""
    }
    fileprivate func open(databaseFile: String) {
        sqlite3_shutdown()
        sqlite3_initialize()
        print("isThreadsafe: \(sqlite3_threadsafe())")

        self.databaseFile = databaseFile
        let fileURL = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false).appendingPathComponent(self.databaseFile)
        print("Database file: \(fileURL)")
        if sqlite3_open_v2(fileURL.path, &op, SQLITE_OPEN_CREATE|SQLITE_OPEN_READWRITE|SQLITE_OPEN_FULLMUTEX, nil) != SQLITE_OK {
            print("Error: Unable to connect database file (\(fileURL)).")
        }
    }
    fileprivate func create(tableName: String, sql: String) {
        if sqlite3_exec(self.op, sql, nil, nil, nil) != SQLITE_OK {
            print("Error: Unable to create \(tableName)) table.")
        }
    }
    fileprivate func prepare(table tableName: String, sql: String) {
        if sqlite3_prepare_v2(self.op, sql, -1, &(self.state), nil) != SQLITE_OK {
            let msg = String(cString: sqlite3_errmsg(self.op)!)
            print("Error: Unable preparing to insert in \(tableName)/ \(msg)")
            return
        }
    }
    fileprivate func step() {
        if sqlite3_step(self.state) != SQLITE_DONE {
            let msg = String(cString: sqlite3_errmsg(self.op)!)
            print("Error: Unable to insert / \(msg)")
        }
    }
    fileprivate func insert(table: String, sql: String, closure: () -> Void) {
        self.prepare(table: table, sql: sql)
        closure()
        self.step()
        sqlite3_finalize(self.state)
    }
    fileprivate func bind_text(order: Int32, value: String) -> Bool {
        if sqlite3_bind_text(self.state, order, value.cString(using: String.Encoding.utf8), -9, nil) != SQLITE_OK {
            let msg = String(cString: sqlite3_errmsg(self.op)!)
            print("Error: Unable binding text to insert / \(msg)")
            return false
        }
        return true
    }
    fileprivate func bind_int(order: Int32, value: Int32) -> Bool {
        if sqlite3_bind_int(self.state, order, value) != SQLITE_OK {
            let msg = String(cString: sqlite3_errmsg(self.op)!)
            print("Error: Unable binding int to insert / \(msg)")
            return false
        }
        return true
    }
    fileprivate func query(table: String, sql: String, closure: () -> Void) {
        self.prepare(table: table, sql: sql)
        while(sqlite3_step(self.state) == SQLITE_ROW) {
            closure()
        }
    }
}

あれやこれやとやろうと思ったらかなりオレオレクラスに・・・

これを使うためのSQLite用風ラッパー(未完)

class SQLiteColumn<Element> {
    var value: Element
    var notAllowNull: Bool = false
    var primaryKey: Bool = false
    var autoIncremental: Bool = false
    var uniqueKey: Bool = false
    var defaultValue: Element? = nil
    init(value: Element, notAllowNull: Bool = false, primaryKey: Bool = false, autoIncremental: Bool = false, uniqueKey: Bool = false, defaultValue: Element? = nil) {
        self.value = value
        self.notAllowNull = notAllowNull
        self.primaryKey = primaryKey
        self.autoIncremental = autoIncremental
        self.uniqueKey = uniqueKey
        self.defaultValue = defaultValue
    }
}

class SQLiteTable<T> {
    var name = String(describing: T.self)
    var columns: [(String?, Any)]
    
    init(db: SQLite, define: T) {
        self.columns = Mirror(reflecting: define).children.compactMap { $0 }
        var sql = "CREATE TABLE IF NOT EXISTS \(self.name) ("
        for (index, column) in self.columns.enumerated() {
            if index >= 1 {
                sql.append(", ")
            }
            if let columnName = column.0 {
                sql.append(columnName)

                let (_, columnType) = getProperty(column: column)
                switch columnType {
                case "Int32":
                    sql.append(" INTEGER")
                    sql.append((column.1 as! SQLiteColumn<Int32>).notAllowNull == true ? " NOT NULL" : "")
                    sql.append((column.1 as! SQLiteColumn<Int32>).primaryKey == true ? " PRIMARY KEY" : "")
                    sql.append((column.1 as! SQLiteColumn<Int32>).autoIncremental == true ? " AUTOINCREMENT" : "")
                    sql.append((column.1 as! SQLiteColumn<Int32>).uniqueKey == true ? " UNIQUE" : "")
                case "String":
                    sql.append(" TEXT")
                    sql.append((column.1 as! SQLiteColumn<String>).notAllowNull == true ? " NOT NULL" : "")
                    sql.append((column.1 as! SQLiteColumn<String>).primaryKey == true ? " PRIMARY KEY" : "")
                    sql.append((column.1 as! SQLiteColumn<String>).autoIncremental == true ? " AUTOINCREMENT" : "")
                    sql.append((column.1 as! SQLiteColumn<String>).uniqueKey == true ? " UNIQUE" : "")
                default: break
                }
            }
        }
        sql.append(")")
        db.create(tableName: self.name, sql: sql)
    }
    func getProperty(column: (String?, Any)) -> (String, String) {
        if let name = column.0 {
            let columnType = String(describing: type(of: column.1))
            if columnType.starts(with: "SQLiteColumn<Int32>") == true {
                return (name, "Int32")
            } else if columnType.starts(with: "SQLiteColumn<String>") == true {
                return (name, "String")
            }
        }
        return ("", "")
    }
    func getValue(columnType: String, column: (String?, Any)) -> Any {
        var value: Any
        switch columnType {
        case "Int32":
            value = (column.1 as! SQLiteColumn<Int32>).value
        case "String":
            value = (column.1 as! SQLiteColumn<String>).value
        default:
            value = ""
        }
        return value
    }

    func insert(db: SQLite, item: T) {
        var prefix = ""
        var suffix = ""
        self.columns = Mirror(reflecting: item).children.compactMap { $0 }
        for (index, column) in columns.enumerated() {
            if index >= 1 {
                prefix.append(", ")
                suffix.append(", ")
            }
            let (name, _) = getProperty(column: column)
            prefix.append(name)
            suffix.append("?")
        }
        let sql = "INSERT INTO \(self.name) (\(prefix)) VALUES (\(suffix))"
        db.insert(table: self.name, sql: sql, closure: {
            for (index, column) in columns.enumerated() {
                let (_, columnType) = getProperty(column: column)
                switch columnType {
                case "Int32":
                    let _ = db.bind_int(order: Int32(index + 1), value: getValue(columnType: columnType, column: column) as! Int32)
                case "String":
                    let _ = db.bind_text(order: Int32(index + 1), value: getValue(columnType: columnType, column: column) as! String)
                default: break
                }
            }
        })
    }
    func query(db: SQLite, define: T, closure: ([Any]) -> Void) {
        let sql = "SELECT * FROM \(self.name)"
        let columns = Mirror(reflecting: define).children.compactMap { $0 }
        db.query(table: self.name, sql: sql, closure: {
            var row = [Any]()
            for (index, column) in columns.enumerated() {
                let (_, columnType) = getProperty(column: column)
                switch columnType {
                case "Int32":
                    row.append( sqlite3_column_int(db.state, Int32(index)) )
                case "String":
                    row.append( String(cString: sqlite3_column_text(db.state, Int32(index))))
                default: break
                }
            }
            closure(row)
        })
    }
}

使う場合は適当なStructureをラッパーで用意。

class Book {
    var name: SQLiteColumn<String>
    var price: SQLiteColumn<Int32>
    init() {
        self.name = SQLiteColumn<String>(value: "", notAllowNull: true )
        self.price = SQLiteColumn<Int32>(value: 0)
    }
    convenience init(name: String, price: Int32) {
        self.init()
        self.name = SQLiteColumn<String>(value: name)
        self.price = SQLiteColumn<Int32>(value: price)
    }
}

で、上記をテーブルとして使うコードが以下。

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let db = SQLite()
        db.open(databaseFile: "sample.db")

        let bookTable = SQLiteTable<Book>(db: db, define: Book())
        bookTable.insert(db: db, item: Book(name: "Morning", price: 370))
        bookTable.insert(db: db, item: Book(name: "Young Jump", price: 350))
        bookTable.insert(db: db, item: Book(name: "Jump", price: 320))
        bookTable.insert(db: db, item: Book(name: "Sunday", price: 350))

        var books = [Book]()
        bookTable.query(db: db, define: Book(), closure: { row in
            books.append(Book(name: row[0] as! String, price: row[1] as! Int32))
        })
        books.forEach({ print("\($0.name.value) \t ¥\($0.price.value)") })
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
}

実行結果

Morning 	 ¥370
Young Jump 	 ¥350
Jump 	 ¥320
Sunday 	 ¥350

デーブルを宣言する際にデータベースファイル内にテーブルがなければStructureに合わせて作成する作りにしてあります。今の所、Int32とStringのみ実装。

色々知識不足が甚だしい。もう少しなんとかできないものか。

Genericsとprotocolについてもっと理解を深めないと、、;

参考
Swift SQLite Tutorial for Beginners – Using SQLite in iOS Application

フォローする