构建 Web API 3 - 走关系?数据库模型与API进阶实现
Vapor Web 开发

构建 Web API 3 - 走关系?数据库模型与API进阶实现

⌛《CRUD、检索操作与 Controller 接口封装》中我们一起了解了 CRUD 的数据库操作,并学习了如何使用 Fluent 框架和 Controller 的封装方式实现基本的 API✨。本文就跟着我开始“走关系”吧🫣,会了模型关系再复杂的 API 需求也能游刃有余解决,搞掂它!

92次点击26分钟阅读
  • 第一部分:构建 Web API
    • 搭建 IDE 新建项目并构建数据库及路由代码
    • CRUD、检索操作与 Controller 接口封装
    • 走关系?数据库模型与API进阶实现
    • Web App 稳定运行的关键-测试
  • 第二部分:制作简单的前端网页应用
  • 第三部分:数据校验、用户验证和授权
  • 第四部分:Vapor 的进阶使用
  • 第五部分:生产部署

在开始本文的实操之前确保前面两节内容已经完成,仅是快食知识的话忽略提醒😂。

本文内容较多,可分多次食用。

父子关系 Parent-Child

父子关系的本质是一对一👨‍👩‍👧或者一对多👨‍👩‍👧‍👦的关系,对应模型就是一个模型拥有 一个多个 其他模型。

举个例子 🌰:

🙋🏼和🐕‍🦺的关系,一个🙋🏼可能养 1 只多只🐕‍🦺,但一个🐕‍🦺可能只有一个主人(🙋🏼)。

回到我们的 TIL 应用中来,用户(父级)创建字母缩写 - Acronym(子级),用户可以拥有很多首字母缩写 ,但是对于一个具体的字母缩写(子级)只可能是由某一个用户(父级)创建的。

构建 User 模型

下面我们需要新建一个 User 模型定义,并创建对应模型的迁移文件,最后创建 User 的 API 控制器(Controller)。

User 模型

打开项目,在 Sources/App/Models 中创建文件 User.swift,添加如下模型定义。

代码注解(类似于之前的 Acronym 模型):

  1. 定义 User 模型类,遵循 Model 协议;
  2. 定义 schema ,这是每个模型都应该有的 let 而且是静态常量,这个名称对应的就是数据库中表名,一般使用 Snake 命名方式,在 Swift 代码中类名、变量名、方法名一般采用驼峰命名法,需要注意一下这个规范;
  3. 每个模型都应该有个用于查询数据的字段,对应定义名为 id,这个变量为 Optional 的 UUID 类型可以为空,同时使用 ID 装饰器,指定 key 为 id 类型;
  4. 接着定义了两个 Field 对应到数据库表中的字段,使用 Field 装饰器指定 key 为表中字段名,如果变量是可选的,需要使用 OptionField 装饰器;
  5. 必须包含两个初始化方法,一个是空的模型初始化实现,用于数据库检索数据时初始化模型数据用,另一个初始化实现用于创建模型数据,这是必须的。
  6. User 模型包含两个 String 属性,分别用于存储用户的姓名和用户名。

下面需要添加模型的迁移文件,在 Sources/App/Migrations 中创建文件 CreateUser.swift,内容参考如下。

类似于 Acronym 模型的迁移,这里:

  1. 为迁移创建一个新的类型 CreateUser ,主要是为了在数据库中创建 名为 users 表;
  2. 添加必要的 preparerevert 方法分别用于创建数据表;

最后,需要在文件 Sources/App/configure.swift 中的 app.migrations.add(CreateAcronym()) 前面添加 User 模型的迁移方法 app.migrations.add(CreateUser())

需要将新模型添加到迁移,以便于 Fluent 在下次应用程序启动时准备数据库中的表。

User API 控制器

在目录 Sources/App/Controllers中添加新文件 UsersController.swift,添加一个用于创建用户的新控制器,如下。

类似于前面添加 Acronym 的 API 路由一样:

  1. 定义了符合 RouteCollection 的新类型 UsersController
  2. 添加了必要的 boot 方法,先是为路径 /api/users 创建了新的路由组,然后绑定 create 方法到 POST 请求;
  3. 添加 create 方法,用于解析请求中的数据并保存到数据库中,最终返回解析后的 user 数据。

我们不要忘记注册控制器,打开文件 Sources/App/routes.swift,添加以下代码。

按照《CRUD、检索操作与 Controller 接口封装》中讲到的 RESTful 接口设计规范,想一下除了上面创建 User 的接口外我们还应添加哪些?我再列一个表供参考。

