diff --git a/docs/fluent/overview.zh.md b/docs/fluent/overview.zh.md new file mode 100644 index 00000000..062725ef --- /dev/null +++ b/docs/fluent/overview.zh.md @@ -0,0 +1,583 @@ +# Fluent + +Fluent 是服务于 Swift 的 [ORM](https://en.wikipedia.org/wiki/Object-relational_mapping) 框架,它利用 Swift 的强类型特性为你的数据库操作提供简易使用的接口。使用 Fluent 的核心是创建用于表示数据库中数据结构的模型,然后通过这些模型来执行创建、读取、更新和删除等操作,而不必编写原始的 SQL 查询语句。 + +## 配置 + +当在终端使用 `vapor new` 命令来创建项目时,对于包含询问 Fluent 的问答选项,请选择 'Yes',并选择要使用的数据库驱动程序,之后将自动生成依赖项添加到你的新项目以及配置示例代码。 + +### 现有项目 + +如果你想为现有项目添加 Fluent,你需要在 [package](../getting-started/spm.md) 中添加两个依赖项: + +- [vapor/fluent](https://github.com/vapor/fluent)@4.0.0 +- 你选择的一个(或多个)Fluent 驱动程序 + +```swift +.package(url: "https://github.com/vapor/fluent.git", from: "4.0.0"), +.package(url: "https://github.com/vapor/fluent--driver.git", from: ), +``` + +```swift +.target(name: "App", dependencies: [ + .product(name: "Fluent", package: "fluent"), + .product(name: "FluentDriver", package: "fluent--driver"), + .product(name: "Vapor", package: "vapor"), +]), +``` + +一旦这些包被添加为依赖项,你就可以在 `configure.swift` 中使用 `app.databases` 配置你的数据库。 + +```swift +import Fluent +import FluentDriver + +app.databases.use(, as: ) +``` + +下面的每个 Fluent 驱动程序都有更具体的配置说明。 + +### 驱动 + +Fluent 目前有四个官方支持的驱动程序。你可以在 GitHub 上搜索标签 [`fluent-driver`](https://github.com/topics/fluent-driver) 来获取官方和第三方 Fluent 数据库驱动程序的完整列表。 + +#### PostgreSQL + +PostgreSQL 是一个开源的、符合标准的 SQL 数据库。 它很容易在大多数云托管提供商上进行配置。这是 Fluent **推荐**的数据库驱动程序。 + +要使用 PostgreSQL,请将以下依赖项添加到 package 中。 + +```swift +.package(url: "https://github.com/vapor/fluent-postgres-driver.git", from: "2.0.0") +``` + +```swift +.product(name: "FluentPostgresDriver", package: "fluent-postgres-driver") +``` + +添加依赖项后,在 `configure.swift` 中使用 `app.databases.use` 配置数据库的凭证。 + +```swift +import Fluent +import FluentPostgresDriver + +app.databases.use(.postgres(hostname: "localhost", username: "vapor", password: "vapor", database: "vapor"), as: .psql) +``` + +还可以从数据库连接字符串解析凭证。 + +```swift +try app.databases.use(.postgres(url: ""), as: .psql) +``` + +#### SQLite + +SQLite 是一种开源的,嵌入式 SQL 数据库。它的简单性使其成为原型设计和测试的理想选择。 + +要使用 SQLite,请将以下依赖项添加到 package 中。 + +```swift +.package(url: "https://github.com/vapor/fluent-sqlite-driver.git", from: "4.0.0") +``` + +```swift +.product(name: "FluentSQLiteDriver", package: "fluent-sqlite-driver") +``` + +添加依赖项后,在 `configure.swift` 中使用 `app.databases.use` 配置数据库的凭证。 + +```swift +import Fluent +import FluentSQLiteDriver + +app.databases.use(.sqlite(.file("db.sqlite")), as: .sqlite) +``` + +你还可以配置 SQLite 将数据库临时存储在内存中。 + +```swift +app.databases.use(.sqlite(.memory), as: .sqlite) +``` + +如果使用内存中的数据库,请确保使用 `--auto-migrate` 将 Fluent 设置为自动迁移,或者在添加迁移后运行 `app.autoMigrate()`。 + +```swift +app.migrations.add(CreateTodo()) +try app.autoMigrate().wait() +// or +try await app.autoMigrate() +``` + +!!! 提示 + SQLite 配置自动对所有创建的连接启用外键约束,但不会更改数据库本身的外键配置。直接删除数据库中的记录可能会违反外键约束和触发器。 + +#### MySQL + +MySQL 是一种流行的开源 SQL 数据库。许多云托管服务提供商都提供它。该驱动程序还支持 MariaDB。 + +要使用 MySQL,请将以下依赖项添加到你的 package 中。 + +```swift +.package(url: "https://github.com/vapor/fluent-mysql-driver.git", from: "4.0.0") +``` + +```swift +.product(name: "FluentMySQLDriver", package: "fluent-mysql-driver") +``` + +添加依赖项后,在 `configure.swift` 中使用 `app.databases.use` 配置数据库的凭证。 + +```swift +import Fluent +import FluentMySQLDriver + +app.databases.use(.mysql(hostname: "localhost", username: "vapor", password: "vapor", database: "vapor"), as: .mysql) +``` + +你还可以从数据库连接字符串中解析凭证。 + +```swift +try app.databases.use(.mysql(url: ""), as: .mysql) +``` + +要配置不涉及 SSL 证书的本地连接,你应该禁用证书验证。例如,如果连接到 Docker 中的 MySQL 8 数据库,你可能需要这样做。 + +```swift +var tls = TLSConfiguration.makeClientConfiguration() +tls.certificateVerification = .none + +app.databases.use(.mysql( + hostname: "localhost", + username: "vapor", + password: "vapor", + database: "vapor", + tlsConfiguration: tls +), as: .mysql) +``` + +!!! 警告 + 不要在生产中禁用证书验证。你应该向 `TLSConfiguration` 提供一个证书以进行验证。 + +#### MongoDB + +MongoDB 是一种流行的无模式 NoSQL 数据库,专为程序员设计。该驱动程序支持 3.4 及更高版本的所有云托管提供商和自托管安装。 + +!!! 注意 + 该驱动程序由一个名为 [MongoKitten](https://github.com/OpenKitten/MongoKitten) 的社区创建和维护的 MongoDB 客户端提供支持。MongoDB 维护着一个官方客户端,[mongo-swift-driver](https://github.com/mongodb/mongo-swift-driver) 以及 Vapor 集成的 [mongodb-vapor](https://github.com/mongodb/mongodb-vapor)。 + +要使用 MongoDB,请将以下依赖项添加到你的 package 中。 + +```swift +.package(url: "https://github.com/vapor/fluent-mongo-driver.git", from: "1.0.0"), +``` + +```swift +.product(name: "FluentMongoDriver", package: "fluent-mongo-driver") +``` + +添加依赖项后,在 `configure.swift` 中使用 `app.databases.use` 配置数据库的凭证。 + +要进行连接,请传递标准 MongoDB [连接 URI 格式](https://docs.mongodb.com/master/reference/connection-string/index.html)的连接字符串。 + + +```swift +import Fluent +import FluentMongoDriver + +try app.databases.use(.mongo(connectionString: ""), as: .mongo) +``` + +## Models + +模型表示数据库中固定的数据结构,如表或集合。模型有一个或多个存储可编码值的字段。所有模型都有一个唯一的标识符。属性包装器用于表示标识符和字段以及后面提到的更复杂的映射。看看下面这个代表星系的模型。 + +```swift +final class Galaxy: Model { + // Name of the table or collection. + static let schema = "galaxies" + + // Unique identifier for this Galaxy. + @ID(key: .id) + var id: UUID? + + // The Galaxy's name. + @Field(key: "name") + var name: String + + // Creates a new, empty Galaxy. + init() { } + + // Creates a new Galaxy with all properties set. + init(id: UUID? = nil, name: String) { + self.id = id + self.name = name + } +} +``` + +要创建一个新的模型,请创建一个遵循 `Model` 协议的类。 + +!!! 建议 + 建议将模型类标记为 `final`,以提高性能并简化一致性要求。 + +`Model` 协议第一个要求是静态字符串 `schema`。 + +```swift +static let schema = "galaxies" +``` + +此属性告诉 Fluent 模型对应于哪个表或集合。这可以是数据库中已经存在的表,也可以是你将通过 [migration](#migrations) 创建的表。schema 通常是 `snake_case` 复数形式. + +### Identifier + +下一个要求是一个名为 `id` 的标识符字段。 + +```swift +@ID(key: .id) +var id: UUID? +``` + +该字段必须使用 `@ID` 属性包装器。Fluent 建议使用 `UUID` 和 特殊 `.id` 字段键,因为它兼容 Fluent 的所有驱动程序。 + +如果要使用自定义 ID 键或类型, 请使用 [`@ID(custom:)`](model.md#custom-identifier) 重载。 + +### Fields + +添加标识符之后,你可以添加任意多的字段来存储额外的信息。在本例中,唯一的附加字段是星系的名称。 + +```swift +@Field(key: "name") +var name: String +``` + +对于简单字段,使用 `@Field` 属性包装器。 与 `@ID` 一样,`key` 参数指定数据库中字段的名称。这在数据库字段命名约定可能与 Swift 不同的情况下特别有用,例如,使用 `snake_case` 而不是 `camelCase`。 + +接下来,所有模型都需要一个空的 init。这允许 Fluent 创建模型的新实例。 + +```swift +init() { } +``` + +最后,你可以为模型添加一个方便的 init 来设置其所有属性。 + +```swift +init(id: UUID? = nil, name: String) { + self.id = id + self.name = name +} +``` + +如果你向模型添加新属性,则使用便捷的 inits 尤其有用,因为如果 init 方法发生更改,你可能会收到编译时错误。 + +## Migrations + +如果数据库使用预定义的模式,如 SQL 数据库,则需要进行迁移,以便为模型准备数据库。迁移对于用数据填充数据库也很有用。要创建一个迁移,定义一个符合 `migration` 或 `AsyncMigration` 协议的新类型。看看下面对之前定义的 `Galaxy` 模型的迁移。 + +```swift +struct CreateGalaxy: AsyncMigration { + // Prepares the database for storing Galaxy models. + func prepare(on database: Database) async throws { + try await database.schema("galaxies") + .id() + .field("name", .string) + .create() + } + + // Optionally reverts the changes made in the prepare method. + func revert(on database: Database) async throws { + try await database.schema("galaxies").delete() + } +} +``` + +该 `prepare` 方法用于准备数据库以存储 `Galaxy` 模型。 + +### Schema + +在此方法中,`database.schema(_:)` 用来创建一个新的 `schembuilder`。然后,在调用 `create()` 创建模式之前,将一个或多个 `字段` 添加到构建器中。 + +添加到构建器的每个字段都有一个名称、类型和可选约束。 + +```swift +field(, , ) +``` + +使用 Fluent 推荐的默认值,有一个方便的 `id()` 方法可以添加 `@ID` 属性。 + +恢复迁移会撤消在 prepare 方法中所做的任何更改。在这种情况下,这意味着删除 Galaxy 的模式。 + +一旦定义了迁移,你必须将迁移添加到 `configure.swift` 中的 `app.migrations` 来告知 Fluent 。 + +```swift +app.migrations.add(CreateGalaxy()) +``` + +### Migrate + +要运行迁移,在命令行中调用 `vapor run migrate` 或者添加 `migrate` 参数到 Xcode 的运行方案中。 + + +``` +$ vapor run migrate +Migrate Command: Prepare +The following migration(s) will be prepared: ++ CreateGalaxy on default +Would you like to continue? +y/n> y +Migration successful +``` + +## Querying + +现在你已经成功地创建了一个模型并迁移了你的数据库,你可以进行第一次查询了。 + +### All + +看看下面的路由,它将返回一个包含数据库中所有星系的数组。 + +```swift +app.get("galaxies") { req async throws in + try await Galaxy.query(on: req.db).all() +} +``` + +为了在路由闭包中直接返回 Galaxy,添加遵循 `Content` 协议 + +```swift +final class Galaxy: Model, Content { + ... +} +``` + +`Galaxy.query` 用于为模型创建新的查询构建器。`req.db` 是对你的应用程序的默认数据库的引用。 最后, `all()` 返回存储在数据库中的所有模型。 + +如果你编译并运行项目并请求 `GET /galaxies`, 你会看到返回一个空数组。 让我们添加一个创建新星系的路由。 + +### Create + + +根据 RESTful 约定,使用 `POST /galaxies` 端点创建新星系。 由于模型是可编码的,因此你可以直接从请求正文中解码星系。 + +```swift +app.post("galaxies") { req -> EventLoopFuture in + let galaxy = try req.content.decode(Galaxy.self) + return galaxy.create(on: req.db) + .map { galaxy } +} +``` + +!!! 也可以看看 + 有关解码请求正文的更多信息,请参阅 [内容 → 概述](../basics/content.md)。 + +一旦有了模型的实例,调用 `create(on:)` 就会将模型保存到数据库中。这将返回一个 `EventLoopFuture` 表示保存已完成的信号。保存完成后,使用 `map` 返回新创建的模型。 + +如果你正在使用 `async/await` 你可以这样编写代码: + +```swift +app.post("galaxies") { req async throws -> Galaxy in + let galaxy = try req.content.decode(Galaxy.self) + try await galaxy.create(on: req.db) + return galaxy +} +``` + +在这种情况下,异步版本不会返回任何内容,但会在保存完成后返回。 + +构建并运行项目并发送以下请求。 + +```http +POST /galaxies HTTP/1.1 +content-length: 21 +content-type: application/json + +{ + "name": "Milky Way" +} +``` + +你应该以标识符作为响应来取回创建的模型。 + +```json +{ + "id": ..., + "name": "Milky Way" +} +``` + +现在,如果你 `GET /galaxies` 再次查询,你应该会看到数组中返回了新创建的星系。 + + +## Relations + +没有恒星的星系算什么! 让我们通过在 `Galaxy` 和新的 `Star` 模型之间添加一对多关系来快速了解一下 `Fluent` 强大的关系特性。 + +```swift +final class Star: Model, Content { + // Name of the table or collection. + static let schema = "stars" + + // Unique identifier for this Star. + @ID(key: .id) + var id: UUID? + + // The Star's name. + @Field(key: "name") + var name: String + + // Reference to the Galaxy this Star is in. + @Parent(key: "galaxy_id") + var galaxy: Galaxy + + // Creates a new, empty Star. + init() { } + + // Creates a new Star with all properties set. + init(id: UUID? = nil, name: String, galaxyID: UUID) { + self.id = id + self.name = name + self.$galaxy.id = galaxyID + } +} +``` + +### Parent + +除了 `@Parent` 新字段类型,新的 `Star` 模型与 `Galaxy` 非常相似。 + +```swift +@Parent(key: "galaxy_id") +var galaxy: Galaxy +``` + +parent 属性是存储另一个模型标识符的字段。持有引用的模型称为`子`,被引用的模型称为`父`。这种类型的关系也称为“一对多”。该 `key` 参数指定了用于在数据库中存储父键的字段名称。 + +在 init 方法中,父标识符使用 `$galaxy`。 + +```swift +self.$galaxy.id = galaxyID +``` + +通过为父属性的名称添加前缀 `$`,你可以访问底层属性包装器。这是访问 `@Field` 存储实际标识符值的内部所必需的。 + +!!! 也可以看看 + 查看 Swift Evolution 关于属性包装器的提案以获得更多信息:[[SE-0258] Property Wrappers](https://github.com/apple/swift-evolution/blob/master/proposals/0258-property-wrappers.md) + +接下来,创建一个迁移以准备数据库来处理 `Star`。 + +```swift +struct CreateStar: AsyncMigration { + // Prepares the database for storing Star models. + func prepare(on database: Database) async throws { + try await database.schema("stars") + .id() + .field("name", .string) + .field("galaxy_id", .uuid, .references("galaxies", "id")) + .create() + } + + // Optionally reverts the changes made in the prepare method. + func revert(on database: Database) async throws { + try await database.schema("stars").delete() + } +} +``` + +这与星系的迁移基本相同,除了存储父星系标识符的额外字段。 + +```swift +field("galaxy_id", .uuid, .references("galaxies", "id")) +``` + +该字段指定了一个可选的约束,告诉数据库该字段的值引用了 “galaxies” 模式中的字段 “id”。这也称为外键,有助于确保数据完整性。 + +一旦迁移创建完成,将其添加到 `app.migrations` 中的 `CreateGalaxy`之后。 + +```swift +app.migrations.add(CreateGalaxy()) +app.migrations.add(CreateStar()) +``` + +因为迁移是按顺序运行的,而且 `CreateStar` 引用了星系模式,所以顺序是很重要的。最后,[运行迁移](#migrate)准备数据库。 + +添加创建新恒星的路由。 + +```swift +app.post("stars") { req async throws -> Star in + let star = try req.content.decode(Star.self) + try await star.create(on: req.db) + return star +} +``` +使用下面的HTTP请求创建一个引用之前创建的星系的新星。 + +```http +POST /stars HTTP/1.1 +content-length: 36 +content-type: application/json + +{ + "name": "Sun", + "galaxy": { + "id": ... + } +} +``` + +你应该看到新创建的星星返回一个惟一标识符。 + +```json +{ + "id": ..., + "name": "Sun", + "galaxy": { + "id": ... + } +} +``` + +### Children + +现在让我们看看如何利用 Fluent 的预加载功能自动返回 `GET /galaxies` 路由中的星系恒星。将以下属性添加到 `Galaxy`模型中。 + +```swift +// All the Stars in this Galaxy. +@Children(for: \.$galaxy) +var stars: [Star] +``` + +`@Children` 属性包装器与 `@Parent`相反。它采用子 `@Parent` 字段的键路径作为 `for`参数。它的值是一个子模型数组,因为可能存在零个或多个子模型。星系的迁移不需要改变,因为这种关系所需的所有信息都存储在`恒星`上。 + + +### Eager Load + +现在关系已经完成,你可以在查询构建器上使用 `with` 方法来自动获取和序列化星系-星型关系。 + +```swift +app.get("galaxies") { req in + try await Galaxy.query(on: req.db).with(\.$stars).all() +} +``` + +一个指向 `@Children` 关系的键路径被传递给 `with`,告诉 Fluent 在所有结果模型中自动加载这个关系。创建并运行另一个请求到 `GET /galaxies`。你现在应该看到响应中自动包含星星。 + +```json +[ + { + "id": ..., + "name": "Milky Way", + "stars": [ + { + "id": ..., + "name": "Sun", + "galaxy": { + "id": ... + } + } + ] + } +] +``` + +## 下一步 + +祝贺你创建了第一个模型和迁移,并执行了基本的创建和读取操作。要了解更多关于所有这些特性的深入信息,请查看 Fluent 指南中的相应部分。 \ No newline at end of file