SQift 5.1

SQift 5.1

Christian NoonBen Scheirman 维护。



SQift 5.1

  • Dave Camp, Christian Noon 和 Ben Scheirman

SQift

Build Status CocoaPods Compatible Carthage Compatible Platform

SQift 是 SQLite 的轻量级 Swift 封装。

特性

  • 磁盘、内存和临时数据库连接
  • SQL 语句执行
  • 通用参数绑定和值提取
  • Codable 和 Codable Collection 绑定
  • 简单的值、行和集合查询 API
  • 事务和保存点
  • 跟踪和跟踪事件支持
  • 标量和聚合函数
  • 提交、回滚、更新、授权钩子
  • WAL 检查点
  • ConnectionQueue 用于每个数据库连接的串行执行
  • ConnectionPool 用于并行执行只读连接
  • 顶级数据库以简化线程安全的读写操作
  • 数据库迁移支持
  • 数据库备份
  • 全面的单元测试覆盖率
  • 完整的文档

要求

  • iOS 10.0+, macOS 10.12+, tvOS 10.0+, watchOS 3.0+
  • Xcode 10.2+
  • Swift 5.0+

迁移指南

通讯

  • 需要帮助?开启一个问题。
  • 有功能请求?开启一个问题。
  • 发现了错误?开启一个问题。
  • 想要贡献?Fork 存储库并提交拉取请求。

安装

CocoaPods

CocoaPods 是 Cocoa 项目的依赖管理器。您可以使用以下命令安装它:

$ gem install cocoapods

要构建 SQift,需要 CocoaPods 1.3+。

要使用 CocoaPods 将 SQift 集成到您的 Xcode 项目中,请在您的 Podfile 中指定它。

platform :ios, '11.0'
use_frameworks!

target '<Your Target Name>' do
    pod 'SQift', '~> 4.0'
end

然后运行以下命令:

$ pod install

Carthage

Carthage 是一个去中心化的依赖管理器,它构建您的依赖并提供二进制框架。

您可以使用以下命令使用 Homebrew 安装 Carthage:

$ brew update
$ brew install carthage

要使用 Carthage 将 SQift 集成到您的 Xcode 项目中,请在您的 Cartfile 中指定它。

github "Nike-Inc/SQift" ~> 4.0

运行 carthage update 构建框架,并将构建的 SQift.framework 拖至您的 Xcode 项目中。


使用方法

SQift 设计得尽可能简单,以从 Swift 与 SQLite 交互。然而,它并没有消除理解 SQLite 如何实际工作的需求。在深入 SQift 之前,建议您首先对 SQLite 是什么、它是如何工作和如何使用它有一个坚实的理解。

SQift充分利用了Swift 2.0版本中发布的新的错误处理模型。它从设计之初就是为了在所有适用的情况下抛出异常。这使得将所有SQift调用包裹在do-catch范式变得十分简单。

创建数据库连接

创建数据库连接很简单。

let onDiskConnection = try Connection(storageLocation: .onDisk("path_to_db"))
let inMemoryConnection = try Connection(storageLocation: .inMemory)
let tempConnection = try Connection(storageLocation: .temporary)

还有一些便利参数可以使得在初始化数据库连接时轻松调整标志。

let connection = try Connection(
    storageLocation: .onDisk("path_to_db"),
    readOnly: true,
    multiThreaded: false,
    sharedCache: false
)

在大多数情况下,建议使用默认值。有关创建数据库连接的更多详情,请参阅SQLite 文档

执行语句

要在Connection上执行SQL语句,您首先需要创建一个Connection,然后调用execute

let connection = try Connection(storageLocation: .onDisk("path_to_db"))

try connection.execute("PRAGMA foreign_keys = true")
try connection.execute("PRAGMA journal_mode = WAL")

try connection.execute("CREATE TABLE cars(id INTEGER PRIMARY KEY, name TEXT, price INTEGER)")

try connection.execute("INSERT INTO cars VALUES(1, 'Audi', 52642)")
try connection.execute("INSERT INTO cars VALUES(2, 'Mercedes', 57127)")

try connection.execute("UPDATE cars SET name = 'Honda' where id = 1")
try connection.execute("UPDATE cars SET price = 61_999 where name = 'Mercedes'")