创建 User 的接口实现了,所以开始实现剩下的接口。

Sources/App/Controllers/UsersController.swiftUserController 中添加获取所有用户、单个用户数据路由处理方法。

类似于 Acronyms 的 API 处理,这里不做过多解释了,如果不明白这里留言评论。

将两个方法在 boot 方法中绑定到路由组,boot 方法代码如下

此时构建并运行程序,我们仍然适用 RapidAPI 测试我们实现的 API。

💡 Tips:我这里使用的是 macOS 下的 RapidAPI 的客户端,与之前使用的 VScode 扩展 RapidAPI Client 使用方法一摸一样。

就像之前 Acronym 的API测试时创建的 API Requests 一样,也可以为 User 提前做好 API 测试请求。

先测试 创建 User。

  • URLhttp://127.0.0.1:8080/api/users
  • MethodPOST
💡Tips:记得对于发送 JSON 负载的请求,需要在 Headers 中添加 Content-Type,设为 application/json。后面创建和更新的接口测试请求都需要注意这一点!
  • name水木
  • usernameDaisy

使用 json 数据,点击 ❺ 位置的按钮就会发送请求。

用户创建 API 请求测试 demo

正常的话我们就会看到显示响应 200 OK,同时收到响应数据(包含了 id 信息)

测试获取所有用户的接口。

  • URLhttp://127.0.0.1:8080/api/users
  • MethodGET
获取所有用户 API 测试 Demo

测试获取单个用户数据的接口。

  • URLhttp://127.0.0.1:8080/api/users/<ID>
  • MethodGET

URL 中加了 ID 数据,这里的 ID 使用的是 Get All Users 获得的数据中的第一条数据的 ID。

获取指定 ID 用户的 API 测试 Demo

搞好父子关系🫣

为了搞通“父(User)子(Acronym)”关系,需要通过模型定义声明关系,昭告世界(数据库)他们是有关系的,具体需要将 user 属性定义到 Acronym 中让每个 Acronym 搞清楚自己的 user 是谁。数据库会以此作为 acronym 表中 user 的引用。

这样做的好处是 Fluent 框架也能更轻易有效的检索数据。

如此一来,如果要获取某个 user 的所有 acronym 数据,就只需要检索包含某个 user 引用的全部 acronym 数据就可以了。同时 acronym 可以通过这个 user 引用找到对应的 user 数据,Fluent 通过装饰器的方式极大的方便了定义这些内容,两字“优雅”

打开 Sources/App/Models/Acronym.swift文件,在 var long: String 下面添加 user

使用 @Parent 属性装饰器创建 UserAcronym 两个模型之间的父子关系,有了这个 Fluent 就知道此属性表述父子关系的父级,说明使用 "userID" 来绑定关系。

这样方便查询数据之外,还允许仅使用 userid 创建 Acronym 数据,不不需要完整 User 数据,有助于减少额外的数据查询。

另外,这个类型非可选,也就是说每个 acronym 都有 user

还需要对其中的 init 方法改造,如下:

这样做主要是:

  1. 为用户 id 添加一个新参数,类型为 User.IDValue 。这是一个由 Model 定义的类型别名,它解析为 UUID
  2. 设置 user 属性包装器的映射值的 id。这避免了您必须执行查找以获取完整的 User 模型来创建 Acronym

因为修改了模型定义,所以还得更新一下迁移文件,打开 Sources/App/Migrations/CreateAcronym.swift,在 .create() 之前添加 .field("userID", .uuid, .required)

优化数据层级

这里用到一种叫做 Domain Transfer Objects(DTOs)的设计模式,优化数据解析和处理。

技术含义
Data Transfer Objects (DTOs) 是一种设计模式,用于在不同层次或部分的应用程序之间传输数据。DTOs通常用于封装数据,它们可以简化和优化网络通信,特别是在客户端和服务器之间交换大量数据时。DTOs可以减少网络请求的次数,因为它们允许一次传输多个数据项。

如果按照模型的定义创建或更新 Acronym时发送的 JSON 有效负载数据,可能是如下的样子:

这是因为 Acronym 中有一个 user 属性,所以必须匹配起来,但这略嫌麻烦冗杂。要优化这个问题就用到了 DTO,使用 DTO 表示客户端应发送或接收内容的类型数据,然后路由处理解析的时候对 DTO 解析转换为代码可用的数据即可,打开 Sources/App/Controllers/AcronymsController.swift 文件添加 CreateAcronymData 定义:

这个 DTO 表示我们从客户端期望的 JSON 数据格式,请求负载就可以这样写了:

