黑鸟 (Blackbird)

一个 SQLite 数据库包装器和模型层,使用 Swift 并发和 Codable,没有其他依赖项。

哲学

项目状态

Blackbird 是一个 beta 版本。

仍然可能发生细微更改,从而破坏与代码或数据库的向后兼容性。

我现在正在发布软件中使用 Blackbird,但风险自负。

BlackbirdModel

一个协议,用于在由 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")
}

SwiftUI

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)
            }
        }
    }
}

Blackbird.Database

一个轻量级的异步包装器,围绕 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 语言功能的愿望清单

常见问题解答

为什么叫 blackbird

那架飞机,当然。

它古老、很棒而且速度极快。好吧,这个数据库库是基于古老、很棒的技术(SQLite),而且速度极快。

(但说实话,主要是因为它是一架很酷的飞机。我通常根本不在乎飞机。只是那一架。)

你知道有很多其他东西也叫这个

当然 。谁在乎?

这是一个数据库引擎,最多只有少数极客会使用它。它叫什么名字并不重要。

我喜欢独特的名称(而不是通用或描述性名称,如 ModelSwiftSQLite),因为它们更容易搜索,并且更难与其他类型混淆。所以我想要一些令人难忘的东西。我想我可以把它叫做 ButtDB——令人难忘!——但随着我在未来几年使用它,我希望在所有 struct 定义后输入一些更酷的东西。

为什么你不支持 [SQLite 功能]

Blackbird 旨在使编写具有最常见、最直接数据库需求的应用程序变得非常快速和容易。

在许多方面都支持自定义 SQL,但 Blackbird 不直接支持更高级的 SQLite 行为,如触发器、视图、窗口、外键约束、级联写入、部分或表达式索引、虚拟列等,如果使用可能会导致未定义的行为。

通过不支持应用程序通常不需要的深奥或专门的功能,Blackbird 能够为常见情况提供更简洁的 API 和更有用的功能。

为什么你不直接使用 [其他 SQLite 库]

我喜欢编写自己的库。

我的库可以完美地满足我的需求以及我期望它们的工作方式。如果我的需求或期望发生变化,我可以更改库。

在编写它们时,我也学到了很多东西,锻炼和提高我的技能,从而使我的其他工作受益。

当我编写库时,我了解在使用它们时一切是如何工作的,因此减少了错误并编写了更高效的软件。

你知道 [其他 SQLite 库] 更快

我知道。具有讽刺意味的是,我以最快的飞机命名了这个。

Blackbird 针对开发速度进行了优化。它的执行速度也很快,但清晰度、易用性、减少重复性和简单的工具是更高的优先级。

Blackbird 还提供自动缓存和细粒度的更改报告。这有助于应用程序避免许多不必要的查询、重新加载和 UI 刷新,从而可以提高整体应用程序性能。

其他 Swift SQLite 库可以通过省略 Blackbird 的大部分反射、抽象和键路径使用来实现更快的原始数据库性能。 一些使用代码生成方法,这些方法可以非常有效地执行,但比我想要的更复杂化开发。 其他人则采用较少抽象的方法,这些方法能够实现更多自定义行为,但使使用更加复杂。

我选择了不同的权衡方案,以更好地满足我的需求。 我从未编写过一个读取其数据库速度太慢的应用程序,但我经常为大型、复杂代码库的维护而苦恼。

Blackbird 的目标是实现我在易用性和避免错误方面的理想平衡,即使因此它不是执行速度最快的 SQLite 库。

手机变得越来越快,但 bug 永远是一个 bug。