Vapor 教程系列 - 02 实现基本 CRUD 操作与控制器封装
👋 我们继续构建 Web API,本文将一起了解 CRUD 的数据库操作与 RESTful API 结合,并且学习几个 Fluent 检索方法,最后将我们写的接口使用 Controller 的方式封装起来。🚀
- 第一部分:构建 Web API
- 创建项目和设置数据库与路由
- 实现基本 CRUD 操作与控制器封装
- 进阶数据库模型与 API 实现
- Web API 测试与稳定性验证
- 异步编程与全面错误处理
- 第二部分:制作简单的前端网页应用
- 第三部分:数据校验、用户验证和授权
- 第四部分:Vapor 的进阶使用
- 第五部分:生产部署
CRUD 操作和 RESTful 接口
CRUD 包括 Create(创建)、Retrieve(获取)、Update(更新)、Delete(删除)操作,这是数据库持久化存储的常用功能。上一部分中我们已经尝试过Create。
RESTful APIs 在我们的应用中为客户端提供了一种调用 CRUD 操作的比较不错的方式,这是一种接口规范。通常情况下,需要通过一个 URL 触及我们的数据库模型数据,对于我们的 TILApp 来说,我们就是通过 http://127.0.0.1:8080/api/acronyms
这个 URL 获取资源的。对于这个 URL 我们需要定义一些路由,匹配相应的 HTTP 的请求方法实现 CRUD 操作,如下:
Create创建数据
在《Vapor - 构建 Web API - 1. 新建工程和使用数据库》一文中我们曾经注册了一个 POST 路由,这里再重新回顾一下。当时我们在 Sources/App/routes.swift
中添加了以下代码:
代码注释:
- 解析请求中的 json 数据,生成 acronym 模型数据;
- 尝试执行模型数据的 save 方法保存数据到数据库,期间会自动生成 id,最后返回传入的数据。
vscode 打开工程的时候就会自动生成调试运行的配置,可以通过 vscode 的调试运行应用:
- 点击侧边栏调试按钮;
- 点击调试按钮;
- 成功运行后会显示如图位置 3 的工具栏;
- 应用运行在 http://127.0.0.1:8080
下面我们打开 RapidAPI Client 扩展,测试我们在 http://127.0.0.1:8080/api/acronyms 上注册的 POST 路由。
- URL:
http://127.0.0.1:8080/api/acronyms
- Method:
POST
- short:
OMG
- long:
oh my god
正常的话就能看到右侧显示响应 200 OK和具体响应时间,同时能看到响应数据为生成的数据库数据,包含了 id:
后面我都会用 RapidAPI Client 测试写的路由,具体使用教程可以参考:A deep dive into RapidAPI Client for VS Code。
💡 提示
当然你可以使用自己常用的 API 测试工具,比如 Postman、Insomnia(开源免费)等,看你个人习惯使用,选择自己常用的工具即可。
Retrieve获取数据
获取数据分为两种:获取所有字母缩写数据h和获取指定 ID 的字母缩写数据,他们遵循 RESTful API 规范,一个获取的是列表一个获取的是单个数据。
获取所有字母缩写数据
要获取所有数据很简单,需要在 URLhttp://127.0.0.1:8080/api/acronyms
下注册 GET 路由,打开文件 Sources/App/routes.swift
在 routes 方法最后添加以下代码:
代码注释:
- 返回项是 Acronym 对象列表;
- 调用 all 方法即可获取 query 对象中所有的数据,相当于在 SQL 数据库中执行了 SQL 语句
SELECT * FROM acronyms
。
按照上面 Create 创建数据 的方法运行程序,使用 API 测试工具测试路由:
- URL:
http://127.0.0.1:8080/api/acronyms
- Method:
GET
可以看到能够获取到我们创建的数据,是一个列表的形式。
获取指定ID数据
获取单条数据稍微有一点不同是,多了一步解析 ID 的过程,需要在 URLhttp://127.0.0.1:8080/api/acronyms/:acronymID
下注册 GET 路由,打开文件 Sources/App/routes.swift
在 routes 方法最后添加以下代码:
- 路由参数中增加了
:acronymID
,加上冒号表是这是一个动态参数,这相当于一个占位值,用于请求处理中解析此处的值; - 首先解析请求参数中
acronymID 的值,作为 ID 用于 find/ 方法检索 id.
notFound 异常; - 如果找到数据就返回这条数据。
编译运行应用,测试中使用的 HTTP 方法依旧是 GET 方法,但是 URL 中加了 ID 数据,这里的 ID 使用的是 Get All
获得的数据中的第一条数据的 ID,配置好后点击发送测试这个新增的路由:
- URL:
http://127.0.0.1:8080/api/acronyms/<ID>
- Method:
GET
Update更新数据
在 RESTful APIs 中,使用 HTTP 请求的 PUT 方法实现单条数据的更新,使用 URL 与获取单条数据的一致,对应请求需要通过 json 格式把要更新的数据发送到应用,应用通过解析路由中的 ID 检索后将请求中的数据更新的检索出的 Acronym 数据中,具体打开文件 Sources/App/routes.swift
的 routes 方法最后添加以下代码:
代码注释:
- 反序列化请求体的 json 数据得到要更新的 Acronym 数据;
- 路由参数中获取 ID,根据 ID 值检索数据库中对应 ID 的数据。如果找不到数据就返回
.notFound
表示 404; - 将反序列化的数据更新到检索出来的 Acronym 对象中;
- 最后保存数据到数据库中,并把更新后的数据返回;
编译运行我们的应用,然后使用工具测试我们新加的更新路由,这里更新的数据如下:
将 json 数据设置到 API 测试工具的请求体中,配置好对应的 HTTP 方法和 URL,其中 URL 中的 ID 使用的是 Get All
得到的数据中第一条数据的 ID,所以会将唯一的数据更新成以上的 json 数据:
- URL:
http://127.0.0.1:8080/api/acronyms
- Method:
PUT
- short:
WTF
- long:
what the flip
DELETE删除数据
与获取单条数据使用一样的 URL,不过需要使用 DELETE 的请求方法,在文件 routes.swift
的 routes 方法最后加上以下代码:
代码注释:
- 同样的解析路由中的 ID,根据 ID 检索对应数据,如果检索不到就返回
.notFound
响应; - 如果检索到数据,尝试调用
delete()
方法删除数据,最后返回.noContent
的 204 响应;
编译运行应用,使用 API 测试工具测试新增的删除路由:
- URL:
http://127.0.0.1:8080/api/acronyms/<ID>
- Method:
DELETE
至此,我们就完成了 Acronym 数据的 CRUD 操作,下一小节中我们尝试基本的 Query 操作。
Query检索操作
上一小节中使用 Fluent 库很轻松的完成了数据 CRUD 操作,同样的使用 Fluent 我们也会很容事实现各种检索操作,更多检索操作可以参考Fluent查询。
filter操作
搜索是一个 web 应用中比较常见的功能了,所以我们先尝试添加一个搜索的路由,搜索的关键词通过 URL query 字符串实现获取,这里的话我们设置搜索关键词对应 URL query 字符串的 term 字段,最终搜索 short 包含 term 对应值或者 long 包含 term 对应值的 Acronym 数据。
具体先看实现代码吧,依旧在 Sources/App/routes.swift
的 routes
方法中添加路由:
代码注释:
- 从 URL 中解析 term 字段 query 值,如果解析不到就抛出
.badRequest
错误; - 这里使用了
group
组合,使用或的关系,在闭包中使用filter
方法过滤数据,其中~~
表示包含子字符串,最后调all
方法得到所有数据。
💡 在Flent查询 中对Group
、filter
都有相应的描述,所以比较建议结合官方文档内容进行实践。
使用 Fluent 定义 model 数据时,为 field 对应的属性使用了很多装饰器,下面是 swift 官方文档中对装饰器的描述:
“a property wrapper adds a layer of separation between code that manages how a property is stored and the code that defines a property”
一个属性的装饰器添加了一个隔离层,做个隔离层用来区分管理属性存储代码和定义属性的代码。在装饰器上可以指定相应数据库中字段名称、类型等信息,这样就将模型数据的属性与数据库的字段关联起来用于检索。
在上面的代码中我们使用 \.$short
使用了 $
符号,这个取得值称作projectedValue,通过这个值可以获取检索用到的字段名称,如果我们直接获取 short 的值不加 $
使用 .short
这就是 propertyValue,在后面我们会大量用到这种用法,后面可能理解起来就更轻松了。
我们编译应用,打开 API 测试工具测试我们新增的路由,这里我们指定检索关键词为 o:
- URL:
http://127.0.0.1:8080/api/acronyms/search?term=o
- Method:
GET
First操作
很多应用中会需要获取检索结果中的第一个数据,我们就实现一下,有了前面的实现,这个就很简单了,在 Sources/App/routes.swift
文件的 routes
方法中添加相应的路由如下:
代码注释:
- 调用
first
方法从 query 结果中获取第一条数据; - 如果找不到数据就抛出
.notFound
错误。
编译运行应用,我们先使用 API 测试工具调用 creat 接口创建以下数据:
然后我们测试 first 路由:
- URL:
http://127.0.0.1:8080/api/acronyms/first
- Method:
GET
sort操作
通常 web 应用中都会用到数据排序,Fluent 同样提供了相应的操作方法,在 routes
方法中添加以下代码,实现获取排好序的数据,这里按照 short 名称排序:
- 使用
sort
方法进行排序,可以指定具体模型属性的 projectedValue 和顺序,这里选择的是对short
进行升序排序; - 使用
all
方法获取排好序的所有数据。
编译运行程序,我们使用 API 测试工具测试新加的排序数据路由:
- URL:
http://127.0.0.1:8080/api/acronyms/sorted
- Method:
GET
至此我们学习了常用的 CRUD 和检索相关的路由注册和 Fluent 方法的使用,下一部分我们学习如何使用 controller 组织我们已经编写的代码。
Controller 封装接口
Controller 简单介绍
Vapor 中的 Controller 类似于 iOS 开发中的 Controller。在 Vapor 中 Controller 用与客户端进行交互处理,比如接收请求、进行相应处理后并返回响应数据。Controller 提供了更好组织我们代码的方式,处理 Model 相应交互处理就是一个很好的例子。在 TILApp 中我们可以使用 Controller 封装我们 CRUD 和检索的所有操作。
Controller 还能很好的处理我们应用的代码,比如可以使用一个 Controller 管理我们旧版本的接口而另一个 Controller 管理我们新版本的接口。这使不同功用的代码有了很清晰的分离,方便我们进行维护。
开始使用Controller
在 Sources/App/Controller
中创建名为 AcronymsController.swift
的文件。
路由集(Route Collections)
在 Controller 中,我们会定义很多 Handler,为了能访问相应的路由,我们会将这些 Handler 注册到相应的路由上。比较简单的方式就是直接调用 Controller 中的函数,如下:
上面的代码调用的是 acronymsController 中的 index 方法,这与 CRUD 操作中的方式不同的是这里最后一个参数是一个函数,而之前的代码是传入的闭包。
这种方式可能在比较小的应用中没有问题,但是随着 routes.swift 这种路由原来越多,会变得不可维护。比较好的实现就是使用 Controller 管理相应要注册的路由,Vapor 提供路由集(RouteCollection)实现这一点。
打开上面创建的文件 AcronymsController.swift
,创建一个 AcronymsController
遵循 RouteCollection
协议,这个协议必须实现 boot
方法,如下:
在 boot
方法后面实现 index
方法用于实现获取所有数据的处理,如下:
然后在 boot
方法中添加路由注册:
这样我们就在 AcronymsController 中注册好了获取所有数据的路由,但是这与我们之前在 Sources/App/routes.swift
中注册的路由重复了,需要打开 routes.swift
删除原来的代码即:
我们需要告诉 app 我们新加的 Controller 路由处理,所以还需要在 Sources/App/routes.swift
的 routes
方法中加入以下代码:
代码注释:
- 创建 AcronymsController
- 将新建的 Controller 注册到 app 的路由上
编译运行应用,然后使用 API 测试工具测试更换的获取所有缩写数据的 API。
路由组(Route Group)
在之前的 REST 所有关于 acronyms 的 REST 接口对应的 URL 都是 /api/acronyms
开头的,如果我们修改这个初始路由端点,需要改很多地方,Vapor 提供了 route group 进行路由端点分组,解决这个问题,使代码更清晰。
打开 AcronymsController.swift
文件的 boot
方法内部开头添加代码:
然后更改之前添加的代码 routes.get("api", "acronyms", use: index)
为 acronymsRoutes.get(use: index)
。
这样我们就实现了同样的功能,但代码更加清晰易于维护。
下面我们按照 getAllHandler 的方式将 routes.swift
文件模版添加的路由删掉,只剩 acronymsController 相关代码即可,之前添加的所有路由处理都删除:
app.post("api", "acronyms")
app.get("api", "acronyms", ":acronymID")
app.put("api", "acronyms", ":acronymID")
app.delete("api", "acronyms", ":acronymID")
app.get("api", "acronyms", "search")
app.get("api", "acronyms", "first")
app.get("api", "acronyms", "sorted")
并将相应的代码迁移到 AcronymsController.swift
的 AcronymsController
中改造为相应的方法如下:
代码注释(参照官网 Controller 示例代码的命名方式对某些方法修改了命名):
- getAllHandler 为获取所有的数据;
- createHandler 为创建新的数据;
- getHandler 为获取单条数据;
- updateHandler 为更新单条数据;
- deleteHandler 为删除单条数据;
- searchHandler 为搜索数据;
- firstHandler为获取检索结果的首条数据;
- sortedHandler 为获取排序数据。
然后在 boot 方法中的 acronymsRoutes
上注册相应的路由,最后的代码如下:
代码注释:
- 在 URL /api/acronyms 注册 GET 方法路由使用 getAllHandler 方法;
- 在 URL /api/acronyms 注册 POST 方法路由使用 createHandler 方法;
- 在 URL /api/acronyms/<acronymID> 注册 GET 方法路由使用 getHandler 方法;
- 在 URL /api/acronyms/<acronymID> 注册 PUT 方法路由使用 updateHandler 方法;
- 在 URL /api/acronyms/<acronymID> 注册 DELETE 方法路由使用 deleteHandler 方法;
- 在 URL /api/acronyms/search 注册 GET 方法路由使用 searchHandler 方法;
- 在 URL /api/acronyms/first 注册 GET 方法路由使用 firstHandler 方法;
- 在 URL /api/acronyms/sorted 注册 GET 方法路由使用 sortedHandler方法;
编译运行应用,使用之前 API 使用的接口测试所有的接口,不出意外,均可使用。
至此我们大概学会了 Controller 封装接口的基本使用方法,那本文就先写这些吧,下一篇再见!
附件
本文最终代码:s1-2。