这样的话我们就需要调整 Acronym 创建路由的处理了,找到 Sources/App/Controllers/AcronymsController.swift 文件

这样在解析请求负载数据的时候不是原来解析 Acronym 模型了而是 CreateAcronymData 数据,然后穿件 Acronym 数据并保存。

至此就搞定了父子关系。

💡 Tips
因为数据模型多了字段所以需要在运行应用程序之前重置数据库,必须先删掉数据库,以便 Fluent 实现新模型迁移和表的建立。

停止运行的应用程序,终端停止并删除 postgres 的数据库容器,然后重新启动新的数据库,终端命令参考:

  • host: localhost
  • port: 5432
  • user: vapor_username
  • password: vapor_password
  • database: postgres
💡Tips
记得替换命令中的 userpassworddb 名称。新的迁移还是可以在更新表的情况下不丢失生产数据,我会在后面专门的文章中介绍,这里先按照我的安排走。

构建应用并运行,然后测试更新后的创建 Acronym 的接口

  • URLhttp://127.0.0.1:8080/api/acronyms
  • MethodPOST
  • shortDTO
  • longData Transfer Objects
  • userID:使用之前创建用户响应数据中的用户 id
调整后的创建 Acronym API 的测试Demo

同理,还得调整更新 Acronym 的路由处理:

同样,最好是也测试一下,这里就不展示了,自行测试吧🫣

关系数据查询

建立父子关系是为了更高效地查询数据,Fluent 将这一切变得非常简单。

获取父级数据

AcronymsController 中实现通过 acronym 数据直接获取对应的 User 数据。 Sources/App/Controllers/AcronymsController.swift文件中添加 getUser方法:

通过路由参数 id 查询到 acronym 数据,然后通过属性装饰器 $user从数据库中直接获取到 user 数据,底层是通过 idusers 表中查询对应的 user 数据

这里不能通过 acronym.user去获取,一定要注意,否则会出错,记住这种使用方式。

定义了路由处理方法,还需要绑定路由,需要绑定到 /api/acronyms/<acronymID>/user 上,在 boot 方法中添加:

保存,运行应用,测试这个接口。

  • URLhttp://127.0.0.1:8080/api/acronyms/<acronymID>/user
  • MethodGET
获取某个 acronym 的对应用户的 API 的测试 Demo

获取子级数据

为了让 Fluent 快速高效通过 user 直接获取相关联的 acronym 数据,需要在 User 模型中定义 acronyms 属性,使用 @Children 装饰器,打开 Sources/App/Models/User.swift,在 Uservar username: String 之后添加:

这里通过 @Children 装饰器高速 Fluent,,acronyms 表述的是父子关系中的子级项。与 @Parent 不同的是,acronyms 不会再数据库的数据表中添加任何列,只是被用来确定关系所链接的内容。这里就是通过指定 for 参数将属性包装器传递给子模型的父级属性装饰器的键路径,这里的话就是 \Acronym.$user 或者简写为 \.$userFluent 便是通过这个关系说明快速检索子级数据。

我们打开 Sources/App/Controllers/UsersController.swiftUsersController 中添加获取所有 acronym 数据的路由处理方法:

通过路由中 id 参数查询到 user 数据,然后直接通过 acronyms 属性检索获取所有隶属于 useracronyms 数据。

记得将新增的 getAcronyms 方法绑定到对应路由 /api/users/<userID>/acronyms,在 boot 方法中添加:

保存并构建运行应用,测试新增的接口。

  • URLhttp://127.0.0.1:8080/api/users/<userID>/acronyms
  • MethodGET
获取某用户所有 Acronym 数据API 的测试 Demo

外键约束

外键约束描述了两个表之间的链接 🔗。它们经常用于验证。目前,数据库中的 users 表和 acronysm 表之间没有链接。Fluent 是唯一知道该链接的,而使用外键约束其实也让数据库见证这个事情👀。为什么要使用外键?

  1. 它确保无法使用不存在的 user 创建 acronym
  2. 在删除所有 acronym 之前,您无法删除 user
  3. 在删除 acronyms 表之前,您无法删除 users 表。

添加外键约束非常简单,只需要微调 Acronym 模型的迁移文件 Sources/App/Migrations/CreateAcronym.swift 中的 .field("userID", .uuid, .required)为:

.field("userID", .uuid, .required, .references("users", "id"))

这里是添加了从 userID 列到 users 表中 id 列的引用。

因为我们一直是将 AcronymuserID 链接到 users 表,所以应该先创建 users 表,再创建 acronyms 表。也就是将 Sources/App/Configure.swift 中 migration 的顺序调整一下,user 的在前面:

