一个 SQLite 数据库包装器和模型层,使用 Swift 并发和 Codable
,没有其他依赖项。
哲学
Blackbird 是一个 beta 版本。
仍然可能发生细微更改,从而破坏与代码或数据库的向后兼容性。
我现在正在发布软件中使用 Blackbird,但风险自负。
一个协议,用于在由 SQLite 驱动的 Blackbird.Database 中存储结构体,并为常见操作提供编译器检查的键路径。
以下是如何定义一个表
import Blackbird
struct Post: BlackbirdModel {
@BlackbirdColumn var id: Int
@BlackbirdColumn var title: String
@BlackbirdColumn var url: URL?
}
就是这样。无需 CREATE TABLE
,无需单独的表定义逻辑,无需其他步骤。
并且具有自动迁移功能。想要添加或删除列或索引,或者开始使用 Blackbird 的更多功能,例如自定义 enum
列、唯一索引或自定义主键?只需更改代码
struct Post: BlackbirdModel {
static var primaryKey: [BlackbirdColumnKeyPath] = [ \.$guid, \.$id ]
static var indexes: [[BlackbirdColumnKeyPath]] = [
[ \.$title ],
[ \.$publishedDate, \.$format ],
]
static var uniqueIndexes: [[BlackbirdColumnKeyPath]] = [
[ \.$guid ],
]
enum Format: Int, BlackbirdIntegerEnum {
case markdown
case html
}
@BlackbirdColumn var id: Int
@BlackbirdColumn var guid: String
@BlackbirdColumn var title: String
@BlackbirdColumn var publishedDate: Date?
@BlackbirdColumn var format: Format
@BlackbirdColumn var url: URL?
@BlackbirdColumn var image: Data?
}
……Blackbird 将在运行时自动将表迁移到新模式。
安全轻松地将实例写入 Blackbird.Database
let post = Post(id: 1, title: "What I had for breakfast")
try await post.write(to: db)
以多种不同的方式执行查询,首选使用键路径的结构化查询,以进行编译时检查、类型安全和便利性
// Fetch by primary key
let post = try await Post.read(from: db, id: 2)
// Or with a WHERE condition, using compiler-checked key-paths:
let posts = try await Post.read(from: db, matching: \.$title == "Sports")
// Select custom columns, with row dictionaries typed by key-path:
for row in try await Post.query(in: db, columns: [\.$id, \.$image], matching: \.$url != nil) {
let postID = row[\.$id] // returns Int
let imageData = row[\.$image] // returns Data?
}
永远不需要 SQL,但它始终可用
try await Post.query(in: db, "UPDATE $T SET format = ? WHERE date < ?", .html, date)
let posts = try await Post.read(from: db, sqlWhere: "title LIKE ? ORDER BY RANDOM()", "Sports%")
for row in try await Post.query(in: db, "SELECT MAX(id) AS max FROM $T WHERE url = ?", url) {
let maxID = row["max"]?.intValue
}
使用 Combine 监视行和列级别的更改
let listener = Post.changePublisher(in: db).sink { change in
if change.hasPrimaryKeyChanged(7) {
print("Post 7 has changed")
}
if change.hasColumnChanged(\.$title) {
print("A title has changed")
}
}
// Or monitor a single column by key-path:
let listener = Post.changePublisher(in: db, columns: [\.$title]).sink { _ in
print("A post's title changed")
}
// Or listen for changes for a specific primary key:
let listener = Post.changePublisher(in: db, primaryKey: 3, columns: [\.$title]).sink { _ in
print("Post 3's title changed")
}
Blackbird 专为 SwiftUI 设计,提供异步加载、自动更新的结果包装器
struct RootView: View {
// The database that all child views will automatically use
@StateObject var database = try! Blackbird.Database.inMemoryDatabase()
var body: some View {
PostListView()
.environment(\.blackbirdDatabase, database)
}
}
struct PostListView: View {
// Async-loading, auto-updating array of matching instances
@BlackbirdLiveModels({ try await Post.read(from: $0, orderBy: .ascending(\.$id)) }) var posts
// Async-loading, auto-updating rows from a custom query
@BlackbirdLiveQuery(tableName: "Post", { try await $0.query("SELECT MAX(id) AS max FROM Post") }) var maxID
var body: some View {
VStack {
if posts.didLoad {
List {
ForEach(posts.results) { post in
NavigationLink(destination: PostView(post: post.liveModel)) {
Text(post.title)
}
}
}
} else {
ProgressView()
}
}
.navigationTitle(maxID.didLoad ? "\(maxID.results.first?["max"]?.intValue ?? 0) posts" : "Loading…")
}
}
struct PostView: View {
// Auto-updating instance
@BlackbirdLiveModel var post: Post?
var body: some View {
VStack {
if let post {
Text(post.title)
}
}
}
}
一个轻量级的异步包装器,围绕 SQLite,可以与 BlackbirdModel 一起使用,也可以单独使用。
let db = try Blackbird.Database(path: "/tmp/db.sqlite")
// SELECT with parameterized queries
for row in try await db.query("SELECT id FROM posts WHERE state = ?", 1) {
let id = row["id"]?.intValue
// ...
}
// Run direct queries
try await db.execute("UPDATE posts SET comments = NULL")
// Transactions with synchronous queries
try await db.transaction { core in
try core.query("INSERT INTO posts VALUES (?, ?)", 16, "Sports!")
try core.query("INSERT INTO posts VALUES (?, ?)", 17, "Dewey Defeats Truman")
}
静态类型反射,用于更清晰的模式检测: Swift 目前没有办法在不创建实例的情况下反射一个类型的属性 —— Mirror 仅反映给定实例的属性名称和值。如果该语言将来添加静态类型反射,我的模式检测就不需要依赖于使用 Decoder 生成空实例的 hack。
KeyPath 与 String 之间的转换,类型 KeyPaths 的静态反射: 如果能够获取类型的可用 KeyPaths(无需一些可怕的黑客技巧)并在运行时从字符串创建 KeyPaths,我许多使用 Codable 的 hack 都可以用 KeyPaths 替换,这将更简洁,可能也更快。
获取 CodingKeys 枚举名称和自定义值的方法: 目前不可能获取 CodingKeys
case 的名称,只能求助于 这个可怕的 hack。解码器必须知道这些名称才能对可能声明了自定义 CodingKeys
的任意类型执行正确的解码。 如果此 hack 停止工作,BlackbirdModel 将无法支持自定义 CodingKeys
。
更清晰的协议名称 (Blackbird.Model
): 协议不能包含点或嵌套在另一种类型中。
协议内部的嵌套结构体定义 可以使我的许多 “BlackbirdModel…” 名称更短。
那架飞机,当然。
它古老、很棒而且速度极快。好吧,这个数据库库是基于古老、很棒的技术(SQLite),而且速度极快。
(但说实话,主要是因为它是一架很酷的飞机。我通常根本不在乎飞机。只是那一架。)
当然 有。谁在乎?
这是一个数据库引擎,最多只有少数极客会使用它。它叫什么名字并不重要。
我喜欢独特的名称(而不是通用或描述性名称,如 Model
或 SwiftSQLite
),因为它们更容易搜索,并且更难与其他类型混淆。所以我想要一些令人难忘的东西。我想我可以把它叫做 ButtDB
——令人难忘!——但随着我在未来几年使用它,我希望在所有 struct
定义后输入一些更酷的东西。
Blackbird 旨在使编写具有最常见、最直接数据库需求的应用程序变得非常快速和容易。
在许多方面都支持自定义 SQL,但 Blackbird 不直接支持更高级的 SQLite 行为,如触发器、视图、窗口、外键约束、级联写入、部分或表达式索引、虚拟列等,如果使用可能会导致未定义的行为。
通过不支持应用程序通常不需要的深奥或专门的功能,Blackbird 能够为常见情况提供更简洁的 API 和更有用的功能。
我喜欢编写自己的库。
我的库可以完美地满足我的需求以及我期望它们的工作方式。如果我的需求或期望发生变化,我可以更改库。
在编写它们时,我也学到了很多东西,锻炼和提高我的技能,从而使我的其他工作受益。
当我编写库时,我了解在使用它们时一切是如何工作的,因此减少了错误并编写了更高效的软件。
我知道。具有讽刺意味的是,我以最快的飞机命名了这个。
Blackbird 针对开发速度进行了优化。它的执行速度也很快,但清晰度、易用性、减少重复性和简单的工具是更高的优先级。
Blackbird 还提供自动缓存和细粒度的更改报告。这有助于应用程序避免许多不必要的查询、重新加载和 UI 刷新,从而可以提高整体应用程序性能。
其他 Swift SQLite 库可以通过省略 Blackbird 的大部分反射、抽象和键路径使用来实现更快的原始数据库性能。 一些使用代码生成方法,这些方法可以非常有效地执行,但比我想要的更复杂化开发。 其他人则采用较少抽象的方法,这些方法能够实现更多自定义行为,但使使用更加复杂。
我选择了不同的权衡方案,以更好地满足我的需求。 我从未编写过一个读取其数据库速度太慢的应用程序,但我经常为大型、复杂代码库的维护而苦恼。
Blackbird 的目标是实现我在易用性和避免错误方面的理想平衡,即使因此它不是执行速度最快的 SQLite 库。
手机变得越来越快,但 bug 永远是一个 bug。