try connection.execute("DELETE FROM cars where name = 'Mercedes'")
try connection.execute("DROP TABLE cars")

绑定

大多数Swift数据类型不能直接存储在数据库中。它们需要转换为SQLite支持的数据类型。为了支持在数据库中移动Swift数据类型,SQift利用了三个强大的协议:BindableExtractableBinding

Bindable协议

Bindable协议负责将Swift数据类型转换为可以存储在数据库中的BindingValue枚举类型。

public protocol Bindable {
    var bindingValue: BindingValue { get }
}

Extractable协议

虽然Bindable协议帮助将Swift数据类型移动到数据库中,但Extractable协议允许SQift从中提取Connection中的值,并将它们转换回请求的Swift数据类型。

public protocol Extractable {
    typealias BindingType
    typealias DataType = Self
    static func fromBindingValue(_ value: Any) -> DataType?
}

绑定协议

为了扩展Swift数据类型以便可以安全地插入数据库,并从中提取,Binding协议强制数据类型遵守BindableExtractable协议。

public protocol Binding: Bindable, Extractable {}

为了尽可能地使用SQift,SQift扩展了以下Swift数据类型,使其符合Binding协议:

  • NULL: NSNull
  • INTEGER: BoolInt8Int16Int32Int64IntUInt8UInt16UInt32UInt64UInt
  • REAL: FloatDouble
  • TEXT: StringURLDate
  • BLOB: Data

如需,Swift数据类型可以轻松添加Bindable协议的符合性。

将参数绑定到语句

Binding协议使得将参数安全地绑定到Statement变得很容易。首先你需要prepare一个Statement对象,然后绑定参数并使用方法链调用run

let connection = try Connection(storageLocation: .onDisk("path_to_db"))

try connection.prepare("INSERT INTO cars VALUES(?, ?, ?)").bind(1, "Audi", 52_642).run()
try connection.prepare("INSERT INTO cars VALUES(:id, :name, :price)").bind([":id": 1, ":name": "Audi", ":price": 52_642]).run()

Connection上也有方便的方法用于准备Statement,绑定参数并在单个名为run的方法中运行它。

let connection = try Connection(storageLocation: .onDisk("path_to_db"))

try connection.run("INSERT INTO cars VALUES(?, ?, ?)", 1, "Audi", 52_642)
try connection.run("INSERT INTO cars VALUES(:id, :name, :price)", parameters: [":id": 1, ":name": "Audi", ":price": 52_642])

正确转义SQL语句中提供的所有参数值非常重要。如有疑问,始终使用提供的绑定功能。

查询数据

查询数据从数据库中广泛使用Binding协议。它从数据库中提取原始值,然后使用泛型和Binding协议将最终Swift类型进行转换。

单一值

使用query API可以从数据库中提取单个值。

let synchronous: Int? = try db.query("PRAGMA synchronous")
let minPrice: UInt? = try db.query("SELECT avg(price) FROM cars WHERE price > ?", 40_000)

多个值

您还可以使用query API通过Row类型提取多个值。

if let row = try db.query("SELECT name, type, price FROM cars WHERE type = ? LIMIT 1", "Sedan") {
    let name: String = row[0]
    let type: String = row[1]
    let price: UInt = row[2]
}

可以通过索引或名称来访问这些值。

if let row = try db.query("SELECT name, type, price FROM cars WHERE type = ? LIMIT 1", "Sedan") {
    let name: String = row["name"]
    let type: String = row["type"]
    let price: UInt = row["price"]
}

Row类型支持通过索引和名称子脚本文本可选和非可选值提取。非可选子脚本当然是最安全的,但并不总是最方便的。您需要决定在每个情况下哪种使用更有意义。通常,尽量在将SQL语句与行值提取解耦的地方使用可选类型。在这些情况下,可以使用ExpressibleByRow类型来帮助处理可选性。

ExpressibleByRow 类型

在许多情况下,您想从结果集中的行构建模型对象。ExpressibleByRow类型就是为了这种情况设计的。

protocol ExpressibleByRow {
    init(row: Row) throws
}

要使用ExpressibleByRow协议,首先创建您的模型对象并遵守该协议。

struct Car {
    let name: String
    let type: String
    let price: UInt
}

