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
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.
}
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)")
}
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") }
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()
let db.set(autoVacuum:.incremental)
// do some inserts, deletes here
try db.incrementalVacuum()
try db.vacuum()
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
}
递归触发器默认是关闭的,但是根据文档,在未来的版本中可能会默认开启。
自限制递归触发器的例子
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 中执行此操作 🤓。
我们将在这里使用 Value
和 Result
类。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 从 1.1.0 版本及更高版本开始支持
要使用 SQLCipher 而不是 SQLite,请参阅 LICENSE.sqlcipher.md
许可文件。
如果您不想使用它,您仍然可以使用标准包 SwiftSQLite
。
如果您不打算使用加密,我建议您使用 SwiftSQLite
,因为它会稍微减小您的二进制文件大小。
以下方法仅适用于 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)
// 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()
根据 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 助手,用于将密码保存在 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)
}
将以下内容添加到您的 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
选择您的项目,在通用选项卡中的Frameworks and Libraries下,点击 + 按钮。
输入网址
https://github.com/moshegottlieb/SwiftSQLite.git
选择您的版本,然后就完成了。
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