停止应用运行,按照前面提到的步骤重新删除数据库并启动新的数据库容器。创建好新的数据库容器后,构建启动应用,使用不存在的 user id 和数据库存在 userid 分别测试一下 Acronym 创建接口。

  • URLhttp://127.0.0.1:8080/api/acronyms
  • MethodPOST
  • shortDTO
  • longData Transfer Objects
  • userID:第一次用不存在的 id ,第二次用已创建 useruserID
创建 Acronym API 中使用无效 User ID 的测试 Demo

对于不存在的 userID 后台响应了 500 错误,也可以看到提示是外键约束的内容,外键约束生效了。

下面响应成功的则是使用存在且有效 userID 的测试。

创建 Acronym API 中使用有效 User ID 的测试 Demo

很好,搞到这里说明你已经相当有耐心了,算是真正搞定了常规的父子关系,你要不要休息一下♨️。

下一小节我们开始处理“兄弟关系”了。

处理好兄弟关系

兄弟关系父子关系不一样的地方是,它描述的是两个模型之间的互相链接关系,也就是 多对多 的关系,并且两个模型之间没有约束。

比如你的宠物 🐱玩具🪀 之间的关系,一只宠物 🐱 可以玩一个或者多个玩具🪀,反过来一个玩具🪀可以被一只或者多只 宠物 🐱 玩。

在我们的应用中,我们将对 acronym 进行分类(指定 category),acronymcategory 之间就是兄弟关系,一个 acronym 可以指定多个 category,同时一个 category 可以包含多个 acronym

构建 Category 模型

按照上面👆User 模型的构建思路,我们同样需要创建 Category 的模型文件、迁移文件、控制器文件,不同的是我们还需要为 兄弟关系 创建映射关系的表,称为 pivot

Category 模型

Sources/App/Models 目录中创建文件 Category.swift,添加 category 模型基本的定义:

模型除了基本的 id 属性之外,只包含一个 String 属性用来保存类别的名称。

Sources/App/Migrations 目录中添加迁移文件 CreateCategory.swift,内容与之前几个模型的类似,代码如下:

我想对于代码应该不用过多解释了吧,注意 schema 要与模型定义中的一致。

然后打开 Sources/App/Configure.swift 文件,在 app.migrations.add(CreateAcronym()) 添加 Category 的迁移 app.migrations.add(CreateCategory()),这样在下次应用程序启动时 Fluent 就会在数据库中创建表。

Category 控制器

Sources/App/Controllers 中创建文件 CategoriesController.swift 文件,并添加相应代码如下:

代码简单解释一下,和前面各模型相关的路由控制器基本类似:

  1. 定义了新的控制器 CategoriesController,实现协议 RouteCollection必须的方法 boot,并添加路由组,在组中绑定相应路由到具体的路由处理方法;
  2. 实现了三个路由处理方法,分别是 createindexshow,对应的是创建 category、获取所有 category 数据、获取指定 idcategory 数据。

具体接口设计如下:

下面我们需要把控制器注册给应用,打开 Sources/App/routes.swift,在 route(_:) 方法内部最后添加代码:

至此,我们完成了接口的实现,运行应用并测试新增的三个接口。

这里只展示创建 category 接口的测试过程,剩余两个自行测试。

RapidAPI 客户端中添加 category 的请求,并开始测试,下面是创建 category 数据的测试信息:

  • URLhttp://127.0.0.1:8080/api/categories
  • MethodPOST

还是实用 JSON 数据,记得 Headers 中添加 Content-Typeapplication/jsonBody 中添加数据:

创建类别 API 测试 Demo

创建 Pivot 表

这个 Pivot 表是关系型数据库中多对多实现高效检索必须要创建的,通常包含两个字段,分别是两个多对多模型对应数据表中的主键。在我们应用这里,创建 Pivot 这么个映射表这样就能通过 id 快速检索某个 acronym 对应的所有 category,反过来亦然。

好,我们现在就开始创建,在 Sources/App/Models 中创建 AcronymCategoryPivot.swift 文件,并添加 Pivot 模型定义:

  1. 代码中定义了 AcronymCategoryPivot 模型,添加 id 属性,然后使用 @Parent 装饰器将定义的 acronym 链接到模型 Acronym,使用 @Parent 装饰器将定义的 category 链接到模型 Category
  2. 然后添加必要的两个初始化方法。

Pivot 也是一个模型,所以也需要创建它的迁移文件,在 Sources/App/Migrations 中创建 CreateAcronymCategorPivot.swift文件,添加以下内容:

