一个快速、纯 Swift 编写的 MongoDB 驱动程序,基于 Swift NIO 构建,专为服务器端 Swift 设计。它具有出色的 API 和经过实战考验的核心。支持服务器和嵌入式环境中的 MongoDB。

MongoKitten 是一个完全异步的驱动程序,这意味着它不会阻塞任何线程。这也意味着它可以用于任何异步环境,例如 VaporHummingbird

目录

快速开始

在 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 服务器

如果您还没有,您应该设置一个 MongoDB 服务器以开始使用 MongoKitten。MongoKitten 支持 MongoDB 3.6 及以上版本。

对于开发,这可以在您的本地计算机上进行。

安装适用于 UbuntumacOS 或任何其他受支持的 Linux 发行版的 MongoDB。

或者,使用 DAAS(数据库即服务),例如 MongoDB Atlas

将 MongoKitten 添加到您的 Swift 项目 🚀

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 (可选)

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

Connect 与 LazyConnect

在 MongoKitten 中,您会发现连接到 MongoDB 的两种主要变体。

Connect 的优势在于,已启动的服务器已知具有连接。 MongoBD 的任何问题都将立即出现,并且可以轻松检查错误。

LazyConnect 在开发过程中很有用,因为在某些设置中,连接到 MongoDB 可能是一个耗时的过程。 LazyConnect 允许您几乎立即开始使用您的系统,而无需等待 MongoKitten。 另一个优点是,集群中断或定时不佳的拓扑更改不会影响应用程序启动。 因此,MongoKitten 可以简单地尝试在后台恢复。 但是,如果出现问题,则很难调试它。

CRUD (增删改查)

在执行操作之前,您需要访问一个用于存储模型的集合。 这是 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
}

GridFS

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

关于 BSON

MongoDB 是一个文档数据库,它在底层使用 BSON 来存储类似 JSON 的数据。 MongoKitten 在其配套项目 OpenKitten/BSON 中实现了 BSON 规范。 您可以在单独的 BSON 存储库中找到有关我们的 BSON 实现的更多信息,但以下是基本知识

字面量

通常,您像这样创建 BSON 文档

let documentA: Document = ["_id": ObjectId(), "username": "kitty", "password": "meow"]
let documentB: Document = ["kitty", 4]

从上面的示例中,我们可以学习到一些东西

只是另一个集合

像普通数组和字典一样,Document 符合 Collection 协议。 因此,您通常可以直接使用您的 Document,使用您已经从 ArrayDictionary 中了解的 API。 例如,您可以使用 for 循环迭代文档

for (key, value) in documentA {
	// ...
}

for value in documentB.values {
	// ...
}

Document 还提供了下标来访问各个元素。 下标返回 Primitive? 类型的值,因此您可能需要在使用它们之前使用 as? 将它们强制转换。

let username = documentA["username"] as? String

DocumentDictionary 之间转换之前请三思

我们的 Document 类型以优化的、高效的方式实现,并提供了许多有用的特性来读取和操作数据,包括 Swift Dictionary 类型上不存在的特性。 最重要的是,Document 还实现了 Dictionary 上存在的大多数 API,因此学习曲线非常小。

Codable

MongoKitten 通过提供 BSONEncoderBSONDecoder 类型来支持 EncodableDecodable (Codable) 协议。 使用我们的编码器和解码器与使用 Foundation JSONEncoderJSONDecoder 类非常相似,不同之处在于 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)

一些注意事项

高级特性

变更流

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"]

最佳实践

连接管理

性能优化

问题排查

我无法连接到 MongoDB,身份验证失败!
  1. 请确保您已指定 authSource=admin,除非您知道您的 authSource 是什么。MongoDB 的默认值非常容易混淆。
  2. 如果您指定了 authMechanism,请尝试移除它。MongoKitten 可以自动检测到正确的机制。
变更流(Change Streams)无法正常工作
  1. 确保您已连接到复制集或分片集群
  2. 检查您的用户是否具有所需的权限
  3. 确认您没有尝试监视系统集合
性能问题
  1. 使用 explain() 检查您的索引
  2. 监控连接池的使用情况
  3. 确保您没有获取不必要的字段
  4. 为批量操作使用适当的批处理大小

Meow ORM

Meow 是一个基于 MongoKitten 构建的轻量级但强大的 ORM 层。

与 Vapor 集成

extension Application {
    public var meow: MeowDatabase {
        MeowDatabase(mongo)
    }
}

extension Request {
    public var meow: MeowDatabase {
        MeowDatabase(mongo)
    }
}

与 Hummingbird 集成

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]

接下来,运行 findcount 查询,但使用类型检查的语法!路径的每个部分都需要以 $ 作为前缀来访问 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)
}

赞助者