SwiftSQLite

Swift License Apple Platforms Linux SQLite SQLCipher

Swift 的 SQLite 封装,不多不少。

它是什么?

一个简单直接的 SQLite C API 封装。
连接到 SQLite 数据库,运行查询,准备语句并绑定参数,就像使用常规 SQLite API 一样,只不过是在 Swift 中。
如果您需要一个轻量级的本地数据库 API,不需要花哨的功能,那么这个库就适合您。

不是什么

操作指南

创建数据库连接

// For example, place the database in the user's library folder
guard let path = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first?.appendingPathComponent("db.sqlite").absoluteString else { fatalError("Could not create path") }
let db = try Database(path:path)

显式打开或关闭数据库连接

有时您可能需要显式地关闭或打开数据库,而不仅仅是使用 CTOR 和 DTOR。

db.close() // will silently do nothing if already closed
try db.open(pathToFile) // Open a new connection, the old handle is closed first

运行简单的 SQL 语句

try db.exec("CREATE TABLE demo(a INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, b INTEGER NOT NULL)")

准备语句并使用参数运行

// Prepare once
let insert = try db.statement(sql: "INSERT INTO demo (b) VALUES (?)")
for i in 0..<10 {
    // Parameters are 1 based, this is how SQLite works
    try insert.bind(param: 1,i)
    try insert.step() // Run the statement
    let last_row_id = db.lastInsertRowId
    print("Last row id is: \(last_row_id)")
    try insert.reset() // must reset before we can run it again
    try insert.clearBindings() // Bindings are not cleared automatically, since we bind the same param again, this is not strictly required in this example, but it's good practice to clear the bindings.
}

运行 SELECT 查询

let select = try db.statement(sql: "SELECT a,b FROM demo WHERE b > ?")
try select.bind(param: 1, 5)
while try select.step() {
    guard let a = select.integer(column: 0), let b = select.string(column: 1) else {
        fatalError("Expected b to be non nil")
    }
    print("a: \(a), b: \(b)")
}

其他帮助程序和封装

使用 codables

struct Student : Codable {
    let name:String
    let grade:Int
    let city:String
}

db.useJSON1 = true
try db.exec("CREATE TABLE students (value json)") // JSON1 extension, JSON is actually TEXT
let ins = try db.statement(sql: "INSERT INTO students (value) VALUES (?)")
let student = Student(name:"Bart Simpson",grade:4,city:"Springfield")
try ins.bind(param: 1,student) // Bind a decodable object
try ins.step() // Execute the statement
let sel = try db.statement(sql: "SELECT json_extract(value,"$.name") FROM students")
guard try sel.step() else { fatalError("Expected step to succeed") }
guard let the_student:Student? = sel.object(column: 0) // deduce that the object is C by the return type, which must be an optional Decodable
else { fatalError("Expected object to be decoded to a C instance") }

设置 journal mode (日志模式)

try db.set(journalMode: .wal) // Set journaling mode to WAL, useful when several processes read the database file, such as with an app and an app extension
let current_mode = try db.journalMode()

自动 vacuum (清理)

let db.set(autoVacuum:.incremental)
// do some inserts, deletes here
try db.incrementalVacuum()

Vacuum (清理)

try db.vacuum()

外键 on/off (启用/禁用)

db.foreignKeys = true
// foreign keys are now enforced
try db.withoutForeignKeys {
    // This code will run without foreign keys enforcement 
}
try db.withForeignKeys {
    // This code will run with foreign keys enforcement
}

递归触发器 on/off (启用/禁用)

递归触发器默认是关闭的,但是根据文档,在未来的版本中可能会默认开启
自限制递归触发器的例子

CREATE TABLE rt(a INTEGER);
CREATE TRIGGER rt_trigger AFTER INSERT ON rt WHEN new.a < 10
BEGIN
    INSERT INTO rt VALUES (new.a + 1);
END;
db.recursiveTriggers = true
try db.exec("INSERT INTO rt VALUES (1)")
// rt should now have the 10 values (1..10)
// if recursiveTriggers was off - rt would only have 2 rows (1,2) as the trigger would not trigger itself.

设置繁忙超时

try db.set(busyTimoeut:30)

这将安装一个繁忙处理程序,该程序将休眠,直到数据库解锁或直到超时到期,这对于 WAL 模式非常有用。
参见 busy handler (繁忙处理程序) 和 PRAGMA busy_timouet
请注意,一个数据库连接只能有一个繁忙处理程序。

