跳转至

第5章:Fluent与持久化模型

在第2章"你好,Vapor!"中,你学到了创建Vapor应用程序的基础知识,包括如何创建路由。本章介绍了如何使用Fluent来保存Vapor应用程序中的数据。你需要安装并运行Docker。访问https://www.docker.com/get-docker,按照说明进行安装。

Fluent

FluentVaporORM或对象关系映射工具。它是Vapor应用程序和数据库之间的一个抽象层,其目的是使数据库的工作更容易。使用像Fluent这样的ORM有很多好处。

最大的好处是你不需要直接使用数据库 当你直接与数据库交互时,你把数据库查询写成字符串。这些都不是类型安全的,在Swift中使用会很痛苦。

Fluent让你受益匪浅,它允许你使用许多数据库引擎中的任何一个,甚至在同一个应用程序中。最后,你不需要知道如何写查询,因为你可以用Swifty的方式与你的模型互动。

模型是你的数据的Swift表示,在整个Fluent中使用。模型是你在数据库中保存和访问的对象,例如用户资料。在与数据库交互时,Fluent返回并使用类型安全的模型,给你带来编译时的安全。

缩略语

在接下来的几章中,你将建立一个复杂的"今天我学到了"的应用程序,可以保存不同的首字母缩写和它们的含义。首先创建一个新的项目,使用Vapor工具箱。在终端,输入以下命令:

cd ~/vapor

这条命令将你带入你的主目录中一个叫做vapor的目录,并假定你完成了第2章"你好,Vapor"中的步骤。接下来,输入:

vapor new TILApp

当问到你是否愿意使用Fluent时,输入y,然后按Enter。接下来输入1,选择PostgreSQL作为数据库,接着按Enter。当工具箱询问你是否要使用Leaf或其他依赖项时,输入n,然后按Enter。这将创建一个新的Vapor项目,名为TILApp,使用该模板并配置PostgreSQL作为数据库。

img

本书中的TIL应用一直使用PostgreSQL。然而,它应该不需要任何修改就能与Fluent支持的任何数据库一起工作。你将在第6章"配置数据库"中学习如何配置不同的数据库。

模板提供了模型、迁移和控制器的示例文件。你将建立你自己的,所以请删除这些例子。在终端,输入:

