跳转至

Fluent

Fluent 是服务于 Swift 的 ORM 框架,它利用 Swift 的强类型特性为你的数据库操作提供简易使用的接口。使用 Fluent 的核心是创建用于表示数据库中数据结构的模型,然后通过这些模型来执行创建、读取、更新和删除等操作,而不必编写原始的 SQL 查询语句。

配置

当在终端使用 vapor new 命令来创建项目时,对于包含询问 Fluent 的问答选项,请选择 'Yes',并选择要使用的数据库驱动程序,之后将自动生成依赖项添加到你的新项目以及配置示例代码。

现有项目

如果你想为现有项目添加 Fluent,你需要在 package 中添加两个依赖项:

  • vapor/fluent@4.0.0
  • 你选择的一个(或多个)Fluent 驱动程序
.package(url: "https://github.com/vapor/fluent.git", from: "4.0.0"),
.package(url: "https://github.com/vapor/fluent-<db>-driver.git", from: <version>),
.target(name: "App", dependencies: [
    .product(name: "Fluent", package: "fluent"),
    .product(name: "Fluent<db>Driver", package: "fluent-<db>-driver"),
    .product(name: "Vapor", package: "vapor"),
]),

一旦这些包被添加为依赖项,你就可以在 configure.swift 中使用 app.databases 配置你的数据库。

import Fluent
import Fluent<db>Driver

app.databases.use(<db config>, as: <identifier>)

下面的每个 Fluent 驱动程序都有更具体的配置说明。

驱动

Fluent 目前有四个官方支持的驱动程序。你可以在 GitHub 上搜索标签 fluent-driver 来获取官方和第三方 Fluent 数据库驱动程序的完整列表。

PostgreSQL

PostgreSQL 是一个开源的、符合标准的 SQL 数据库。 它很容易在大多数云托管提供商上进行配置。这是 Fluent 推荐的数据库驱动程序。

要使用 PostgreSQL,请将以下依赖项添加到 package 中。

.package(url: "https://github.com/vapor/fluent-postgres-driver.git", from: "2.0.0")
.product(name: "FluentPostgresDriver", package: "fluent-postgres-driver")

添加依赖项后,在 configure.swift 中使用 app.databases.use 配置数据库的凭证。

import Fluent
import FluentPostgresDriver

app.databases.use(
    .postgres(
        configuration: .init(
            hostname: "localhost",
            username: "vapor",
            password: "vapor",
            database: "vapor",
            tls: .disable
        )
    ),
    as: .psql
)

还可以从数据库连接字符串解析凭证。

try app.databases.use(.postgres(url: "<connection string>"), as: .psql)

SQLite

SQLite 是一种开源的,嵌入式 SQL 数据库。它的简单性使其成为原型设计和测试的理想选择。

要使用 SQLite,请将以下依赖项添加到 package 中。

.package(url: "https://github.com/vapor/fluent-sqlite-driver.git", from: "4.0.0")
.product(name: "FluentSQLiteDriver", package: "fluent-sqlite-driver")

添加依赖项后,在 configure.swift 中使用 app.databases.use 配置数据库的凭证。

import Fluent
import FluentSQLiteDriver

app.databases.use(.sqlite(.file("db.sqlite")), as: .sqlite)

你还可以配置 SQLite 将数据库临时存储在内存中。

app.databases.use(.sqlite(.memory), as: .sqlite)

如果使用内存中的数据库,请确保使用 --auto-migrate 将 Fluent 设置为自动迁移,或者在添加迁移后运行 app.autoMigrate()

app.migrations.add(CreateTodo())
try app.autoMigrate().wait()
// or
try await app.autoMigrate()

建议

SQLite 配置自动对所有创建的连接启用外键约束,但不会更改数据库本身的外键配置。直接删除数据库中的记录可能会违反外键约束和触发器。

MySQL

MySQL 是一种流行的开源 SQL 数据库。许多云托管服务提供商都提供它。该驱动程序还支持 MariaDB。

要使用 MySQL,请将以下依赖项添加到你的 package 中。

