とりあえず右も左も分からない状況から初めてみる。
当初SQLiteのテーブルの管理が面倒と思い、SQLPluginなどのプラグインでデータベースを管理できればいいかなと思ったけれど、無知がゆえXcode 8以降では Alcatraz とかのPluginをXcode上で使うには未署名のものをどうのこうのとWarningがでたので取り合えずは後回しに。
Pluginを使う場合の大まかな流れはAlcatrazなどのパッケージマネージャー(これもPlugin)をインストール。update_xcode_updatesコマンド(別途gemでインストールが必要)を使ってPluginを使えるようにする必要があるようです。個人的なメモ。
事前準備で無駄に時間かかった・・・(経験値不足)
- SQLiteのブラウザを用意する
- アプリ経由でテーブルを作成してみる
- DB Browser for SQLiteでテーブルを変更してみる
- 作成、挿入、参照をコードで実装してみる
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でアプリ実行時に生成されたファイルを指定して開く。
ファイルを開いたらテーブルを選択したのち、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