extension Car: ExpressibleByRow {
    init(row: Row) throws {
        guard
            let name: String = row[0],
            let type: String = row[1],
            let price: UInt = row[2]
        else {
            throw ExpressibleByRowError(type: Car.self, row: row)
        }

        self.name = name
        self.type = type
        self.price = price
    }
}

然后,您可以使用query API自动将结果集转换为Car

let car: Car? = try db.query("SELECT name, type, price FROM cars WHERE type = ? LIMIT 1", "Sedan")

多行

query API还支持查询包含多行的结果集。

let names: [String] = try db.query("SELECT name FROM cars")
let cars: [Car] = try db.query("SELECT * FROM cars WHERE price > ?", [20_000])

除了数组结果集外,您还可以创建字典结果集。

let sql = "SELECT name, price FROM cars WHERE price > ?"
let prices: [String: UInt] = try db.query(sql, 20_000) { ($0[0], $0[1]) }

日期

SQift完全支持Date对象,允许您轻松利用SQLite中内置的日期功能。您可以将日期轻松插入到数据库中,对其运行查询,然后提取出来。SQift通过利用bindingDateFormatterDate绑定来处理这种情况。默认情况下,所有Date类型都存储为TEXT,因此请确保相应地映射您的列类型。

let date1975: Date!
let date1983: Date!
let date1992: Date!
let date2001: Date!

try connection.execute(
	"CREATE TABLE cars(id INTEGER PRIMARY KEY, name TEXT, release_date TEXT)"
)

try connection.execute("INSERT INTO cars(?, ?)", "70s car", date1975)
try connection.execute("INSERT INTO cars(?, ?)", "80s car", date1983)
try connection.execute("INSERT INTO cars(?, ?)", "90s car", date1992)
try connection.execute("INSERT INTO cars(?, ?)", "00s car", date2001)

将日期存储在数据库中后,您可以运行日期范围查询来缩小您的数据。

let date1980: Date!
let date2000: Date!

let carCount: Int = try connection.query(
    "SELECT count(*) FROM cars WHERE release_date >= date(?) AND release_date <= date(?)",
    date1980, 
    date2000
)

print("Total Cars from the 80s and 90s: \(carCount)") // should equal 2

您可以更换默认的日期格式,但进行此操作时请注意。您需要确保新的日期格式符合SQLite的要求,以确保日期范围查询可以继续按预期工作。

您还可以将每行的日期提取为Date类型。

let sql = "SELECT release_date WHERE name = ? LIMIT 1"
let releaseDate: Date = try connection.query(sql, "80s car")

自定义绑定

SQift支持许多常见的原始类型,但您想则在数据库中存储自定义类型怎么办?这就是自定义绑定发挥作用的地方。要在数据库中存储自己的自定义类型,只需符合Binding协议即可。

enum DownloadState: Int {
    case pending, downloading, downloaded, failed
}

extension DownloadState: Binding {
    typealias BindingType = Int64

    var bindingValue: BindingValue { return .integer(Int64(rawValue)) }

    static func fromBindingValue(_ value: Any) -> AssetType? {
        guard let value = value as? Int64, let rawValue = Int(exactly: value) else { return nil }
        return DownloadState(rawValue: rawValue)
    }
}

try connection.execute("CREATE TABLE downloads(name TEXT PRIMARY KEY, state INTEGER NOT NULL)")
try connection.run("INSERT INTO downloads VALUES(?, ?)", "image1", DownloadState.pending)

if let state: DownloadState? = try connection.query("SELECT state FROM downloads WHERE name = 'image1') {
    print(state)
}

// Output
// DownloadState.pending

可编码绑定

SQift还支持默认的Codable绑定。例如,假设我们有一个Person对象,它可以编码。我们可以直接在数据库中存储Person实例,而不必创建自定义绑定。

struct Employee {
    let id: Int64
    let firstName: String
    let lastName: String
    let age: UInt
}

let phil = Person(id: 1, firstName: "Phil", lastName: "Knight", age: 79)

try connection.execute("CREATE TABLE employees(id INTEGER PRIMARY KEY, employee BLOB NOT NULL)")
try connection.run("INSERT INTO employees(employee) VALUES(?)", phil)

