Core Data:SQLite的一面

如果用Core Data做持久化的数据库,那极为可能你选用SQLite作为数据库的存储格式。绝大多数时候,我们不需要直接接触SQLite,也不建议这么做。最常碰到到SQLite相关的事情就是添加Persistent Store时传入特定的NSSQLitePragmasOption。这些options可以方便的在 官方文档 里查阅到。

然后,Core Data还有另一个可以应用SQLite的地方。

对于很多apps来说,数据库是它们最重要的组成。启动崩溃听起来很可怕,但是发布一个新版本,用户更新了之后依旧可以使用。如果一个app的数据库损坏了,对于用户来说则是丢失了全部重要的信息。所以推荐定期备份数据库,这样如果数据库损坏了,可以从备份恢复,这样可以减少用户的损失。

SQLite官方有一篇详细的文字描述什么情况下可能会造成数据库损坏

经过自己实验下来,Core Data本身是相当敏感的。哪怕只是文件的magic string受损,Core Data也无法读取数据。然而,利用SQLite的C APIs可以访问数据库。只要可以访问数据库,就可能从受损的数据库部分恢复数据:

  1. 导出数据:找到所有数据库的表,记录表的SQL创建语句。遍历每一张表,把每张表的数据读取出来,保存为SQL插入语句记录。遇到读取失败的情况则略过这一部分,这部分数据已经损坏了,没有办法。
  2. 把这些导出的SQL语句存储在文本文件里,然后在新创建的数据库中执行。
  3. 用新的数据库来代替受损的数据库。

下面我们就来看一下如何使用SQLite的C APIs来导出数据。本文的实现是参考开源的Python代码

打开数据库

打开数据库文件特别简单。

var db: OpaquePointer?
var rc = sqlite3_open(databasePath, &db)
guard rc == SQLITE_OK else {
    fatalError("无法打开数据库")
}

如果数据库无法打开,我们这些伟大的程序员也无能为力了。

通过这个方法,我们获取到了数据库文件的handle,之后操作会用到它。对应的关闭数据库文件handle也只需要调用一个函数。

sqlite3_close(db)

读取sqlite_master

可以将sqlite_master理解为数据库的metadata表,里面存储着数据库里所有表的信息。

首先,我们需要调用sqlite3_prepare_v2方法将SQL语句编译byte-code来用于数据库的查询。

var stmtTable: OpaquePointer?
rc = sqlite3_prepare_v2(
            db,
            """
            SELECT "name", "type", "sql"
            FROM "sqlite_master"
                WHERE "sql" NOT NULL AND
                "type" == 'table'
                ORDER BY "name";
            """,
            -1,
            &stmtTable,
            nil)
guard rc == SQLITE_OK else {
    fatalError("访问sqlite_master失败")
}

我们需要从sqlite_master表中读出数据库里每个表的表名,以及生成该表的SQL语句。

创建表

我们调用sqlite3_step方法逐行读取数据。

rc = sqlite3_step(stmtData)
while rc == SQLITE_ROW {
    defer {
        rc = sqlite3_step(stmtData)
    }
    // 处理每一个表
}

对于非系统内的表,我们记录下它们的SQL表的创建语句。

guard let name = sqlite3_column_text(stmtTable, 0),
    let type = sqlite3_column_text(stmtTable, 1),
    let sql = sqlite3_column_text(stmtTable, 2) else {
        throw Error.makeSql(sqlite3_errmsg(db))
    }

let tableName = String.init(cString: name)

if tableName == "sqlite_sequence" {
    output.write("DELETE FROM \"sqlite_sequence\";\n")
} else if tableName == "sqlite_stat1" {
    output.write("ANALYZE \"sqlite_master\";\n")
} else if tableName.hasPrefix("sqlite_") {
    // Not support virtual table
    continue
} else {
    // CREATE TABLE statements
    output.write(String(cString: sql) + "\n")
}

对于每张表,我们利用类似的方法读取每张表的数据。只是执行的SQL语句不同。
```swift
sqlite3_prepare_v2(

top Created with Sketch.