.package(url: "https://github.com/vapor/fluent-mysql-driver.git", from: "4.0.0")
.product(name: "FluentMySQLDriver", package: "fluent-mysql-driver")

添加依赖项后,在 configure.swift 中使用 app.databases.use 配置数据库的凭证。

import Fluent
import FluentMySQLDriver

app.databases.use(.mysql(hostname: "localhost", username: "vapor", password: "vapor", database: "vapor"), as: .mysql)

你还可以从数据库连接字符串中解析凭证。

try app.databases.use(.mysql(url: "<connection string>"), as: .mysql)

要配置不涉及 SSL 证书的本地连接,你应该禁用证书验证。例如,如果连接到 Docker 中的 MySQL 8 数据库,你可能需要这样做。

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 的社区创建和维护的 MongoDB 客户端提供支持。MongoDB 维护着一个官方客户端,mongo-swift-driver 以及 Vapor 集成的 mongodb-vapor

要使用 MongoDB,请将以下依赖项添加到你的 package 中。

.package(url: "https://github.com/vapor/fluent-mongo-driver.git", from: "1.0.0"),
.product(name: "FluentMongoDriver", package: "fluent-mongo-driver")

添加依赖项后,在 configure.swift 中使用 app.databases.use 配置数据库的凭证。

要进行连接,请传递标准 MongoDB 连接 URI 格式的连接字符串。

import Fluent
import FluentMongoDriver

try app.databases.use(.mongo(connectionString: "<connection string>"), as: .mongo)

Models

模型表示数据库中固定的数据结构,如表或集合。模型有一个或多个存储可编码值的字段。所有模型都有一个唯一的标识符。属性包装器用于表示标识符和字段以及后面提到的更复杂的映射。看看下面这个代表星系的模型。

final class Galaxy: Model {
    // 表或集合名。
    static let schema = "galaxies"

    // 星系唯一标识符。
    @ID(key: .id)
    var id: UUID?

    // 星系名称。
    @Field(key: "name")
    var name: String

    // 创建一个空的星系。
    init() { }

    // 创建一个新的星系并设置所有属性。
    init(id: UUID? = nil, name: String) {
        self.id = id
        self.name = name
    }
}

要创建一个新的模型,请创建一个遵循 Model 协议的类。

建议

建议将模型类标记为 final,以提高性能并简化一致性要求。

Model 协议第一个要求是静态字符串 schema

static let schema = "galaxies"

此属性告诉 Fluent 模型对应于哪个表或集合。这可以是数据库中已经存在的表,也可以是你将通过 migration 创建的表。schema 通常是 snake_case 复数形式.

Identifier

下一个要求是一个名为 id 的标识符字段。

@ID(key: .id)
var id: UUID?

该字段必须使用 @ID 属性包装器。Fluent 建议使用 UUID 和 特殊 .id 字段键,因为它兼容 Fluent 的所有驱动程序。

如果要使用自定义 ID 键或类型, 请使用 @ID(custom:) 重载。

Fields

添加标识符之后,你可以添加任意多的字段来存储额外的信息。在本例中,唯一的附加字段是星系的名称。

@Field(key: "name")
var name: String

对于简单字段,使用 @Field 属性包装器。 与 @ID 一样,key 参数指定数据库中字段的名称。这在数据库字段命名约定可能与 Swift 不同的情况下特别有用,例如,使用 snake_case 而不是 camelCase

接下来,所有模型都需要一个空的 init。这允许 Fluent 创建模型的新实例。

init() { }

最后,你可以为模型添加一个方便的 init 来设置其所有属性。

init(id: UUID? = nil, name: String) {
    self.id = id
    self.name = name
}

如果你向模型添加新属性,则使用便捷的 inits 尤其有用,因为如果 init 方法发生更改,你可能会收到编译时错误。

Migrations

如果数据库使用预定义的模式,如 SQL 数据库,则需要进行迁移,以便为模型准备数据库。迁移对于用数据填充数据库也很有用。要创建一个迁移,定义一个符合 migrationAsyncMigration 协议的新类型。看看下面对之前定义的 Galaxy 模型的迁移。