if let employee1: Employee? = try connection.query("SELECT employee FROM employees WHERE id = 1) {
    print(employee.firstName)
}

// Output
// "phil"

您需要考虑使用Codable绑定是否合理。虽然它们非常方便,但这些信息无法进行查询。在上面的例子中,您不能运行如SELECT count(1) FROM employees where firstName = 'Phil'之类的查询。如果您的用例不需要此类搜索,那么Codable绑定可能是一个有用的选择。

可编码集合

SQift还通过ArrayBindingSetBindingDictionaryBinding类型支持Codable集合。

let points: ArrayBinding = [
    CGPoint(x: 1.0, y: 2.0),
    CGPoint(x: 3.0, y: 4.0),
    CGPoint(x: 5.0, y: 6.0),
    CGPoint(x: 7.0, y: 8.0),
    CGPoint(x: 9.0, y: 10.0)
]

try connection.execute("CREATE TABLE stream(id INTEGER PRIMARY KEY, data BLOB NOT NULL)")
try connection.run("INSERT INTO stream(data) VALUES(?)", points)

let pointsQueried: ArrayBinding<CGPoint>? = try connection.query("SELECT data FROM stream WHERE id = 1")

pointsQueried?.elements.forEach { print("(\($0.x), \($0.y))") }

// Output
// (1.0, 2.0)
// (3.0, 4.0)
// (5.0, 6.0)
// (7.0, 8.0)
// (9.0, 10.0)

在需要写入大量数据且永远不会部分查询的情况下,可编码集合可能很有用。如果您只进行一次数据写入并作为整个集合进行查询,则可编码集合可能是一个不错的选择。

事务

除了在事务中,否则不能对数据库进行更改。默认情况下,任何更改数据库的命令如果没有正在执行的事务,则会自动启动一个事务。在SQift中,当需要在单个事务中运行多个操作时,也可以手动启动事务。

try connection.execute("CREATE TABLE cars(id INTEGER PRIMARY KEY, name TEXT, price INTEGER)")

try connection.transaction {
    try connection.prepare("INSERT INTO cars VALUES(?, ?, ?)").bind(1, "Audi", 52642).run()
    try connection.prepare("INSERT IN cars VALUES(?, ?, ?)").bind(2, "Mercedes", 57127).run()
}

如果事务中发生任何错误,SQift会自动回滚所有更改。

跟踪

当调试SQL语句时,有时候能够打印出SQLite实际执行的内容是非常有用的。通过通过注册一个闭包以运行每个语句执行,SQift允许您通过traceEvent API来实现这一点。

let connection = try Connection(storageLocation: storageLocation)

connection.traceEvent { event in
    if case .statement(_, let sql) = sql {
        print(sql)
    }
}

try connection.execute("CREATE TABLE employees(id INTEGER PRIMARY KEY, name TEXT)")
try connection.prepare("INSERT INTO employees VALUES(?, ?)").bind(1, "Bill Bowerman").run()
try connection.prepare("INSERT INTO employees VALUES(?, ?)").bind(2, "Phil Knight").run()
let employees: [Employee] = try connection.query("SELECT * FROM employees")

// Output
// "CREATE TABLE employees(id INTEGER PRIMARY KEY, name TEXT)"
// "INSERT INTO employees VALUES(1, 'Bill Bowerman')"
// "INSERT INTO employees VALUES(2, 'Phil Knight')"
// "SELECT * FROM employees"

traceEvent API允许您在跟踪时对要跟踪的语句类型进行更精细的选择。您可以使用跟踪事件掩码选择要跟踪的语句类型。

排序规则

SQift支持自定义排序规则函数,用于三种内置排序规则函数不足以使用的情况。一些实际的定制用例可能包括:有关于重音符号的排序和数值排序。

let connection = try Connection(storageLocation: storageLocation)

connection.createCollation(named: "NUMERIC") { lhs, rhs in
    return lhs.compare(rhs, options: .numeric, locale: .autoupdatingCurrent)
}

try connection.execute("CREATE TABLE values(text TEXT COLLATE 'NUMERIC' NOT NULL)")

let values = ["string 1", "string 21", "string 12", "string 11", "string 02"]

