GRDB.swiftの生SQL発行とマイグレーション

個人開発したアプリの宣伝
目的地が設定できる手帳のような使い心地のTODOアプリを公開しています。
Todo with Location

Todo with Location

  • Yoshiko Ichikawa
  • Productivity
  • Free

GRDB.swiftの生SQL発行とmigrationを試したのでメモ。

github.com

僕のXcode + Swiftのバージョンでは、公式通りのドキュメント + αが必要だったので記録しておく。

インストール

podを使用した。

Podfile

pod 'GRDB.swift'
$ pod install

で、これでworkspaceファイルで起動すれば、GRDBライブラリ を読み込めるはずだが、何故かライブラリ自体のビルドが通らない。。。

多分、

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()

とりあえず、やりたかった目的は達成できたので満足。