Столкнувшись с задачей подключить SQLLite к своему мобильному приложению iOS через FMDB, я не нашел ни одного актуального гайда на русском языке. И тем более для Swift. В этой статье я постараюсь этого исправить.

В этом гайде будут использоваться файлы с 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)


  1. IgorFedorchuk
    18.02.2016 21:50

    FMDB по умолчанию не поддерживает WAL(Write-Ahead Logging). То есть при длительной записи чтение будет блокироваться до окончания записи. Нужно специально проставлять флаги для нее.


    1. mlnewton
      18.02.2016 22:10

      Чем это плохо и как это победить? (что использовать)


      1. IgorFedorchuk
        19.02.2016 00:09

        Чем это плохо — если записывается большой объем данных, то если в это время выполнить запрос на чтение, то он выполнится только по окончании записи, а это может быть достаточно долгая задержка.
        Как решить? Что смог нагуглить(ответ Hamiseixas), но у меня это решение не сработало. Поэтому использовал Core Data.


  1. olegbragin
    19.02.2016 10:58

    В конце концов получается, что чаще всего для реализации каки-то базовых функций для работы программы (работа с локальными данными, работа с графикой) возращаемся к использованию решений, которые предоставляет компания-разработчки OS, в нашем случае Apple.Так что все эти FMDB от лукавого :)


    1. mlnewton
      19.02.2016 11:12

      Не могу согласится, вчера записывал данные и сразу их получал, coredata подвесила мне приложение на несколько секунд, fmdb моментально все сделал. Так что тут наверное зависит от частных случаев, неплохо конечно знать и то и другое и уметь применять.