schema 与模型定义中的一致,这里如父子关系中 Acronym 的迁移文件中 user,需要为 acronymIDcategoryID 添加模型引用创建外键约束。

然后,打开配置文件 Sources/App/Configure.swift 中,在 app.migrations.add(CreateCategory()) 之后添加 app.migrations.add(CreateAcronymCategoryPivot())

Fluent 框架为两个模型数据之间关系创建和删除提供了很便捷的方法,还需要我们做点准备,打开 Sources/App/Models/Acronym.swift 文件,在 user 属性后面添加 categories 属性:

这样可以极大方便我们检索某个 acronym 的所有 category 数据,可以看到使用了新的装饰器 @Siblings,其中三个参数含义如下:

  • throughPivot的模型类型
  • from:从 Pivot 到根模型的键路径,这里是使用 AcronymCategoryPivotacronym 属性。
  • to:从引用相关模型的 Pivot 键路径。这里是使用 AcronymCategoryPivot 上的 category 属性。

@Parent一样, @Siblings 允许您将相关模型指定为属性,而无需它们初始化实例。同时,它会告诉 Fluent 在数据库中执行查询时如何映射兄弟项。

另外, @Parent使用数据库中的父级 id列,但 @Siblings 必须在数据库中的两个不同模型和 Pivot 之间进行联接查询。比较好的是,Fluent 实现了简单操作。

打开 Sources/App/Controllers/AcronymsController.swift 文件,AcronymsControllergetUser(_ req: Request) 下面添加代码:

attachdetach 分别是绑定和解绑 acronymcategory 关系的路由实现,在两个方法中你会看到 Fluent 提供的模型方法 attach(_:on:)detach(_:on:),方便很多!

boot 方法中绑定这两个方法到路由 /api/acronyms/<acronymID>/categories/<categoryID> 上。

运行应用,测试这两个新的 API,在 RapidAPI 中添加请求

绑定 acronym 和 category

  • URLhttp://127.0.0.1:8080/api/acronyms/<acronymID>/categories/<categoryID>
  • MethodPOST
绑定 Acronym 和 Category 的API测试 Demo

解绑 acronym 和 category

  • URLhttp://127.0.0.1:8080/api/acronyms/<acronymID>/categories/<categoryID>
  • MethodDELETE
解绑 Acronym 和 Category 的API测试 Demo

查询兄弟关系数据

查询 Acronym 的 Category 数据

因为已经在 Acronym 模型中添加了 categories 属性,所以可以使用 Fluent 轻松查询某个 acronym 绑定的所有 category,下面我们添加对应的 API,继续编辑 Sources/App/Controllers/AcronymsController.swift 文件,在刚才添加的 detach 方法后面添加 getCategories 方法:

直接通过属性装饰器修饰的 categories 就能获取即 .$categories.get(on: req.db),绑定这个路由方法到路由 /api/acronyms/<acronymID>/categories 上,在 boot 方法的 acronymRoutes.delete(":acronymID", "categories", ":categoryID", use: detach) 后面添加:

编译运行,正常的话使用 RapidAPI 客户端创建响应的请求就能获取到数据了。

  • URLhttp://127.0.0.1:8080/api/acronyms/<acronymID>/categories
  • MethodGET
获取某 Acronym 绑定的所有 category 的 API 测试 Demo

查询 Category 的 Acronym 数据

打开 Sources/App/Models/Category.swift 文件,在 name 属性下面添加 acronyms 属性,同 Acronym 模型中添加 categories,使用 @Siblings 装饰器:

然后打开 Category 的 API 控制器文件 Sources/App/Controllers/CategoriesController.swift ,在 CategoriesController 中添加 getAcronyms 方法:

然后将其绑定到路由 /api/categories/<categoryID>/acronyms 上,boot 方法 categoriesRoutes.get(":categoryID", use: show) 下面添加:

重新运行应用测试获取某个 category 对应所有 acronym 的 API。

  • URLhttp://127.0.0.1:8080/api/categories/<categoryID>/acronyms
  • MethodGET
获取某 Categor 下所有的 acronym 数据 API 的测试 Demo

啊,长舒一口气~本文内容结束,希望至此你还一切正常🙃

总结

通过本文内容的实操,相信你应该对模型与模型间的关系实现有了初步了解,同时对于接口的设计应该有了更深刻的体会了,RESTful 的接口设计还是很优雅的,布置一个小作业:

📑 Task
尝试为 User 添加上更新和删除的 API 并进行测试。

本文代码:s1-3.zip

RapidAPI API 测试请求文件:TILApp.paw

相关文章