try values.forEach { try connection.run("INSERT INTO values(text) VALUES(?)", $0) }
let extractedValues: [String] = try connection.query("SELECT * FROM values ORDER BY text")

extractedValues.forEach { print($0) }

// Output
// "string 1"
// "string 02"
// "string 11"
// "string 12"
// "string 21"

函数

虽然SQLite是一个非常健壮的库,但有时您会遇到需要扩展SQLite功能的情况,因为它在这些方面有限。例如,您可能需要创建一个用于确定特定日期在日历年份中的哪个月的自定义函数。SQLite无法直接实现这一点,因为它没有日历支持。

SQift支持自定义标量和聚合函数。下面的示例说明了如何通过以下strip_unicode函数扩展SQLite的支持。

try connection.addScalarFunction(named: "strip_unicode", argumentCount: 1) { _, values in
    guard
        let value = values.first, value.isText,
        let valueData = value.text.data(using: .ascii, allowLossyConversion: true),
        let asciiValue = String(data: valueData, encoding: .ascii)
    else { return .null }

    return .text(asciiValue)
}

let sql = "SELECT strip_unicode(?)"

let result1: String? = try connection.prepare(sql, "å").query()
let result2: String? = try connection.prepare(sql, "ć").query()
let result3: String? = try connection.prepare(sql, "áč").query()

// result1 = "a"
// result2 = "c"
// result3 = "ac"

有关标量和聚合函数的更高级示例,请参阅测试套件。


高级

挂钩

SQift 内置了对提交、回滚、更新和授权挂钩的支持。提交挂钩用于确定是否应执行或回滚提交。回滚挂钩在回滚提交时被调用。

var shouldCancelCommit = false

connection.commitHook { return shouldCancelCommit }
connection.rollbackHook { print("rollback occurred") }

更新挂钩可以用于对.insert.update.delete操作做出反应。

connection.updateHook { type, databaseName, tableName, rowID in
    var message = "\(type) row \(rowID)"

    if let databaseName = databaseName { message += " on \(databaseName)" }
    if let tableName = tableName { message += ".\(tableName)" }

    print(message)
    
    // Could update the file system, invalidate a cache, send notifications, etc.
}

try connection.execute("""
    INSERT INTO employee(name) VALUES('Phil Knight');
    UPDATE person SET name = 'Bill Bowerman' WHERE id = 1;
    DELETE FROM person WHERE id = 1
    """
)

// Output
// "insert row 1 on main.person"
// "update row 1 on main.person"
// "delete row 1 on main.person"

授权挂钩是四个中最复杂的。它允许您控制可以在连接上运行哪些语句。例如,您可以选择禁用除选择语句之外的所有特定连接上的操作。

try connection.authorizer { action, p1, p2, p3, p4 in
    guard action == .select else { return .deny }
    return .ok
}

检查点

使用WAL日志模式的数据库使用检查点操作将更新从WAL文件移动到数据库。SQift支持检查点和忙超时以及处理程序,这在某些情况下可能非常有用。例如,您可能出于性能原因想要使用WAL数据库,然后将它转移到远程服务器或不同的设备。在这之前,检查数据库并对其进行清理(vacuum)是明智的选择。

try connection.busyHandler(.timeout(1.0)) // 1 second
let checkpointResult = try connection.checkpoint(mode: .truncate)

try connection.execute("VACUUM")

检查点是一个非常复杂的过程。在使用checkpoint API之前,请务必阅读SQLite 文档

线程安全

在SQLite中,线程安全是一个复杂的话题。一般来说,从多个线程同时访问数据库Connection是不安全的。每个连接都应该串行访问以保证安全性。

如果您希望并行访问数据库,有一些事情您需要了解。首先,您需要使用写前日志(WAL),通过设置日志模式为WAL。通过将数据库更改为WAL日志模式,数据库可以在写入时读取,在使用多个连接的情况下,在读取时写入。

try connection.execute("PRAGMA journal_mode = WAL")

另一个重要的注意事项是,SQLite只能以串行方式执行写操作,不管您创建了多少个连接。因此,如果可能的话,您应该只为写入创建一个连接。您可以使用尽可能多的读取连接。有关线程安全性和WAL日志模式的信息,请参阅以下内容:

连接队列

