Столкнувшись с задачей подключить SQLLite к своему мобильному приложению iOS через FMDB, я не нашел ни одного актуального гайда на русском языке. И тем более для Swift. В этой статье я постараюсь этого исправить.
В этом гайде будут использоваться файлы с objective-c, поэтому не надо ждать порта FMDB на Swift.
Скачать FMDB можно тут.
В FMDB три main class:
FMDatabase — представляет данных SQLite. Используется для выполнения SQL-операторов.
FMResultSet — представляет результаты выполнения запроса по FMDatabase.
FMDatabaseQueue — если вы хотите, чтобы выполнялись запросы и обновления на несколько потоков, можно использовать этот класс. Пример в 8 пункте.
Прежде чем вы сможете взаимодействовать с базой данных, она должен быть открыта. Открытие завершиться с ошибкой, если нет достаточных ресурсов или разрешения на открытие и/или создания базы данных.
Шаги:
1) Добавьте 'libsqlite3' стандартную библиотеку в настройках проекта и скопируйте FMDB файлы в ваш проект. (да, они на objective-c).
2) Создайте новый файл, который будет называться «FMDB-Bridging-Header.h». Внутри «Bridging-Header.h» напишите следующее: #import «FMDB.h».
3) Зайдите в Build Settings -> Swift Compiler — Code Generation и добавьте к 'Objective-C Bridging Header': FMDB-Bridging-Header.h.
Если файл в папке вашего проекта, то так: ИМЯ_ПАПКИ/FMDB-Bridging-Header.h
4) Скопируйте в Ваш проект SQLite database. В этом гайде я буду использовать название 'tempdb.sqlite' всего лишь с одной таблицей внутри:
CREATE TABLE test_tb ( test_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, keywordtext TEXT)
5) В вашем AppDelegate.swift's class AppDelegate добавьте следующие переменные: var dbFilePath: NSString = NSString()
Пример:
6) Добавьте этот метод в AppDelegate.swift's class AppDelegate:
7) Вызовите в AppDelegate.swift's func application:
8) В этом примере мы работаем с данными UITableViewController) используя FMDB:
9) Немного разных фишек, использование мультипотока FMDB через FMDatabaseQueue.
// вставка пяти строк
//давайте попробуем вставить строки, но сознательно ошибемся и посмотрим, что он откатывает правильно
// проверим, что только первые пять строк там
// удаляем таблицу
10) Стандартного на закуску. Использование класса executeUpdate(values:) в Swift2:
Использование queue:
Пример из стандартного описания:
Если что-то не получается, пишете, постараюсь помочь.
В этом гайде будут использоваться файлы с objective-c, поэтому не надо ждать порта FMDB на Swift.
Скачать FMDB можно тут.
В FMDB три main class:
FMDatabase — представляет данных SQLite. Используется для выполнения SQL-операторов.
FMResultSet — представляет результаты выполнения запроса по FMDatabase.
FMDatabaseQueue — если вы хотите, чтобы выполнялись запросы и обновления на несколько потоков, можно использовать этот класс. Пример в 8 пункте.
Прежде чем вы сможете взаимодействовать с базой данных, она должен быть открыта. Открытие завершиться с ошибкой, если нет достаточных ресурсов или разрешения на открытие и/или создания базы данных.
if (![db open]) {
[db release];
return;
}
Шаги:
1) Добавьте 'libsqlite3' стандартную библиотеку в настройках проекта и скопируйте FMDB файлы в ваш проект. (да, они на objective-c).
2) Создайте новый файл, который будет называться «FMDB-Bridging-Header.h». Внутри «Bridging-Header.h» напишите следующее: #import «FMDB.h».
3) Зайдите в Build Settings -> Swift Compiler — Code Generation и добавьте к 'Objective-C Bridging Header': FMDB-Bridging-Header.h.
Если файл в папке вашего проекта, то так: ИМЯ_ПАПКИ/FMDB-Bridging-Header.h
4) Скопируйте в Ваш проект SQLite database. В этом гайде я буду использовать название 'tempdb.sqlite' всего лишь с одной таблицей внутри:
CREATE TABLE test_tb ( test_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, keywordtext TEXT)
5) В вашем AppDelegate.swift's class AppDelegate добавьте следующие переменные: var dbFilePath: NSString = NSString()
Пример:
import UIKit
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var navi: UINavigationController?
var dbFilePath: NSString = NSString()
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: NSDictionary?) -> Bool {
....
6) Добавьте этот метод в AppDelegate.swift's class AppDelegate:
// MARK: - FMDB
let DATABASE_RESOURCE_NAME = "tempdb"
let DATABASE_RESOURCE_TYPE = "sqlite"
let DATABASE_FILE_NAME = "tempdb.sqlite"
func initializeDb() -> Bool {
let documentFolderPath = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)[0] as String
let dbfile = "/" + DATABASE_FILE_NAME;
self.dbFilePath = documentFolderPath.stringByAppendingString(dbfile)
let filemanager = NSFileManager.defaultManager()
if (!filemanager.fileExistsAtPath(dbFilePath) ) {
let backupDbPath = NSBundle.mainBundle().pathForResource(DATABASE_RESOURCE_NAME, ofType: DATABASE_RESOURCE_TYPE)
if (backupDbPath == nil) {
return false
} else {
var error: NSError?
let copySuccessful = filemanager.copyItemAtPath(backupDbPath, toPath:dbFilePath, error: &error)
if !copySuccessful {
println("copy failed: \(error?.localizedDescription)")
return false
}
}
}
return true
}
7) Вызовите в AppDelegate.swift's func application:
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: NSDictionary?) -> Bool {
if self.initializeDb() {
NSLog("Successful db copy")
}
8) В этом примере мы работаем с данными UITableViewController) используя FMDB:
import UIKit
class SecondViewController: UIViewController {
// MARK: - .H
@IBOutlet var dataTable: UITableView?
var dataArray:[MultiField] = []
// MARK: - .M
required init(coder: NSCoder) {
fatalError("NSCoding not supported")
}
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: NSBundle?) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
// Custom initialization
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
self.title = "FMDB Using Swift"
let mainDelegate: AppDelegate = UIApplication.sharedApplication().delegate as AppDelegate
// initialize FMDB
let db: FMDatabase = FMDatabase(path:mainDelegate.dbFilePath)
if (db.open() == nil) {
NSLog("error opening db")
}
// вставка данных
let addQuery = "INSERT INTO test_tb (name, keywordtext) VALUES ('excalibur', 'hot')"
let addSuccessful = db.executeUpdate(addQuery, withArgumentsInArray: nil)
if !addSuccessful {
println("insert failed: \(db.lastErrorMessage())")
}
// вставка данных - конец
// update данных
let updateQuery = "UPDATE test_tb SET keywordtext = 'cool' WHERE name = 'excalibur' "
let updateSuccessful = db.executeUpdate(updateQuery, withArgumentsInArray: nil)
if !updateSuccessful {
println("update failed: \(db.lastErrorMessage())")
}
// update данных - конец
// Получение данных из нашей базы и сохранение их в массив UITableView
let mainQuery = "SELECT name, keywordtext FROM test_tb"
let rsMain: FMResultSet? = db.executeQuery(mainQuery, withArgumentsInArray: [])
while (rsMain!.next() == true) {
let productName = rsMain?.stringForColumn("name")
let keywords = rsMain?.stringForColumn("keywordtext")
let multiField = MultiField(aField1: productName!, aField2: keywords!)
self.dataArray.append(multiField)
}
// получение данных - конец
// удаление данных
let delQuery = "DELETE FROM test_tb WHERE name = 'excalibur' "
let deleteSuccessful = db.executeUpdate(delQuery, withArgumentsInArray: nil)
if !deleteSuccessful {
println("delete failed: \(db.lastErrorMessage())")
}
// удаление данных - конец
// пример: получение номер строк
let rsTemp: FMResultSet? = db.executeQuery("SELECT count(*) AS numrows FROM test_tb", withArgumentsInArray: [])
rsTemp!.next()
let numrows = rsTemp?.intForColumn("numrows")
NSLog("numrows: \(numrows)")
//пример: получение номер строки - конец
db.close()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// MARK: - TableView DataSource
func numberOfSectionsInTableView(tableView: UITableView!) -> Int {
return 1
}
func tableView(tableView: UITableView!, numberOfRowsInSection section: Int) -> Int {
return self.dataArray.count
}
func tableView(tableView: UITableView!, cellForRowAtIndexPath indexPath: NSIndexPath!) -> UITableViewCell! {
let cell: UITableViewCell = UITableViewCell(style: UITableViewCellStyle.Subtitle, reuseIdentifier: "FMDBTest")
let multiField: MultiField = self.dataArray[indexPath.row]
let num = indexPath.row + 1
cell.textLabel.text = "\(num). \(multiField.field1!)"
cell.detailTextLabel.text = multiField.field2
return cell
}
// MARK: - UITableViewDelegate
func tableView(tableView: UITableView!, didSelectRowAtIndexPath indexPath: NSIndexPath!) {
tableView.deselectRowAtIndexPath(indexPath, animated: true)
}
}
9) Немного разных фишек, использование мультипотока FMDB через FMDatabaseQueue.
var queue: FMDatabaseQueue?
func testDatabaseQueue() {
let documentsFolder = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)[0] as String
let databasePath = documentsFolder.stringByAppendingPathComponent("test.sqlite")
queue = FMDatabaseQueue(path: databasePath)
// создание таблицы
<source lang="objectivec">
queue?.inDatabase() {
db in
var success = db.executeUpdate("create table test (id integer primary key autoincrement, a text)", withArgumentsInArray:nil)
if !success {
println("table create failure: \(db.lastErrorMessage())")
return
}
}
// вставка пяти строк
queue?.inTransaction() {
db, rollback in
for i in 0 ..< 5 {
if !db.executeUpdate("insert into test (a) values (?)", withArgumentsInArray: ["Row \(i)"]) {
println("insert \(i) failure: \(db.lastErrorMessage())")
rollback.initialize(true)
return
}
}
}
//давайте попробуем вставить строки, но сознательно ошибемся и посмотрим, что он откатывает правильно
queue?.inTransaction() {
db, rollback in
for i in 5 ..< 10 {
let success = db.executeUpdate("insert into test (a) values (?)", withArgumentsInArray: ["Row \(i)"])
if !success {
println("insert \(i) failure: \(db.lastErrorMessage())")
rollback.initialize(true)
return
}
if (i == 7) {
rollback.initialize(true)
}
}
}
// проверим, что только первые пять строк там
queue?.inDatabase() {
db in
if let rs = db.executeQuery("select * from test", withArgumentsInArray:nil) {
while rs.next() {
println(rs.resultDictionary())
}
} else {
println("select failure: \(db.lastErrorMessage())")
}
}
// удаляем таблицу
queue?.inDatabase() {
db in
let success = db.executeUpdate("drop table test", withArgumentsInArray:nil)
if !success {
println("table drop failure: \(db.lastErrorMessage())")
return
}
}
}
10) Стандартного на закуску. Использование класса executeUpdate(values:) в Swift2:
do {
let identifier = 42
let name = "Liam O'Flaherty (\"the famous Irish author\")"
let date = NSDate()
let comment: String? = nil
try db.executeUpdate("INSERT INTO authors (identifier, name, date, comment) VALUES (?, ?, ?, ?)", values: [identifier, name, date, comment ?? NSNull()])
} catch {
print("error = \(error)")
}
Использование queue:
queue.inTransaction { db, rollback in
do {
try db.executeUpdate("INSERT INTO myTable VALUES (?)", values: [1])
try db.executeUpdate("INSERT INTO myTable VALUES (?)", values: [2])
try db.executeUpdate("INSERT INTO myTable VALUES (?)", values: [3])
if whoopsSomethingWrongHappened {
rollback.memory = true
return
}
try db.executeUpdate("INSERT INTO myTable VALUES (?)", values: [4])
} catch {
rollback.memory = true
print(error)
}
}
Пример из стандартного описания:
let documents = try! NSFileManager.defaultManager().URLForDirectory(.DocumentDirectory, inDomain: .UserDomainMask, appropriateForURL: nil, create: false)
let fileURL = documents.URLByAppendingPathComponent("test.sqlite")
let database = FMDatabase(path: fileURL.path)
if !database.open() {
print("Unable to open database")
return
}
do {
try database.executeUpdate("create table test(x text, y text, z text)", values: nil)
try database.executeUpdate("insert into test (x, y, z) values (?, ?, ?)", values: ["a", "b", "c"])
try database.executeUpdate("insert into test (x, y, z) values (?, ?, ?)", values: ["e", "f", "g"])
let rs = try database.executeQuery("select x, y, z from test", values: nil)
while rs.next() {
let x = rs.stringForColumn("x")
let y = rs.stringForColumn("y")
let z = rs.stringForColumn("z")
print("x = \(x); y = \(y); z = \(z)")
}
} catch let error as NSError {
print("failed: \(error.localizedDescription)")
}
database.close()
Если что-то не получается, пишете, постараюсь помочь.
Комментарии (15)
olegbragin
19.02.2016 10:58В конце концов получается, что чаще всего для реализации каки-то базовых функций для работы программы (работа с локальными данными, работа с графикой) возращаемся к использованию решений, которые предоставляет компания-разработчки OS, в нашем случае Apple.Так что все эти FMDB от лукавого :)
mlnewton
19.02.2016 11:12Не могу согласится, вчера записывал данные и сразу их получал, coredata подвесила мне приложение на несколько секунд, fmdb моментально все сделал. Так что тут наверное зависит от частных случаев, неплохо конечно знать и то и другое и уметь применять.
IgorFedorchuk
FMDB по умолчанию не поддерживает WAL(Write-Ahead Logging). То есть при длительной записи чтение будет блокироваться до окончания записи. Нужно специально проставлять флаги для нее.
mlnewton
Чем это плохо и как это победить? (что использовать)
IgorFedorchuk
Чем это плохо — если записывается большой объем данных, то если в это время выполнить запрос на чтение, то он выполнится только по окончании записи, а это может быть достаточно долгая задержка.
Как решить? Что смог нагуглить(ответ Hamiseixas), но у меня это решение не сработало. Поэтому использовал Core Data.