struct CreateGalaxy: AsyncMigration {
    // 为存储 Galaxy 模型准备数据库。
    func prepare(on database: Database) async throws {
        try await database.schema("galaxies")
            .id()
            .field("name", .string)
            .create()
    }

    // 可选地恢复 prepare 方法中所做的更改。
    func revert(on database: Database) async throws {
        try await database.schema("galaxies").delete()
    }
}

prepare 方法用于准备数据库以存储 Galaxy 模型。

Schema

在此方法中,database.schema(_:) 用来创建一个新的 schembuilder。然后,在调用 create() 创建模式之前,将一个或多个 字段 添加到构建器中。

添加到构建器的每个字段都有一个名称、类型和可选约束。

field(<name>, <type>, <optional constraints>)

使用 Fluent 推荐的默认值,有一个方便的 id() 方法可以添加 @ID 属性。

恢复迁移会撤消在 prepare 方法中所做的任何更改。在这种情况下,这意味着删除 Galaxy 的模式。

一旦定义了迁移,你必须将迁移添加到 configure.swift 中的 app.migrations 来告知 Fluent 。

app.migrations.add(CreateGalaxy())

Migrate

要运行迁移,在命令行中调用 swift run App migrate 或者添加 migrate 参数到 Xcode 的运行方案中。

$ swift run App 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

看看下面的路由,它将返回一个包含数据库中所有星系的数组。

app.get("galaxies") { req async throws in
    try await Galaxy.query(on: req.db).all()
}

为了在路由闭包中直接返回 Galaxy,添加遵循 Content 协议

final class Galaxy: Model, Content {
    ...
}

Galaxy.query 用于为模型创建新的查询构建器。req.db 是对你的应用程序的默认数据库的引用。 最后, all() 返回存储在数据库中的所有模型。

如果你编译并运行项目并请求 GET /galaxies, 你会看到返回一个空数组。 让我们添加一个创建新星系的路由。

Create

根据 RESTful 约定,使用 POST /galaxies 端点创建新星系。 由于模型是可编码的,因此你可以直接从请求正文中解码星系。

app.post("galaxies") { req -> EventLoopFuture<Galaxy> in
    let galaxy = try req.content.decode(Galaxy.self)
    return galaxy.create(on: req.db)
        .map { galaxy }
}

也可以看看

有关解码请求正文的更多信息,请参阅 内容 → 概述

一旦有了模型的实例,调用 create(on:) 就会将模型保存到数据库中。这将返回一个 EventLoopFuture<Void> 表示保存已完成的信号。保存完成后,使用 map 返回新创建的模型。

如果你正在使用 async/await 你可以这样编写代码:

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
}

在这种情况下,异步版本不会返回任何内容,但会在保存完成后返回。

构建并运行项目并发送以下请求。

POST /galaxies HTTP/1.1
content-length: 21
content-type: application/json

{
    "name": "Milky Way"
}

你应该以标识符作为响应来取回创建的模型。

{
    "id": ...,
    "name": "Milky Way"
}

现在,如果你 GET /galaxies 再次查询,你应该会看到数组中返回了新创建的星系。

Relations

没有恒星的星系算什么! 让我们通过在 Galaxy 和新的 Star 模型之间添加一对多关系来快速了解一下 Fluent 强大的关系特性。

final class Star: Model, Content {
    // 表或集合名。
    static let schema = "stars"

    // Star 唯一标识符。
    @ID(key: .id)
    var id: UUID?

    // Star 名称
    @Field(key: "name")
    var name: String

    // 引用这颗恒星所在的星系。
    @Parent(key: "galaxy_id")
    var galaxy: Galaxy

    // 创建一个空的 Star。
    init() { }

    // 创建一个新的 Star,设置所有属性。
    init(id: UUID? = nil, name: String, galaxyID: UUID) {
        self.id = id
        self.name = name
        self.$galaxy.id = galaxyID
    }
}

Parent

除了 @Parent 新字段类型,新的 Star 模型与 Galaxy 非常相似。