版本

设置用户版本或获取用户、数据或模式版本。
参见 PRAGMA data_version
参见 PRAGMA schema_version
参见 PRAGMA user_version

let user_version = try db.get(version: .user) // 0 by default
let schema_version = try db.get(version: .schema) 
let data_version = try db.get(version: .data) 
try db.set(version:12)

自定义函数

SQLite 允许您创建用户定义的函数,而 SwiftSQLite 允许您在 Swift 中执行此操作 🤓。
我们将在这里使用 ValueResult 类。
Value 是提供给您的函数的参数,而 Result 是您的函数的结果。

这是一个标量函数的示例

try db.createScalarFunction(name: "custom_sum_all_args", nArgs: 1, function: { (values:[Value]?) in
    var sum = 0
        values?.forEach({ value in
            sum += value.intValue
        })
    return Result(sum)
})

现在您可以调用

SELECT custom_sum_all_args(1,2,3)

返回的值将是 6! (1+2+3)。

聚合函数稍微复杂一些,但不会太复杂。
这是一个类似的示例,但作为聚合函数

try db.createAggregateFunction(name: "custom_agg_test", step: { (values:[Value]?,result:Result) in
    // Sum all arguments
    var sum = 0
    values?.forEach({ v in
        sum += v.intValue
    })
    // Is it the first value we're setting?
    if result.resultType == .Null {
        // Set the initial value, result type will be automatically set to Int
        result.intValue = sum
    } else {
        // Nope, not the first time, sum with previous value
        result.intValue! += sum
    }
})

现在您可以将其用作聚合函数

SELECT custom_agg_test(value,1) FROM json_each(json_array(1,2,3))

结果值应为 9。((1 + 1) + (2 + 1) + (3 + 1) )

日志记录

可以通过实现协议 Log 来安装记录器。

/// Log protocol
public protocol Log  {
    /// Log SQL
    /// - parameters:
    ///   - prepare: SQL statement being prepared
    func log(prepare:String)
    /// Log error
    /// - parameters:
    ///  - error: Error text
    ///  - code: Error code (SQLite state)
    func log(error:String,code:Int)
    /// Log statement execution
    /// - parameters:
    ///  - sql: Executed SQL
    func log(sql:String)
    /// Log a message
    /// - parameters:
    ///   - message: Message to log (open DB, etc.)
    func log(message:String)
}

设置 Database 类的静态属性 logger,就可以开始了。
内置的控制台记录器可用,要使用它,只需添加
Database.logger = ConsoleLog()
最好在使用库之前设置它(但可以在任何时候设置)。

SQLCipher

支持

SQLCipher 从 1.1.0 版本及更高版本开始支持

SwiftSQLCipher 还是 SwiftSQLite?

要使用 SQLCipher 而不是 SQLite,请参阅 LICENSE.sqlcipher.md 许可文件。
如果您不想使用它,您仍然可以使用标准包 SwiftSQLite
如果您不打算使用加密,我建议您使用 SwiftSQLite,因为它会稍微减小您的二进制文件大小。

如何使用 SwiftSQLCipher

以下方法仅适用于 SQLCipher
不要导入 SwiftSQLite,而是导入 SwiftSQLCipher
打开数据库后,调用 setKey(:)

try db.setKey("TopSecretPassword")

如果数据库尚未加密,这将加密数据库,如果已加密,则允许读取它。
这必须是您新创建/打开的数据库的第一个操作。
您不能使用此方法来加密已存在的数据。

您还可以通过调用 reKey(:) 更改数据库密码。
数据库必须已打开、加密,并且必须已调用 setKey(:) 方法。

try db.setKey("TopSecretPassword") // must call setKey(:) BEFORE calling reKey
try db.reKey("EvenMoreSecretPassword") // will replace the password to a new key
try db.reKey(nil) // remove encryption altogether, can now read without SQLCipher
try db.removeKey() // same as reKey(nil) 

高级 SQLCipher 支持