cd TILApp
rm -rf Sources/App/Models/*
rm -rf Sources/App/Migrations/*
rm -rf Sources/App/Controllers/*

如果被提示确认删除,请输入y。现在,在Xcode中打开该项目:

open Package.swift

这是从你的Swift包中创建一个Xcode项目,使用XcodeSwift包管理器的支持。它需要一段时间来下载所有的依赖项,这是第一次。当它完成后,你会看到侧边栏中的依赖项和一个TILApp方案可用:

img

首先,打开configure.swift,删除以下一行:

app.migrations.add(CreateTodo())

接下来,打开routes.swift,删除以下一行:

try app.register(collection: TodoController())

这就删除了对模板的示例模型迁移和控制器的剩余引用。

Sources/App/Models创建一个新的Swift文件,名为Acronym.swift。在这个新文件中,插入以下内容:

import Vapor
import Fluent

// 1
final class Acronym: Model {
  // 2
  static let schema = "acronyms"

  // 3
  @ID
  var id: UUID?

  // 4
  @Field(key: "short")
  var short: String

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

  // 5
  init() {}

  // 6
  init(id: UUID? = nil, short: String, long: String) {
    self.id = id
    self.short = short
    self.long = long
  }
}

下面是这段代码的作用:

  1. 定义一个符合Model的类。
  2. 按照Model的要求指定schema。这是数据库中的表的名称。
  3. 定义一个可选的id属性,用于存储模型的ID,如果已经设置了一个ID。这是用Fluent@ID属性包装器来注释的。这将告诉Fluent用什么在数据库中查找该模型。
  4. 定义两个String属性来保存缩写和它的定义。这两个属性使用@Field属性包装器来表示一个通用的数据库字段。key参数是数据库中列的名称。
  5. 按照Model的要求,提供一个空的初始化器。Fluent使用它来初始化从数据库查询返回的模型。
  6. 提供一个初始化器,按要求创建模型。

如果你是从Fluent 3来的,这个模型看起来非常不同。Fluent 4利用属性包装器来提供强大而复杂的数据库集成。@ID将一个属性标记为该表的IDFluent在寻找模型时使用这个属性包装器在数据库中进行查询。该属性包装器也用于关系,你将在接下来的章节中了解到这一点。在Fluent中,默认情况下,ID必须是一个UUID,并称为id

@Field将模型的属性标记为数据库中的一个通用列。Fluent使用属性包装器来执行带有过滤器的查询。使用属性包装器允许Fluent更新模型中的单个字段,而不是整个模型。你也可以从数据库中选择指定的字段,而不是一个模型的所有字段。请注意,你应该只对非可选的属性使用@Field。如果你的模型中有一个可选的属性,你应该使用@OptionalField

为了在数据库中保存模型,你必须为它创建一个表。Fluent通过migration来实现这一目标。迁移允许你对你的数据库进行可靠的、可测试的、可重复的改变。它们通常被用来为你的模型创建一个数据库模式,或表描述。它们也被用来向数据库中输入数据,或者在模型被保存后对其进行修改。

Fluent 3可以为你推断出很多表的信息。然而,这并没有扩展到大型复杂的项目,特别是当你需要添加或删除列,甚至重命名它们时。在Xcode中,在Sources/App/Migrations创建一个新的Swift文件,名为CreateAcronym.swift

在这个新文件中插入以下内容:

import Fluent

// 1
struct CreateAcronym: Migration {
  // 2
  func prepare(on database: Database) -> EventLoopFuture<Void> {
    // 3
    database.schema("acronyms")
      // 4
      .id()
      // 5
      .field("short", .string, .required)
      .field("long", .string, .required)
      // 6
      .create()
  }

  // 7
  func revert(on database: Database) -> EventLoopFuture<Void> {
    database.schema("acronyms").delete()
  }
}

以下是迁移工作的情况:

  1. 定义一个新的类型,CreateAcronym,符合Migration
  2. 按照Migration的要求实现prepare(on:)。当你运行你的迁移时,你会调用这个方法。
  3. 定义这个模型的表名。这必须与模型中的`schema'相匹配。
  4. 定义数据库中的ID列。
  5. 定义shortlong的列。将列的类型设置为string,并将列标记为必填。这与模型中的非选择的String属性相匹配。字段名必须与属性包装器的键匹配,不是属性本身的名称。
  6. 在数据库中创建表。
  7. 按照Migration的要求实现revert(on:)。当你恢复你的迁移时,你将调用这个函数。这将删除用schema(_:)引用的表。

所有对列名和表名的引用都是字符串。这是故意的,因为如果这些属性名称在未来发生变化,使用属性会导致问题。第35章,"生产问题与Redis"描述了一种改进的解决方案,并使其类型安全。

迁移只运行一次;一旦它们在数据库中运行,就不会再被执行。记住这一点很重要,因为如果你改变了迁移,Fluent不会试图重新创建一个表。

现在你有了Acronym的迁移,你可以告诉Fluent创建这个表。打开configure.swift,在app.databases.use(_:as:)之后,添加以下内容:

// 1
app.migrations.add(CreateAcronym())

// 2
app.logger.logLevel = .debug

// 3
try app.autoMigrate().wait()

以下是你的新代码的作用:

  1. CreateAcronym添加到要运行的迁移列表中。
  2. 将应用程序的日志级别设置为debug。这提供了更多的信息,使你能够看到你的迁移。
  3. 自动运行迁移并等待结果。Fluent允许你选择何时运行你的迁移。例如,当你需要安排它们时,这很有帮助。你可以在这里使用wait(),因为你不是在一个EventLoop上运行。

为了测试PostgreSQL,你将在一个Docker容器中运行Postgres服务器。打开Terminal并输入以下命令:

docker run --name postgres -e POSTGRES_DB=vapor_database \
  -e POSTGRES_USER=vapor_username \
  -e POSTGRES_PASSWORD=vapor_password \
  -p 5432:5432 -d postgres

以下是这样做的:

  • 运行一个名为postgres的新容器。
  • 通过环境变量指定数据库名称、用户名和密码。
  • 允许应用程序连接到Postgres服务器的默认端口:5432.
  • 在后台作为一个守护程序运行服务器。
  • 为这个容器使用名为postgresDocker镜像。如果你的机器上没有这个镜像,Docker会自动下载它。

要检查你的数据库是否在运行,在终端输入以下内容,以列出所有活动的容器:

docker ps

img

现在你已经准备好运行这个应用程序了! 将活动方案设置为TILApp,将My Mac作为目标。建立并运行。检查控制台,看看迁移是否已经运行。

你应该看到与下面类似的东西:

img

保存模型

当你的应用程序的用户输入一个新的缩写时,你需要一种方法来保存它。

Vapor 4中,Codable使之变得微不足道。Vapor提供了Content,一个围绕Codable的封装器,它允许你在各种格式之间转换模型和其他数据。

这在Vapor中被广泛使用,你将在本书中看到它。

打开Acronym.swift,在文件末尾添加以下内容,使Acronym符合Content

extension Acronym: Content {}

由于Acronym已经通过Model符合Codable,你不必再添加其他东西。为了创建一个缩写,用户的浏览器会发送一个POST请求,其中包含一个JSON有效载荷,看起来类似于以下内容:

{
  "short": "OMG",
  "long": "Oh My God"
}

你需要一个路由来处理这个POST请求并保存新的首字母缩写。打开routes.swift,在routes(_:)的末尾添加以下内容:

// 1
app.post("api", "acronyms") { req -> EventLoopFuture<Acronym> in
  // 2
  let acronym = try req.content.decode(Acronym.self)
  // 3
  return acronym.save(on: req.db).map { 
    // 4
    acronym 
  }
}

这是它的作用:

  1. /api/acronyms注册一个新的路由,接受一个POST请求并返回EventLoopFuture<Acronym>。一旦保存好,它就会返回首字母缩写。
  2. 使用Codable将请求的JSON解码为Acronym模型。
  3. 使用Fluent和数据库从Request中保存该模型。
  4. save(on:)返回EventLoopFuture<Void>,所以使用map在保存完成后返回首字母。

FluentVaporCodable的综合使用使之变得简单。由于Acronym符合Content,它很容易在JSONModel之间转换。

这允许Vapor在响应中把模型作为JSON返回,而不需要你做任何努力。构建并运行应用程序以进行尝试。

测试的好工具是RESTed,可以从Mac App Store免费下载。其他工具,如PawPostman也很适合。

RESTed中,按以下方式配置请求:

  • URL: http://localhost:8080/api/acronyms
  • method: POST
  • Parameter encoding: JSON-encoded

添加两个带有名称和值的参数:

  • short: OMG
  • long: Oh My God

将参数编码设置为JSON-encoded可以确保数据以JSON形式发送。

值得注意的是,这也将Content-Type头设置为application/json,这告诉Vapor请求包含JSON。如果你使用一个不同的客户端来发送请求,你可能需要手动设置。

点击Send Request,你会看到响应中提供的缩写。

id字段将有一个值,因为它现在已经被保存在数据库中:

img

接下来去哪?

本章向你介绍了Fluent以及如何在Vapor中创建模型并将其保存在数据库中。接下来的章节将在这个应用程序的基础上创建一个全功能的TIL应用程序。