SQift中的ConnectionQueue类旨在确保数据库Connection能够在多线程中被安全访问时的线程安全性。它在内部串行调度队列上执行所有操作,从而确保对连接操作的所有操作都是串行进行的。ConnectionQueue还支持在事务和保存点内执行逻辑。

let queue = try ConnectionQueue(connection: Connection(storageLocation: .onDisk("path_to_db")))

try queue.execute { connection in
    try connection.execute("PRAGMA foreign_keys = true")
    try connection.execute("PRAGMA journal_mode = WAL")
    try connection.execute("CREATE TABLE cars(id INTEGER PRIMARY KEY, name TEXT, price INTEGER)")
}

try queue.executeInTransaction { connection in
    try connection.execute("INSERT INTO cars VALUES(1, 'Audi', 52642)")
    try connection.execute("INSERT INTO cars VALUES(2, 'Mercedes', 57127)")
}

try queue.executeInSavepoint("drop_cars_table") { connection in
	try connection.execute("DROP TABLE cars")
}

连接池

ConnectionPool类允许多个只读连接以线程安全的方式同时访问数据库。内部,池管理两组不同的连接,一组是可用的,一组是当前正在执行SQL逻辑的。当有可用连接时,池将重用这些连接,当所有可用连接都忙碌时,将初始化新的连接,直到达到最大连接数。

let pool = try ConnectionPool(storageLocation: .onDisk("path_to_db"))

try pool.execute { connection in
    let count: Int = try connection.query("SELECT count(*) FROM cars")
}

由于SQLite对单个数据库的最大打开连接数没有限制,ConnectionPool会在短时间内初始化所需的所有连接。每当执行一个连接时,内部排空延迟计时器启动。当排空延迟计时器触发时,如果没有更多的忙碌连接,它会排空可用连接。如果仍有忙碌连接,计时器将被重新启动。这允许ConnectionPool在非常短的时间内启动所需的连接数量。

连接池通过总是在连接队列内执行SQL闭包来保证线程安全性,从而确保在连接上执行的每个SQL闭包都是以串行方式完成的,从而保证了每个连接的线程安全性。

数据库

Database类是一种简易的方式,用于创建所有只读语句的单个可写连接队列和连接池。读取和写入API的设计旨在简单地在适当的Connection类型上执行SQL语句,以线程安全的方式执行。

let database = try Database(storageLocation: .onDisk("path_to_db"))

try database.executeWrite { connection in
    try connection.execute("PRAGMA foreign_keys = true")
    try connection.execute("PRAGMA journal_mode = WAL")
    try connection.execute("CREATE TABLE cars(id INTEGER PRIMARY KEY, name TEXT, price INTEGER)")
}

try database.executeRead { connection in
    let count: Int = try connection.query("SELECT count(*) FROM cars")
}

这是以100%线程安全方式运行的最简单方法,无需处理ConnectionQueueConnectionPool类的底层复杂性。

在使用Database类型时,还有一个重要的考虑因素,那就是是否使用共享缓存。如果您正在使用WAL日志模式,最好将sharedCache参数设置为true。这允许读者池始终能够访问写入连接所做的新更改。如果您不使用共享缓存,读者将无法始终访问最新的写入更改。这可能会在其他长时间运行的读取操作运行时,而另一个连接正在写入更改时发生。

我们鼓励每个人使用Database对象而不是直接与连接队列或连接池工作。

表锁策略

表锁错误是executepreparestep操作抛出的SQLITE_ERROR类型的错误。这些错误可能在数据库配置了WAL日志模式和共享缓存时发生。当一个连接获得对表的锁定时,另一个在不同线程上运行的连接将直到之前的锁释放才会接收到表锁错误。在这些情况下,有几种方法可以继续进行。错误可以立即抛出并由客户端处理,或者调用线程可以轮询操作直到锁释放。

TableLockPolicy定义了处理表锁错误的两种不同方式。第一种选择是在指定的间隔轮询调用线程直到锁定释放。另一种选择是在遇到表锁定错误时立即快速失败。

要启用对表锁定错误的轮询,只需要在ConnectionDatabase初始化器中设置策略。

let connection = try Connection(storageLocation: storageLocation, tableLockPolicy: .poll(0.01))
let database = try Database(storageLocation: .onDisk("path_to_db"), tableLockPolicy: .poll(0.01))