// var cipherSalt:Data? { get throws }
let salt:Data? = try db.cipherSalt // database salt (16 bytes), nil if not encrypted or plain text header
...
// func setCipherSalt(_ salt:Data) throws
let salt:Data = ....
try db.setCipherSalt(salt) // set the salt for the database. you need this when using plain text header
...
// func setPlainTextHeader(size:Int32) throws
// Set a plain text header for the DB.
// This causes sqlcipher not to be able to read the salt part of your database, make sure you store it if you use it
try db.setPlainTextHeader(size:32) // 32 is recommended for iOS WAL journaling in a shared container in iOS

// func flushHeader() throws
// This simply reads the user version and writes it, you should call this after creating databases with plain text headers
try db.flushHeader()

iOS 模式,带有 WAL 模式和共享容器

根据 SQLCipher,iOS 将检查您的数据库文件是否是 WAL 模式的 SQLite 数据库,如果是,则允许它在后台锁定该文件。
否则,当您尝试从后台执行此操作时,您的应用程序将被终止。
由于 SQLCipher 数据库已加密,因此 iOS 无法验证文件是否满足要求。
请阅读 SQLCipher 文档
请参阅 Apple 的文档
您应该使用的正确顺序如下

    // new database
    let db = try Database(path: filename)
    try db.setKey(password)
    // SAVE this salt somewhere safe
    guard let salt = try db.cipherSalt else {
        throw E.error("Could not get salt")
    }
    try db.setPlainTextHeader(size: 32)
    try db.set(journalMode: .wal)
    try db.flushHeader()
    
    // Open an existing database
    try db.open(path: filename)
    try db.setKey(password)
    try db.setPlainTextHeader(size: 32)
    try db.setCipherSalt(salt)
    try db.set(journalMode: .wal)
    // Good to go

还有一个支持 keychain 的版本(仅限 Apple 平台),可以省去您的麻烦。

    // func openSharedWalDatabase(path:String,accessGroup:String? = nil,identifier:String) throws
    
    let db = try Database()
    // Use a different identifier for each database!
    try db.openSharedWalDatabase(path:"path/to/file.db",accessGroup:"com.your.shared.identifier",identifier:"MyDB")
    // Your database is now opened, encrypted, in WAL mode, and the key and salt are stored in the keychain
    // (even though the salt is not really a secret)
    
    // func deleteCredentials(accessGroup:String? = nil,identifier:String) throws
    // This will REMOVE the credentials from the keychain
    // Important: you cannot access your database after calling this method
    // Use it when deleting your database file
    try db.deleteCredentials(accessGroup:"com.your.shared.identifier",identifier:"MyDB")

Keychain 支持(仅限 Apple 平台)

包含一个 keychain 助手,用于将密码保存在 keychain 中。

    
    // Save key to keychain
    // Pass nil to delete the key from the keychain
    try db.saveToKeyChain(account:"mydb",key:"MySecretPassword")
    // When sharing the database using a group identifier:
    try db.saveToKeyChain(account:"mydb",key:"MySecretPassword",accessGroup:"your.group.identifier.if.you.have.it")
    
    // Delete the key from the keychain
    
    try db.deleteFromKeyChain(account:"mydb")
    try db.deleteFromKeyChain(account:"mydb",accessGroup:"your.group.identifier.if.you.have.it")
    
    // Read the password from the keychain
    if let password = try db.readFromKeyChain(account:"mydb") {
        try db.setKey(password)
    }
    // of course, readFromKeyChain accepts also an accessGroup:
    if let password = try db.readFromKeyChain(account:"mydb", accessGroup:"your.group.identifier.if.you.have.it") {
        try db.setKey(password)
    }

安装

Swift Package Manager

将以下内容添加到您的 Package.swift 依赖项中

dependencies: [
...
.package(url: "https://github.com/moshegottlieb/SwiftSQLite.git", from: "1.1.0")
...
]

import SwiftSQLite // for standard SQLite
import SwiftSQLCipher // for SQLCipher version

如何添加到现有的 Xcode 项目

选择您的项目,在通用选项卡中的Frameworks and Libraries下,点击 + 按钮。
输入网址
https://github.com/moshegottlieb/SwiftSQLite.git
选择您的版本,然后就完成了。

Linux 依赖项

swift package manager 不会自动安装所需的依赖项。
在 ubuntu/debian 系统上
sudo apt-get install libsqlite3-dev
对于 SQLCipher
sudo apt-get install sqlcipher-dev

在 RedHat/Centos 系统上
sudo yum install sqlite-devel
对于 SQLCipher
sudo yum install sqlcipher-devel