@Parent(key: "galaxy_id")
var galaxy: Galaxy

parent 属性是存储另一个模型标识符的字段。持有引用的模型称为,被引用的模型称为。这种类型的关系也称为“一对多”。该 key 参数指定了用于在数据库中存储父键的字段名称。

在 init 方法中,父标识符使用 $galaxy

self.$galaxy.id = galaxyID

通过为父属性的名称添加前缀 $,你可以访问底层属性包装器。这是访问 @Field 存储实际标识符值的内部所必需的。

也可以看看

查看 Swift Evolution 关于属性包装器的提案以获得更多信息:[SE-0258] Property Wrappers

接下来,创建一个迁移以准备数据库来处理 Star

struct CreateStar: AsyncMigration {
    // 为存储 Star 模型准备数据库。
    func prepare(on database: Database) async throws {
        try await database.schema("stars")
            .id()
            .field("name", .string)
            .field("galaxy_id", .uuid, .references("galaxies", "id"))
            .create()
    }

    // 可选地恢复 prepare 方法中所做的更改。
    func revert(on database: Database) async throws {
        try await database.schema("stars").delete()
    }
}

这与星系的迁移基本相同,除了存储父星系标识符的额外字段。

field("galaxy_id", .uuid, .references("galaxies", "id"))

该字段指定了一个可选的约束,告诉数据库该字段的值引用了 “galaxies” 模式中的字段 “id”。这也称为外键,有助于确保数据完整性。

一旦迁移创建完成,将其添加到 app.migrations 中的 CreateGalaxy之后。

app.migrations.add(CreateGalaxy())
app.migrations.add(CreateStar())

因为迁移是按顺序运行的,而且 CreateStar 引用了星系模式,所以顺序是很重要的。最后,运行迁移准备数据库。

添加创建新恒星的路由。

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请求创建一个引用之前创建的星系的新星。

POST /stars HTTP/1.1
content-length: 36
content-type: application/json

{
    "name": "Sun",
    "galaxy": {
        "id": ...
    }
}

你应该看到新创建的星星返回一个惟一标识符。

{
    "id": ...,
    "name": "Sun",
    "galaxy": {
        "id": ...
    }
}

Children

现在让我们看看如何利用 Fluent 的预加载功能自动返回 GET /galaxies 路由中的星系恒星。将以下属性添加到 Galaxy模型中。

// 这个星系的所有恒星。
@Children(for: \.$galaxy)
var stars: [Star]

@Children 属性包装器与 @Parent相反。它采用子 @Parent 字段的键路径作为 for参数。它的值是一个子模型数组,因为可能存在零个或多个子模型。星系的迁移不需要改变,因为这种关系所需的所有信息都存储在恒星上。

Eager Load

现在关系已经完成,你可以在查询构建器上使用 with 方法来自动获取和序列化星系-星型关系。

app.get("galaxies") { req in
    try await Galaxy.query(on: req.db).with(\.$stars).all()
}

一个指向 @Children 关系的键路径被传递给 with,告诉 Fluent 在所有结果模型中自动加载这个关系。创建并运行另一个请求到 GET /galaxies。你现在应该看到响应中自动包含星星。

[
    {
        "id": ...,
        "name": "Milky Way",
        "stars": [
            {
                "id": ...,
                "name": "Sun",
                "galaxy": {
                    "id": ...
                }
            }
        ]
    }
]

查询日志

Fluent 驱动程序在日志级别为 debug 时会记录生成的 SQL 语句。一些驱动程序,如 FluentPostgreSQL,允许在配置数据库时进行配置。

要设置日志级别,请在 configure.swift(或你设置应用程序的位置)文件中添加:

app.logger.logLevel = .debug

这会将日志级别设置为 debug。下次构建和运行应用程序时,Fluent 生成的 SQL 语句会在控制台输出。

下一步

祝贺你创建了第一个模型和迁移,并执行了基本的创建和读取操作。要了解更多关于所有这些特性的深入信息,请查看 Fluent 指南中的相应部分。