GRDB.swiftの生SQL発行とmigrationを試したのでメモ。
Swiftでカジュアルに生SQLでSQLiteの操作したいんだけど、GRDB.swift入れるしかなないのかなあ...
— Fumiya Ichikawa (@LET__IT__RIDE) December 15, 2019
Androidはめっちゃ簡単なんだけどなあ。
僕のXcode + Swiftのバージョンでは、公式通りのドキュメント + αが必要だったので記録しておく。
インストール
podを使用した。
Podfile
pod 'GRDB.swift'
$ pod install
で、これでworkspaceファイルで起動すれば、GRDBライブラリ を読み込めるはずだが、何故かライブラリ自体のビルドが通らない。。。
ん? podからGRDB入れてもライブラリ自体のビルドが通らないんだけどw
— Fumiya Ichikawa (@LET__IT__RIDE) December 15, 2019
多分、
GRDBPrecondition(current?.allows(db) ?? false, message, file: file, line: line)
が呼ばれている箇所で、Add () to forward @autoclosure parameter
のようなエラーが出ているはずだ。
これはXCodeの指示に従って、message()
とすることでビルドを通した。
dbファイルのオープン
僕の場合、Document/ に配置した。Library/Application Support/とお好みに応じて変更すれば良い。dbファイルはなければ作成される。
let databaseFile = NSHomeDirectory() + "/Documents/" let queue = try! DatabaseQueue(path: databaseFile + "application.sqlite")
SQLの実行
readerステートメントとwriterステートメントで実行する。
DDL
let databaseFile = NSHomeDirectory() + "/Documents/" let queue = try! DatabaseQueue(path: databaseFile + "application.sqlite") try! queue.write({ (db) in try! db.execute("create table foo ( c1 INT, c2 TEXT)") })
INSERT
let databaseFile = NSHomeDirectory() + "/Documents/" let queue = try! DatabaseQueue(path: databaseFile + "application.sqlite") try! queue.write({ (db) in try! db.execute("insert into foo (c1, c2)values(?, ?)", arguments: [1, "hoge"]) })
SELECT文
let databaseFile = NSHomeDirectory() + "/Documents/" let queue = try! DatabaseQueue(path: databaseFile + "application.sqlite") try! queue.read{ db in rows = try Row.fetchAll(db, "select c1, c2 from foo where bar = ? and hoge = ?", arguments: [1, "hoge"] ) rows.forEach { (row) in print(row["columnname"]) } }
Row
はGRDBでDBレコードを表すクラス。クラスメソッドでSQLを実行できる。
で、[Row]
の配列が返却されるので、ハッシュ表現でカラムが持つ値にアクセスできる
rows.forEach { (row) in print(row["c1"]) }
もちろん、都度DBファイルをオープンするのは良くないし、ライブラリのアップデート時のメンテコストを考えて、以下のようなWrapperクラスを作成した。
import GRDB class MySQLite{ private static let databaseFile = NSHomeDirectory() + "/Documents/" private static var dbQueue:DatabaseWriter? init(){ self.connect() } private func connect(){ guard nil == MySQLite.dbQueue else{ return } do{ MySQLite.dbQueue = try DatabaseQueue(path: MySQLite.databaseFile + "addtionalQRapplication.sqlite") }catch{} } public func migrate(){ let migrator = MigrationSQL().getMigrator() try! migrator.migrate(MySQLite.dbQueue!) } public func execute(sql:String, arguments:StatementArguments? = nil){ do{ try MySQLite.dbQueue?.write({ (db) in try db.execute(sql, arguments: arguments) }) }catch{ } } public func select(sql:String, arguments:StatementArguments? = nil)->[Row]{ var rows = [Row]() do{ try MySQLite.dbQueue?.read{ db in rows = try Row.fetchAll(db, sql, arguments: arguments ) } }catch{} return rows } }
let sqlite = MySQLite() let rows = sqlite.select(sql:"select * from foo")
のように利用すれば良い。dbハンドラは、最初に必ずメインスレッドでmigrateを実行する前提でスレッドセーフにしてない(笑)
これで一応、GRDBへの依存がこのファイルと後述するマイグレーションファイルのみとなる。はず。
migration
マイグレーションは
var migrator = DatabaseMigrator() migrator.registerMigration("v1") { (db) in try db.execute(""" CREATE TABLE foo ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL ) """) try db.execute(""" CREATE index foo_name_idx on foo (name) """) } migrator.registerMigration("v2") { (db) in try db.execute(""" alter table foo add foo_profile TEXT """) } migrator.registerMigration("v3") { (db) in try db.execute(""" alter table foo add age INT """) } let databaseFile = NSHomeDirectory() + "/Documents/" let queue = try! DatabaseQueue(path: databaseFile + "application.sqlite") try! migrator.migrate(queue)
registerMigration()の引数にインクリメントするバージョンを指定することでマイグレーション管理ができる。
上記の場合、v2まで実行されているアプリはv3を実行。まだv1を実行してないユーザはv1〜v3まで実行というように、実行実績のあるバージョン以降のSQLを実行する。
尚、試してないが、v3の後にv2を実行というようなことはできないようなので、実行順序は必ずインクリメントに沿って記述しよう。
こちらは、アプリバージョンによってどんどん記述が増えていく処理なので以下のようなマイグレーションを管理するファイルを作ってみた。
import GRDB class MigrationSQL{ var migrator = DatabaseMigrator() func getMigrator()->DatabaseMigrator{ migrator.registerMigration("v1") { (db) in self.v1MigrateSQL(db) } migrator.registerMigration("v2") { (db) in self.v2MigrateSQL(db) } migrator.registerMigration("v3") { (db) in self.v3MigrateSQL(db) } return migrator } func v1MigrateSQL(_ db:Database){ do{ try db.execute(""" CREATE TABLE foo ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, value DOUBLE, count INT) """) try db.execute(""" CREATE index foo_name_idx on foo (name) """) }catch{} } func v2MigrateSQL(_ db:Database){ do{ try db.execute(""" drop index foo_name_idx """) }catch{} } func v3MigrateSQL(_ db:Database){ do{ try db.execute(""" alter table foo add group_id text default '1' """) }catch{} } }
こんな感じでアプリ起動時に使用する。
MySQLite().migrate()
とりあえず、やりたかった目的は達成できたので満足。
とりあえず、GRDBから生SQL発行するwrapperクラスとmigrationの検証まで済ませたー。
— Fumiya Ichikawa (@LET__IT__RIDE) December 15, 2019