一个快速、纯 Swift 编写的 MongoDB 驱动程序,基于 Swift NIO 构建,专为服务器端 Swift 设计。它具有出色的 API 和经过实战考验的核心。支持服务器和嵌入式环境中的 MongoDB。
MongoKitten 是一个完全异步的驱动程序,这意味着它不会阻塞任何线程。这也意味着它可以用于任何异步环境,例如 Vapor 或 Hummingbird。
在 5 分钟内启动并运行
// Add to Package.swift
.package(url: "https://github.com/orlandos-nl/MongoKitten.git", from: "7.2.0")
// In your code
import MongoKitten
// Connect to database
let db = try await MongoDatabase.connect(to: "mongodb:///my_database")
// Insert a document
try await db["users"].insert(["name": "Alice", "age": 30])
// Query documents
let users = try await db["users"].find("age" >= 18).drain()
// Use with Codable
struct User: Codable {
let name: String
let age: Int
}
let typedUsers = try await db["users"]
.find()
.decode(User.self)
.drain()
加入我们的 Discord 以获得任何问题和友好的交流。
如果您需要项目方面的实际支持,我们的团队可以通过 joannis@unbeatable.software 联系。
查看使用 MongoKitten & Vapor 的示例代码
已经出现了一些基于 MongoKitten 的项目,快去看看!
如果您还没有,您应该设置一个 MongoDB 服务器以开始使用 MongoKitten。MongoKitten 支持 MongoDB 3.6 及以上版本。
对于开发,这可以在您的本地计算机上进行。
安装适用于 Ubuntu、macOS 或任何其他受支持的 Linux 发行版的 MongoDB。
或者,使用 DAAS(数据库即服务),例如 MongoDB Atlas。
MongoKitten 使用 Swift Package Manager。在您的 Package.swift 文件中将 MongoKitten 添加到您的依赖项中
.package(url: "https://github.com/orlandos-nl/MongoKitten.git", from: "7.2.0")
另外,不要忘记将 product "MongoKitten"
添加为您的 target 的依赖项。
.product(name: "MongoKitten", package: "MongoKitten"),
Meow 是一个 ORM,位于同一个包中。
.product(name: "Meow", package: "MongoKitten"),
首先,连接到数据库
import MongoKitten
let db = try await MongoDatabase.connect(to: "mongodb:///my_database")
Vapor 用户应该将数据库注册为服务
extension Request {
public var mongo: MongoDatabase {
return application.mongo.adoptingLogMetadata([
"request-id": .string(id)
])
}
}
private struct MongoDBStorageKey: StorageKey {
typealias Value = MongoDatabase
}
extension Application {
public var mongo: MongoDatabase {
get {
storage[MongoDBStorageKey.self]!
}
set {
storage[MongoDBStorageKey.self] = newValue
}
}
public func initializeMongoDB(connectionString: String) throws {
self.mongo = try MongoDatabase.lazyConnect(to: connectionString)
}
}
Hummingbird 用户也是如此
extension HBApplication {
public var mongo: MongoDatabase {
get { extensions.get(\.mongo) }
set { extensions.set(\.mongo, value: newValue) }
}
}
extension HBRequest {
public var mongo: MongoDatabase {
application.mongo.adoptingLogMetadata([
"hb_id": .string(id)
])
}
}
确保在启动应用程序之前实例化数据库驱动程序。
对于 Vapor
try app.initializeMongoDB(connectionString: "mongodb:///my-app")
对于 Hummingbird
app.mongo = try MongoDatabase.lazyConnect(to: "mongodb:///my-app")
在 MongoKitten 中,您会发现连接到 MongoDB 的两种主要变体。
connect
调用是 async throws
,并且会立即尝试建立连接。 如果不成功,这些函数会抛出错误。lazyConnect
调用是 throws
,并且会推迟建立连接,直到需要时才建立。 只有在提供的凭据无法使用时才会抛出错误。Connect 的优势在于,已启动的服务器已知具有连接。 MongoBD 的任何问题都将立即出现,并且可以轻松检查错误。
LazyConnect 在开发过程中很有用,因为在某些设置中,连接到 MongoDB 可能是一个耗时的过程。 LazyConnect 允许您几乎立即开始使用您的系统,而无需等待 MongoKitten。 另一个优点是,集群中断或定时不佳的拓扑更改不会影响应用程序启动。 因此,MongoKitten 可以简单地尝试在后台恢复。 但是,如果出现问题,则很难调试它。
在执行操作之前,您需要访问一个用于存储模型的集合。 这是 MongoDB 中相当于表的东西。
// The collection "users" in your database
let users = db["users"]
// Create a document to insert
let myUser: Document = ["username": "kitty", "password": "meow"]
// Insert the user into the collection
// The _id is automatically generated if it's not present
try await users.insert(myUser)
要在 MongoDB 中执行以下查询
{
"username": "kitty"
}
使用以下 MongoKitten 代码
if let kitty = try await users.findOne("username" == "kitty") {
// We've found kitty!
}
要在 MongoDB 中执行以下查询
{
"$or": [
{ "age": { "$lte": 16 } },
{ "age": { "$exists": false } }
]
}
使用以下 MongoKitten 代码
for try await user in users.find("age" <= 16 || "age" == nil) {
// Asynchronously iterates over each user in the cursor
}
您也可以自己输入查询,而无需使用查询构建器,就像这样
// This is the same as the previous example
users.findOne(["username": "kitty"])
Find 操作返回一个 Cursor
。 游标是指向查询结果集的指针。 您可以通过迭代结果或获取一个或所有结果来从游标中获取结果。
如果封闭的 Task
被取消,游标将自动关闭。
您可以将所有结果作为数组获取
// Fetch all results and collect them in an array
let users = try await users.find().drain()
请注意,对于非常大的结果集,这可能很危险。 仅当您确定查询的整个结果集可以舒适地放入内存中时,才使用 drain()
。
Find 操作返回一个 FindQueryBuilder
。 您可以使用 map
将此游标(和其他游标)延迟转换为不同的结果类型,这类似于数组或文档上的 map
。 基于 map 的一个简单的常用助手是 .decode(..)
,它将每个结果 Document 解码为您选择的 Decodable
实体。
let users: [User] = try await users.find().decode(User.self).drain()
您可以像在 MongoDB 文档中看到的那样更新和删除实体。
try await users.updateMany(where: "username" == "kitty", setting: ["age": 3], unsetting: nil)
结果被隐式丢弃,但您仍然可以获取和使用它。
try await users.deleteOne(where: "username" == "kitty")
let reply = try await users.deleteAll(where: "furType" == "fluffy")
print("Deleted \(reply.deletes) kitties 😿")
您可以使用 buildIndexes
方法在集合上创建索引。
try await users.buildIndexes {
// Unique indexes ensure that no two documents have the same value for a field
// See https://docs.mongodb.com/manual/core/index-unique/s
UniqueIndex(
named: "unique-username",
field: "username"
)
// Text indexes allow you to search for documents using text
// See https://docs.mongodb.com/manual/text-search/
TextScoreIndex(
named: "search-description",
field: "description"
)
// TTL Indexes expire documents after a certain amount of time
// See https://docs.mongodb.com/manual/core/index-ttl/
TTLIndex(
named: "expire-createdAt",
field: "createdAt",
expireAfterSeconds: 60 * 60 * 24 * 7 // 1 week
)
}
MongoDB 支持聚合管道。 您可以像这样使用它们
let pipeline = try await users.buildAggregate {
// Match all users that are 18 or older
Match(where: "age" >= 18)
// Sort by age, ascending
Sort(by: "age", direction: .ascending)
// Limit the results to 3
Limit(3)
}
// Pipeline is a cursor, so you can iterate over it
// This will iterate over the first 3 users that are 18 or older in ascending age order
for try await user in pipeline {
// Do something with the user
}
原子地执行多个操作
try await db.transaction { session in
let users = db["users"]
let accounts = db["accounts"]
try await users.insert(newUser)
try await accounts.insert(newAccount)
// Changes are only committed if no errors occur
}
MongoKitten 支持 GridFS。 您可以像这样使用它
let database: MongoDatabase = ...
let gridFS = GridFSBucket(in: database)
然后,您可以使用 GridFSBucket 上传和下载文件。
let blob: ByteBuffer = ...
let file = try await gridFS.upload(
blob,
filename: "invoice.pdf",
metadata: [
"invoiceNumber": 1234,
"invoiceDate": Date(),
"invoiceAmount": 123.45
]
)
(可选)您可以定义自定义块大小。 默认值为 255kb。
对于分块文件上传,您可以使用 GridFSFileWriter
let writer = GridFSFileWriter(toBucket: gridFS)
do {
// Stream the file from HTTP
for try await chunk in request.body {
// Assuming `chunk is ByteBuffer`
// Write each HTTP chunk to GridFS
try await writer.write(data: chunk)
}
// Finalize the file, making it available for reading
let file = try await writer.finalize(filename: "invoice.pdf", metadata: ["invoiceNumber": 1234])
} catch {
// Clean up written chunks, as the file upload failed
try await writer.cancel()
// rethrow original error
throw error
}
您可以使用 GridFSReader
或通过将 GridFSFile
作为 AsyncSequence
迭代来读取文件
// Find your file in GridFS
guard let file = try await gridFS.findFile("metadata.invoiceNumber" == 1234) else {
// File does not exist
throw Abort(.notFound)
}
// Get all bytes in one contiguous buffer
let bytes = try await file.reader.readByteBuffer()
// Stream the file
for try await chunk in file {
// `chunk is ByteBuffer`, now do something with the chunk!
}
MongoDB 是一个文档数据库,它在底层使用 BSON 来存储类似 JSON 的数据。 MongoKitten 在其配套项目 OpenKitten/BSON 中实现了 BSON 规范。 您可以在单独的 BSON 存储库中找到有关我们的 BSON 实现的更多信息,但以下是基本知识
通常,您像这样创建 BSON 文档
let documentA: Document = ["_id": ObjectId(), "username": "kitty", "password": "meow"]
let documentB: Document = ["kitty", 4]
从上面的示例中,我们可以学习到一些东西
Int
、String
、Double
和 Bool
,以及 Foundation 中的 Date
ObjectId
像普通数组和字典一样,Document
符合 Collection
协议。 因此,您通常可以直接使用您的 Document
,使用您已经从 Array
和 Dictionary
中了解的 API。 例如,您可以使用 for 循环迭代文档
for (key, value) in documentA {
// ...
}
for value in documentB.values {
// ...
}
Document 还提供了下标来访问各个元素。 下标返回 Primitive?
类型的值,因此您可能需要在使用它们之前使用 as?
将它们强制转换。
let username = documentA["username"] as? String
我们的 Document
类型以优化的、高效的方式实现,并提供了许多有用的特性来读取和操作数据,包括 Swift Dictionary
类型上不存在的特性。 最重要的是,Document
还实现了 Dictionary
上存在的大多数 API,因此学习曲线非常小。
MongoKitten 通过提供 BSONEncoder
和 BSONDecoder
类型来支持 Encodable
和 Decodable
(Codable
) 协议。 使用我们的编码器和解码器与使用 Foundation JSONEncoder
和 JSONDecoder
类非常相似,不同之处在于 BSONEncoder
生成 Document
的实例,而 BSONDecoder
接受 Document
的实例,而不是 Data
。
例如,假设我们要编码以下结构
struct User: Codable {
var profile: Profile?
var username: String
var password: String
var age: Int?
struct Profile: Codable {
var profilePicture: Data?
var firstName: String
var lastName: String
}
}
我们可以像这样编码和解码实例
let user: User = ...
let encoder = BSONEncoder()
let encoded: Document = try encoder.encode(user)
let decoder = BSONDecoder()
let decoded: User = try decoder.decode(User.self, from: encoded)
一些注意事项
BSONEncoder
和 BSONDecoder
的工作方式与其他编码器和解码器非常相似MongoKitten 为 MongoDB 变更流提供了强大的支持,允许您监视集合的实时更改
// Basic change stream usage
let stream = try await users.watch()
for try await change in stream {
switch change.operationType {
case .insert:
print("New document: \(change.fullDocument)")
case .update:
print("Updated fields: \(change.updateDescription?.updatedFields)")
case .delete:
print("Deleted document: \(change.documentKey)")
default:
break
}
}
// Type-safe change streams
struct User: Codable {
let id: ObjectId
let name: String
let email: String
}
let typedStream = try await users.watch(type: User.self)
for try await change in typedStream {
if let user = change.fullDocument {
print("User modified: \(user.name)")
}
}
MongoKitten 为日志记录和监控提供了内置支持
// Add logging metadata
let loggedDb = db.adoptingLogMetadata([
"service": "user-api",
"environment": "production"
])
// Use the logged database
let users = loggedDb["users"]
lazyConnect
connect
authSource
authSource=admin
,除非您知道您的 authSource 是什么。MongoDB 的默认值非常容易混淆。authMechanism
,请尝试移除它。MongoKitten 可以自动检测到正确的机制。explain()
检查您的索引Meow 是一个基于 MongoKitten 构建的轻量级但强大的 ORM 层。
extension Application {
public var meow: MeowDatabase {
MeowDatabase(mongo)
}
}
extension Request {
public var meow: MeowDatabase {
MeowDatabase(mongo)
}
}
extension HBApplication {
public var meow: MeowDatabase {
MeowDatabase(mongo)
}
}
extension HBRequest {
public var meow: MeowDatabase {
MeowDatabase(mongo)
}
}
Meow 中有两种主要的模型类型,本文档将重点介绍最常见的一种。
创建模型时,您的类型必须实现 Model
协议。
import Meow
struct User: Model {
..
}
每个模型都有一个 _id
字段,这是 MongoDB 的要求。该类型必须是 Codable 和 Hashable,其余由您决定。因此,您也可以将 _id
设置为复合键,例如 struct
。它必须仍然是唯一的且可哈希的,但生成的 Document 对于 MongoDB 来说是可以接受的。
每个字段都必须使用 @Field
属性包装器进行标记
import Meow
struct User: Model {
@Field var _id: ObjectId
@Field var email: String
}
您也可以使用嵌套类型,就像您期望的 MongoDB 一样。这些嵌套类型中的每个字段也必须使用 @Field
属性包装器进行标记,以使其可查询。
import Meow
struct UserProfile: Model {
@Field var firstName: String?
@Field var lastName: String?
@Field var age: Int
}
struct User: Model {
@Field var _id: ObjectId
@Field var email: String
@Field var profile: UserProfile
}
使用上面的模型,我们可以从 MeowCollection 中查询它。使用类型化的下标从 MeowDatabase 获取您的实例!
let users = meow[User.self]
接下来,运行 find
或 count
查询,但使用类型检查的语法!路径的每个部分都需要以 $
作为前缀来访问 Field
属性包装器。
let adultCount = try await users.count(matching: { user in
user.$profile.$age >= 18
})
由于 meow 只是回收了常见的 MongoKitten 类型,您可以像在 MongoKitten 中一样使用 find 查询游标。
let kids = try await users.find(matching: { user in
user.$profile.$age < 18
})
for try await kid in kids {
// TODO: Send verification email to parents
}
Meow 有一个名为 Reference
的辅助类型,您可以在模型中使用它,而不是复制标识符。这将在尝试解析模型时为您提供一些额外的帮助程序。
Reference 也是 Codable
并继承了标识符的 LosslessStringConvertible
。因此,它可以作为 Vapor 的 JWT 令牌中的 subject,或者在 Vapor 的路由参数中使用。
// GET /users/:id using Vapor
app.get("users", ":id") { req async throws -> User in
let id: Reference<User> = req.parameters.require("id")
return try await id.resolve(in: req.meow)
}