第5章:Fluent
与持久化模型¶
在第2章"你好,Vapor
!"中,你学到了创建Vapor
应用程序的基础知识,包括如何创建路由。本章介绍了如何使用Fluent
来保存Vapor
应用程序中的数据。你需要安装并运行Docker
。访问https://www.docker.com/get-docker,按照说明进行安装。
Fluent
¶
Fluent
是Vapor
的ORM
或对象关系映射工具。它是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
作为数据库。
本书中的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
项目,使用Xcode
对Swift
包管理器的支持。它需要一段时间来下载所有的依赖项,这是第一次。当它完成后,你会看到侧边栏中的依赖项和一个TILApp
方案可用:
首先,打开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
}
}
下面是这段代码的作用:
- 定义一个符合
Model
的类。 - 按照
Model
的要求指定schema
。这是数据库中的表的名称。 - 定义一个可选的
id
属性,用于存储模型的ID
,如果已经设置了一个ID
。这是用Fluent
的@ID
属性包装器来注释的。这将告诉Fluent
用什么在数据库中查找该模型。 - 定义两个
String
属性来保存缩写和它的定义。这两个属性使用@Field
属性包装器来表示一个通用的数据库字段。key
参数是数据库中列的名称。 - 按照
Model
的要求,提供一个空的初始化器。Fluent
使用它来初始化从数据库查询返回的模型。 - 提供一个初始化器,按要求创建模型。
如果你是从Fluent 3
来的,这个模型看起来非常不同。Fluent 4
利用属性包装器来提供强大而复杂的数据库集成。@ID
将一个属性标记为该表的ID
。Fluent
在寻找模型时使用这个属性包装器在数据库中进行查询。该属性包装器也用于关系,你将在接下来的章节中了解到这一点。在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()
}
}
以下是迁移工作的情况:
- 定义一个新的类型,
CreateAcronym
,符合Migration
。 - 按照
Migration
的要求实现prepare(on:)
。当你运行你的迁移时,你会调用这个方法。 - 定义这个模型的表名。这必须与模型中的`schema'相匹配。
- 定义数据库中的ID列。
- 定义
short
和long
的列。将列的类型设置为string
,并将列标记为必填。这与模型中的非选择的String
属性相匹配。字段名必须与属性包装器的键匹配,不是属性本身的名称。 - 在数据库中创建表。
- 按照
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()
以下是你的新代码的作用:
- 将
CreateAcronym
添加到要运行的迁移列表中。 - 将应用程序的日志级别设置为
debug
。这提供了更多的信息,使你能够看到你的迁移。 - 自动运行迁移并等待结果。
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
. - 在后台作为一个守护程序运行服务器。
- 为这个容器使用名为
postgres
的Docker
镜像。如果你的机器上没有这个镜像,Docker
会自动下载它。
要检查你的数据库是否在运行,在终端输入以下内容,以列出所有活动的容器:
docker ps
现在你已经准备好运行这个应用程序了! 将活动方案设置为TILApp
,将My Mac
作为目标。建立并运行。检查控制台,看看迁移是否已经运行。
你应该看到与下面类似的东西:
保存模型¶
当你的应用程序的用户输入一个新的缩写时,你需要一种方法来保存它。
在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
}
}
这是它的作用:
- 在
/api/acronyms
注册一个新的路由,接受一个POST
请求并返回EventLoopFuture<Acronym>
。一旦保存好,它就会返回首字母缩写。 - 使用
Codable
将请求的JSON
解码为Acronym
模型。 - 使用
Fluent
和数据库从Request
中保存该模型。 save(on:)
返回EventLoopFuture<Void>
,所以使用map
在保存完成后返回首字母。
Fluent
和Vapor
对Codable
的综合使用使之变得简单。由于Acronym
符合Content
,它很容易在JSON
和Model
之间转换。
这允许Vapor
在响应中把模型作为JSON
返回,而不需要你做任何努力。构建并运行应用程序以进行尝试。
测试的好工具是RESTed
,可以从Mac App Store
免费下载。其他工具,如Paw
和Postman
也很适合。
在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
字段将有一个值,因为它现在已经被保存在数据库中:
接下来去哪?¶
本章向你介绍了Fluent
以及如何在Vapor
中创建模型并将其保存在数据库中。接下来的章节将在这个应用程序的基础上创建一个全功能的TIL
应用程序。