当使用WAL日志模式和共享缓存时,建议使用.poll表锁策略,轮询间隔为10 ms

迁移

生产应用程序通常需要不时迁移数据库模式。无论是需要一些新表还是对表进行可能的更改,您都需要一种管理迁移逻辑的方法。通过Migrator类,SQift已经为您内置了迁移支持。您需要做的只是创建Migrator实例并运行它。其余的一切将由SQift内部处理。

let connection = try Connection(storageLocation: .onDisk("path_to_db"))
let migrator = Migrator(connection: connection, desiredSchemaVersion: 2)

try migrator.runMigrationsIfNecessary(
    migrationSQLForSchemaVersion: { version in
        var SQL: String = ""

        switch version {
        case 1:
            return "CREATE TABLE cars(id INTEGER PRIMARY KEY, name TEXT, price INTEGER)"

        case 2:
            return "CREATE TABLE person(id INTEGER PRIMARY KEY, name TEXT, address TEXT)"

        default:
            break
        }

        return SQL
    },
    willMigrateToSchemaVersion: { version in
        print("Will migrate to schema version: \(version)")
    },
    didMigrateToSchemaVersion: { version in
        print("Did migrate to schema version: \(version)")
    }
)

所有的迁移必须从1开始,每次迭代递增1。例如,你第一次创建 Migrator 时,你想将 desiredSchemaVersion 设置为1,并实现 migrationSQLForSchemaVersion 闭包以返回你的初始数据库模式SQL。然后,每次你需要迁移数据库时,将 desiredSchemaVersion 增加1,并将新情况添加到 migrationSQLForSchemaVersion 模式闭包中。在生产应用程序中,编写实际的SQL文件,将它们添加到你的包中,并从文件中加载所需版本的SQL字符串将是最简单的方法。

备份

运行数据库的定期备份通常是个明智的选择,允许用户在检测到损坏时从备份中恢复。SQift使使用SQLite备份API安全地备份数据库变得极为容易。

let sourceConnection = try Connection(storageLocation: sourceLocation)
let destinationConnection = try Connection(storageLocation: destinationLocation)

let progress = try sourceConnection.backup(to: destinationConnection) { result in
    print(result)
}

backup API提供的 progress 实例可以用来监控进度以及取消备份操作。

默认情况下,备份操作按级联过程进行。它在每个操作中备份指定的页面大小,直到完成。传递 pageSize 的值为 -1 会导致备份在单个操作中执行。建议使用默认的 pageSize 值为 100,并允许操作迭代至完成。


FQA

为什么不使用CoreData?

CoreData和SQift之间有很多权衡。SQift绝对不是作为CoreData的替代品而创建的。它是为了使Swift中的SQLite工作尽可能简单和容易而创建的。试图在CoreData和SQift之间做出选择的任何人需要仔细考虑利弊后再做决定。两者都有显著的学习曲线,在集成到应用程序或框架之前都需要大量的前瞻性和架构设计。

我应该在我的项目中使用SQift吗?

也许吧。首先最重要的问题是你是否真的需要一个数据库。有许多其他方式可以存储数据,它们要简单得多。如果你确实有大量需要为查询进行索引的数据,那么数据库可能是最佳选择。

一旦你知道你需要数据库,那么你需要决定是否需要键值存储,或者需要完整的基于关系查询的权限。如果你只需要键值存储,那么有其他的库可以满足你的需求,它们更为简单和针对性更强。如果你需要SQLite的全部功能,那么使用SQift将会是一个好的选择。

是否有计划在SQift上添加基于SQL的Swift DSL?

这是我们已经多次考虑但仍未深入探讨的问题。目前,我们还没有计划构建一个DSL,但这个议题仍在考虑之中。如果我们决定尝试在SQift中添加DSL,我们需要确保不会让用户远离SQL。

SQift的主要目标是尽可能使Swift使用SQLite变得容易和方便。然而,便利并不意味着抽象。SQLite非常复杂,SQift的目标不是简化它,而是启用它。任何想在项目中使用SQift的人都必须深刻理解SQLite及其工作原理。这是故意为之的。


许可证

SQift在新的BSD许可证下发布。详情请见LICENSE文件。

创建者