第7章:CRUD
数据库操作¶
第5章,"Fluent
与持久化模型",解释了模型的概念以及如何使用Fluent
将它们存储在数据库中。本章集中讨论如何与数据库中的模型进行交互。你将学习CRUD
操作以及它们与REST APIs
的关系。你还会看到如何利用Fluent
来对你的模型进行复杂的查询。
Note
本章要求你使用PostgreSQL。按照第5章,"Fluent
与持久化模型"的步骤,在Docker
中设置PostgreSQL
,并配置你的Vapor
应用程序。
CRUD
和REST
¶
CRUD
操作--创建、检索、更新、删除--构成了持久性存储的四个基本功能。通过这些,你可以执行你的应用程序所需的大部分操作。你在第5章中实际实现了第一个功能,即创建。
RESTful APIs
为客户提供了一种方法来调用你应用程序中的CRUD
功能。通常,你有一个用于你的模型的资源URL
。对于TIL
应用程序,这就是缩写的资源。http://localhost:8080/api/acronyms。然后你在这个资源上定义路由,与适当的HTTP
请求方法配对,以执行CRUD
操作。比如说。
GET
http://localhost:8080/api/acronyms/:获取所有的首字母缩写。POST
http://localhost:8080/api/acronyms:创建一个新的首字母缩写。GET
http://localhost:8080/api/acronyms/1:获得ID
为1
的首字母缩写。PUT
http://localhost:8080/api/acronyms/1:更新ID
为1
的首字母缩写词。DELETE
http://localhost:8080/api/acronyms/1:删除ID
为1
的首字母缩写。
创建-C
¶
在第5章"Fluent
与持久化模型"中,你实现了Acronym
的创建路线。你可以继续你的项目,或者打开本章的启动文件夹中的TILApp
。回顾一下,你在routes.swift
中创建了一个新的路由处理器:
// 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 { acronym }
}
这是它的作用:
- 在
/api/acronyms/
注册一个新的路由,接受POST
请求并返回EventLoopFuture<Acronym>
。 - 将请求的
JSON
解码成一个Acronym
。这很简单,因为Acronym
符合Content
的要求。 - 使用
Fluent
保存模型。当保存完成后,你在map(_:)
的完成处理程序中返回模型,这将返回一个EventLoopFuture
--在本例中是EventLoopFuture<Acronym>
。
构建并运行该应用程序,然后打开RESTed
。对请求进行如下配置:
- URL: http://localhost:8080/api/acronyms/
- method: POST
- Parameter encoding: JSON-encoded
添加两个带有名称和值的参数:
- short: OMG
- long: Oh My God
发送请求,你会看到包含创建的首字母缩写的响应:
检索-R
¶
对于TILApp
来说,检索包括两个独立的操作:检索所有的首字母缩写和检索一个单一的、特定的首字母缩写。Fluent
使这两个任务变得简单。
检索所有的首字母缩写词¶
要检索所有的首字母缩写,请创建一个GET
请求的路由处理器到/api/acronyms/
。打开routes.swift
,在routes(_:)
后面添加以下内容:
// 1
app.get("api", "acronyms") {
req -> EventLoopFuture<[Acronym]> in
// 2
Acronym.query(on: req.db).all()
}
这是它的作用:
- 注册一个新的路由处理程序,接受一个GET请求,返回
EventLoopFuture<[Acronym]>
,一个Acronym
的未来数组。 - 执行查询以获得所有的首字母缩写。
Fluent
为模型添加函数,以便能够对其进行查询。你必须给查询一个Database
。这几乎总是请求中的数据库,并为查询提供一个连接。all()
返回数据库中该类型的所有模型。这等同于SQL查询SELECT * FROM Acronyms;
。
建立并运行你的应用程序,然后在RESTed
中创建一个新的请求。配置该请求如下:
- URL: http://localhost:8080/api/acronyms/
- method: GET
发送请求,查看已经在数据库中的首字母缩写:
检索单个首字母缩写词¶
Vapor
的参数与Fluent
的查询功能集成,可以很容易地通过ID
获得首字母缩写。为了获得一个缩写,你需要一个新的路由处理程序。打开routes.swift
,在routes(_:)
后面添加以下内容:
// 1
app.get("api", "acronyms", ":acronymID") {
req -> EventLoopFuture<Acronym> in
// 2
Acronym.find(req.parameters.get("acronymID"), on: req.db)
// 3
.unwrap(or: Abort(.notFound))
}
这是它的作用:
- 在
/api/acronyms/<ID>
注册一个路由,以处理一个GET
请求。该路由将首字母缩写的id
属性作为最后的路径段。这将返回EventLoopFuture<Acronym>
。 - 获取以
acronymID
为名传入的参数。使用find(_:on:)
在数据库中查询具有该ID的Acronym
。
Note
因为find(_:on:)
需要一个UUID
作为第一个参数(因为Acronym
的id
类型是UUID
),get(_:)
推断返回类型为UUID
。默认情况下,它返回String
。你可以用get(_:as:)
指定类型。
find(_:on:)
返回EventLoopFuture<Acronym?>
,因为数据库中可能不存在该ID
的缩写。使用unwrap(or:)
来确保返回一个首字母缩写。如果没有找到首字母缩写,unwrap(or:)
会返回一个失败的未来,并提供错误。在这种情况下,它返回一个404 Not Found
错误。
建立并运行你的应用程序,然后在RESTed
中创建一个新的请求。配置该请求如下:
- URL: http://localhost:8080/api/acronyms/
(用你之前创建的首字母缩写的 ID
来替换这个ID
) - method: GET
发送请求,你会收到第一个缩写作为回应:
更新 - U
¶
在RESTful APIs
中,对单个资源的更新使用PUT请求,请求数据中包含新的信息。
在routes(_:)
的末尾添加以下内容来注册一个新的路由处理程序:
// 1
app.put("api", "acronyms", ":acronymID") {
req -> EventLoopFuture<Acronym> in
// 2
let updatedAcronym = try req.content.decode(Acronym.self)
return Acronym.find(
req.parameters.get("acronymID"),
on: req.db)
.unwrap(or: Abort(.notFound)).flatMap { acronym in
acronym.short = updatedAcronym.short
acronym.long = updatedAcronym.long
return acronym.save(on: req.db).map {
acronym
}
}
}
这是它的分解步骤:
- 注册一个路由,向
/api/acronyms/<ID>
发出PUT
请求,返回EventLoopFuture<Acronym>
。 - 对
Acronym
的请求体进行解码,以获得新的细节。 - 使用请求
URL
中的ID
获取首字母缩写。如果没有找到提供的ID
的首字母缩写,则使用unwrap(or:)
返回404 Not Found
。这将返回EventLoopFuture<Acronym>
,所以使用flatMap(_:)
来等待future
的完成。 - 用新的值更新首字母缩写的属性。
- 保存首字母缩写,用
map(_:)
等待它的完成。一旦保存完成,返回更新的首字母缩写。
建立并运行应用程序,然后使用RESTed
创建一个新的首字母缩写。配置请求如下:
- URL: http://localhost:8080/api/acronyms/
- method: POST
- Parameter encoding: JSON-encoded
添加两个带有名称和值的参数:
- short: WTF
- long: What The Flip
发送请求,你会看到包含创建的首字母缩写的响应:
事实证明,WTF
的意思其实不是What The Flip
,所以它需要更新。把RESTed
中的请求改成如下:
- URL: http://localhost:8080/api/acronyms/
Note: Use the ID from the returned create request.
Note
使用返回的创建请求中的ID
。
- method: PUT
- long: What The Fudge
发送请求。你将在响应中收到更新的缩写:
为了确保这一点,在RESTed
中发送一个请求,以获得所有的首字母缩写。你会看到更新后的首字母缩写返回:
删除 - D
¶
要在RESTful API
中删除一个模型,你要向该资源发送一个DELETE
请求。在routes(_:)
的末尾添加以下内容来创建一个新的路由处理程序:
// 1
app.delete("api", "acronyms", ":acronymID") {
req -> EventLoopFuture<HTTPStatus> in
// 2
Acronym.find(req.parameters.get("acronymID"), on: req.db)
.unwrap(or: Abort(.notFound))
// 3
.flatMap { acronym in
// 4
acronym.delete(on: req.db)
// 5
.transform(to: .noContent)
}
}
这是它的作用:
- 注册一个
DELETE
请求到/api/acronyms/<ID>
的路由,返回EventLoopFuture<HTTPStatus>
。 - 像以前一样从请求的参数中提取要删除的首字母缩写。
- 使用
flatMap(_:)
来等待从数据库返回的缩写。 - 使用
delete(on:)
删除该首字母缩写。 - 将结果转化为
204 No Content
响应。这告诉客户端请求已经成功完成,但没有内容可以返回。
建立并运行该应用程序。WTF
的缩写有点冒失,所以要删除它。在RESTed
中配置一个新的请求,如下所示:
Note
使用前一个请求中的WTF
缩写的ID
- method: DELETE
发送请求;你会收到一个204 No Content
的回应。
发送请求获得所有的缩写,你会看到WTF
的缩写已经不在数据库中。
Fluent
查询¶
你已经看到Fluent
使基本的CRUD
操作变得如此简单。它可以同样容易地执行更强大的查询。
过滤器¶
搜索功能是应用程序中的一个常见功能。如果你想搜索数据库中的所有首字母缩写,Fluent
使之变得简单。确保以下一行代码在routes.swift
的顶部:
import Fluent
接下来,在routes(_:)
的末尾添加一个新的路由处理程序用于搜索:
// 1
app.get("api", "acronyms", "search") {
req -> EventLoopFuture<[Acronym]> in
// 2
guard let searchTerm =
req.query[String.self, at: "term"] else {
throw Abort(.badRequest)
}
// 3
return Acronym.query(on: req.db)
.filter(\.$short == searchTerm)
.all()
}
下面是搜索缩略语的情况:
- 注册一个新的路由处理程序,接受
/api/acronyms/search
的GET
请求,并返回EventLoopFuture<[Acronym]>
。 - 从
URL
查询字符串中检索搜索词。如果失败,抛出一个400 Bad Request
错误。
Note
URL
中的查询字符串允许客户向服务器传递不适合在路径中出现的信息。例如,它们通常被用来定义搜索结果的页码。
- 使用
filter(_:)
找到所有short
属性与searchTerm
相匹配的缩写。因为这使用了关键路径,编译器可以对属性和过滤条件执行类型安全。这可以防止因指定无效的列名或无效的类型来过滤而引起的运行时问题。Fluent
使用属性包装器的预测值,而不是值本身。
在创建模型时,Fluent
大量使用了字段的属性封装器。正如Swift
文档中所描述的,"属性包装器在管理属性存储方式的代码和定义属性的代码之间增加了一层隔离"。你也可以为属性包装器提供一个预测值。这允许你在属性包装器上公开额外的功能。Fluent
使用投影值来提供对关系的键名和查询功能的访问。
在上面的例子中,你提供了属性包装器的预测值来进行过滤,而不是值本身。投射值为Fluent
提供了它所需要的来自属性封装器的信息。例如,Fluent
在执行过滤器的查询时需要列名。如果你只提供属性,Fluent
将没有办法访问这些数据。在接下来的章节中,你会学到更多关于使用属性封装器的知识。
如果你需要一个属性的实际值,你可以使用该属性本身。例如,要读取一个缩写的简短版本,你只需使用acronym.short
。在大多数情况下,这很好。然而在某些情况下,这个属性可能没有一个值。你可能想引用一个还没有从数据库中加载的关系。或者,你可能已经加载了记录,但只检索了选定的字段。你会在第9章"父子关系"、第10章"兄弟姐妹关系"和第31章"高级Fluent
"中了解到这些不同的用例。
建立并运行你的应用程序,然后在RESTed
中创建一个新的请求。配置该请求如下:
- URL: http://localhost:8080/api/acronyms/search?term=OMG
- method: GET
发送请求,你会看到返回的OMG
首字母缩写及其含义:
如果你想搜索多个字段,例如短字段和长字段,你需要改变你的查询。你不能用链式的filter(_:)
函数,因为那只能匹配那些short
和long
属性相同的首字母词。
相反,你必须使用一个filter group
。替换:
return Acronym.query(on: req.db)
.filter(\.$short == searchTerm)
.all()
为以下代码:
// 1
return Acronym.query(on: req.db).group(.or) { or in
// 2
or.filter(\.$short == searchTerm)
// 3
or.filter(\.$long == searchTerm)
// 4
}.all()
以下是这个额外代码的作用:
- 使用
.or
关系创建一个过滤器组。 - 为该组添加一个过滤器,以过滤那些
short
属性与搜索词相匹配的首字母缩写。 - 在该组中添加一个过滤器,以过滤
long
属性符合搜索条件的首字母缩写。 - 返回所有的结果。
这将返回所有符合第一个过滤器或第二个过滤器的缩略语。建立并运行应用程序,然后回到RESTed
。重新发送上面的请求,你仍然会看到相同的结果。
将URL
改为http://localhost:8080/api/acronyms/search?term=Oh+My+God并发送请求。你会得到OMG
的缩写作为响应:
Note
URL
中的空格必须以%20
或+
作为URL
编码才有效。
第一个结果¶
有时一个应用程序只需要一个查询的第一个结果。为此创建一个特定的处理程序,以确保数据库只返回一个结果,而不是将所有结果加载到内存中。创建一个新的路由处理程序来返回routes(_:)
结尾处的第一个首字母缩写词:
// 1
app.get("api", "acronyms", "first") {
req -> EventLoopFuture<Acronym> in
// 2
Acronym.query(on: req.db)
.first()
.unwrap(or: Abort(.notFound))
}
这是它的作用:
- 为
/api/acronyms/first
注册一个新的HTTP GET
路由,返回EventLoopFuture<Acronym>
。 - 执行查询以获得第一个首字母缩写词。
first()
返回一个可选项,因为数据库中可能没有首字母缩写词。使用unwrap(or:)
来确保首字母缩写存在,或者抛出一个404 Not Found
错误。
你也可以将.first()
应用于任何查询,如过滤器的结果。
建立并运行应用程序,然后打开RESTed
。创建新的首字母缩写与:
- short: IKR
- long: I Know Right
现在创建一个新的RESTed
请求,配置为:
- URL: http://localhost:8080/api/acronyms/first
- method: GET
发送请求,你会看到你创建的第一个首字母缩写返回:
对结果进行排序¶
应用程序通常需要在返回查询结果之前对其进行排序。出于这个原因,Fluent
提供了一个排序函数。
在routes(_:)
函数的末尾写一个新的路由处理程序,以返回所有的首字母缩写,按其short
属性升序排序:
// 1
app.get("api", "acronyms", "sorted") {
req -> EventLoopFuture<[Acronym]> in
// 2
Acronym.query(on: req.db)
.sort(\.$short, .ascending)
.all()
}
以下是其工作方式:`
-
为
/api/acronyms/sorted
注册一个新的HTTP GET
路由,返回`EventLoopFuture<[Acronym]>'。 -
为
Acronym
创建一个查询,并使用sort(_:_:)
来执行排序。这个函数接收属性包装器对该字段的预测值的关键路径来进行排序。它还接受排序的方向。最后使用all()
来返回所有的查询结果。
建立并运行应用程序,然后在RESTed
中创建一个新的请求:
- URL: http://localhost:8080/api/acronyms/sorted
- method: GET
发送请求,你会看到按字母顺序排列的缩写词的short
字属性:
接下来去哪?¶
你现在知道如何使用Fluent
来执行不同的CRUD
操作和高级查询。在这个阶段,routes.swift
已经被本章的所有代码弄得杂乱无章。下一章将探讨如何使用控制器来更好地组织你的代码。