第10章:兄弟姐妹关系¶
在第9章"父子关系"中,你学会了如何使用Fluent
来建立模型之间的父子关系。本章告诉你如何实现另一种关系:兄弟姐妹关系。你将学习如何在Vapor
中为它们建模,以及如何在路由中使用它们。
Note
本章要求你已经设置并配置了PostgreSQL
。按照第6章"配置数据库"的步骤,在Docker
中设置PostgreSQL
并配置Vapor
应用程序。
兄弟姐妹关系¶
兄弟姐妹关系描述了一种将两个模型相互连接的关系。它们也被称为多对多关系。与父子关系不同,在兄弟姐妹关系中,模型之间没有约束。
例如,如果你对宠物和玩具之间的关系进行建模,一个宠物可以有一个或多个玩具,一个玩具可以被一个或多个宠物使用。在TIL
应用程序中,你将能够对首字母缩写词进行分类。一个首字母缩写可以是一个或多个类别的一部分,一个类别可以包含一个或多个首字母缩写。
创建一个类别¶
为了实现分类,你需要创建一个模型、一个迁移、一个控制器和一个枢纽。首先要创建模型。
类别模型¶
在Xcode
中,在Sources/App/Models
创建一个新文件Category.swift
。打开该文件并插入一个类别的基本模型:
import Fluent
import Vapor
final class Category: Model, Content {
static let schema = "categories"
@ID
var id: UUID?
@Field(key: "name")
var name: String
init() {}
init(id: UUID? = nil, name: String) {
self.id = id
self.name = name
}
}
该模型包含一个String
属性,用来保存类别的名称。模型还包含一个可选的id
属性,当它被设置时,存储模型的ID
。你用它们各自的属性包装器来注释这两个属性。
接下来,在Sources/App/Migrations
创建一个新文件CreateCategory.swift
。在这个新文件中插入以下内容:
import Fluent
struct CreateCategory: Migration {
func prepare(on database: Database) -> EventLoopFuture<Void> {
database.schema("categories")
.id()
.field("name", .string, .required)
.create()
}
func revert(on database: Database) -> EventLoopFuture<Void> {
database.schema("categories").delete()
}
}
现在你应该明白了 它使用与模型中定义的schema
相同的值来创建表,并带有必要的属性。迁移时在revert(on:)
中删除了该表。
最后,打开configure.swift
,在app.migrations.add(CreateAcronym())
之后,将CreateCategory
加入到迁移列表中:
app.migrations.add(CreateCategory())
这将把新的迁移添加到应用程序的迁移中,以便Fluent
在下次启动应用程序时在数据库中创建该表。
类别控制器¶
现在是创建控制器的时候了。在Sources/App/Controllers
中,创建一个名为CategoriesController.swift
的新文件。打开该文件,为一个新的控制器添加代码,以创建和检索类别:
import Vapor
// 1
struct CategoriesController: RouteCollection {
// 2
func boot(routes: RoutesBuilder) throws {
// 3
let categoriesRoute = routes.grouped("api", "categories")
// 4
categoriesRoute.post(use: createHandler)
categoriesRoute.get(use: getAllHandler)
categoriesRoute.get(":categoryID", use: getHandler)
}
// 5
func createHandler(_ req: Request)
throws -> EventLoopFuture<Category> {
// 6
let category = try req.content.decode(Category.self)
return category.save(on: req.db).map { category }
}
// 7
func getAllHandler(_ req: Request)
-> EventLoopFuture<[Category]> {
// 8
Category.query(on: req.db).all()
}
// 9
func getHandler(_ req: Request)
-> EventLoopFuture<Category> {
// 10
Category.find(req.parameters.get("categoryID"), on: req.db)
.unwrap(or: Abort(.notFound))
}
}
以下是控制器的作用:
- 定义一个新的
CategoriesController
类型,符合RouteCollection
。 - 按照
RouteCollection
的要求实现boot(routes:)
。这是你注册路由处理程序的地方。 - 为路径
/api/categories
创建一个新的路由组。 - 将路由处理程序注册到它们的路由。
- 定义
createHandler(_:)
,创建一个类别。 - 从请求中解码类别并保存它。
- 定义
getAllHandler(_:)
,返回所有的类别。 - 执行一个Fluent查询,从数据库中获取所有的类别。
- 定义
getHandler(_:)
,返回单一类别。 - 从请求中获取ID,并使用它来查找类别。
最后,打开routes.swift
,在routes(_:)
的末尾添加以下内容来注册控制器:
let categoriesController = CategoriesController()
try app.register(collection: categoriesController)
与前几章一样,这将实例化一个控制器,并将其与应用程序注册,以启用其路由。
建立并运行应用程序,然后在RESTed
中创建一个新的请求。配置该请求如下:
- URL: http://localhost:8080/api/categories
- method: POST
- Parameter encoding: JSON-encoded
添加一个带有名称和值的单一参数:
- name: Teenager
发送请求,你将在响应中看到保存的类别:
创建一个枢纽¶
在第9章"父子关系"中,你在首字母缩写中添加了对用户的引用,以创建首字母缩写和用户之间的关系。然而,你不能像这样建立兄弟姐妹关系的模型,因为这样的查询效率太低。如果你有一个类别内的首字母缩写数组,要搜索一个首字母缩写的所有类别,你就必须检查每一个类别。如果你在一个缩写里面有一个类别数组,要搜索一个类别中的所有缩写,你就必须检查每一个缩写。你需要一个单独的模型来保持这种关系。在Fluent
中,这是一个pivot
。
枢轴是Fluent
中的另一种模型类型,包含了这种关系。在Xcode
中,在Sources/App/Models
中创建这个名为AcronymCategoryPivot.swift
的新模型文件。打开AcronymCategoryPivot.swift
并添加以下内容来创建透视:
import Fluent
import Foundation
// 1
final class AcronymCategoryPivot: Model {
static let schema = "acronym-category-pivot"
// 2
@ID
var id: UUID?
// 3
@Parent(key: "acronymID")
var acronym: Acronym
@Parent(key: "categoryID")
var category: Category
// 4
init() {}
// 5
init(
id: UUID? = nil,
acronym: Acronym,
category: Category
) throws {
self.id = id
self.$acronym.id = try acronym.requireID()
self.$category.id = try category.requireID()
}
}
以下是这个模型的作用:
- 定义一个新的对象
AcronymCategoryPivot
,符合Model
。 - 为该模型定义一个
id
。注意这是一个UID
类型,所以你必须导入基金会模块。 - 定义两个属性以链接到
Acronym
和Category
。你用@Parent
属性包装器来注释这些属性。一个枢轴记录只能指向一个Acronym
和一个Category
,但每个类型都可以指向多个枢轴。 - 按照
Model
的要求,实现空初始化器。 - 实现一个初始化器,将两个模型作为参数。这使用
requireID()
来确保模型有一个ID
设置。
接下来为枢轴创建迁移。在Sources/App/Migrations
创建一个新文件,CreateAcronymCategoryPivot.swift
。打开这个新文件,插入以下内容:
import Fluent
// 1
struct CreateAcronymCategoryPivot: Migration {
// 2
func prepare(on database: Database) -> EventLoopFuture<Void> {
// 3
database.schema("acronym-category-pivot")
// 4
.id()
// 5
.field("acronymID", .uuid, .required,
.references("acronyms", "id", onDelete: .cascade))
.field("categoryID", .uuid, .required,
.references("categories", "id", onDelete: .cascade))
// 6
.create()
}
// 7
func revert(on database: Database) -> EventLoopFuture<Void> {
database.schema("acronym-category-pivot").delete()
}
}
以下是新迁移的作用:
- 定义一个新的类型,
CreateAcronymCategoryPivot
,符合Migration
。 - 按照
Migration
的要求实现prepare(on:)
。 - 使用为
AcronymCategoryPivot
定义的模式名称选择表。 -
创建
ID
列。 -
为两个属性创建两列。这两个列使用提供给属性包装器的键,将类型设置为
UUID
,并将该列标记为必需。它们还设置了对各自模型的引用,以创建一个外键约束。正如第9章"父子关系"中所说,在兄弟姐妹关系中使用外键约束是一个很好的做法。目前的AcronymCategoryPivot
没有检查缩略语和类别的ID
。如果没有这个约束,你可以删除仍然由枢轴连接的首字母缩写和类别,这种关系将继续存在,而不会出现错误提示。当你删除模型的时候,迁移还设置了一个级联模式参考动作。这将导致数据库自动删除这个关系,而不是抛出一个错误。 - 调用
create()
来在数据库中创建表。 - 按照
Migration
的要求执行revert(on:)
。这将删除数据库中的表。
最后,打开configure.swift
,在app.migrations.add(CreateCategory())
之后,将CreateAcronymCategoryPivot
加入到迁移列表:
app.migrations.add(CreateAcronymCategoryPivot())
这样就把新的pivot
模型添加到了应用程序的迁移中,这样Fluent
就会在下一次应用程序启动时在数据库中准备好这个表。
要真正在两个模型之间建立关系,需要使用透视器。Fluent
提供了创建和删除关系的便利函数。首先,打开Acronym.swift
,在模型下面添加一个新的属性var use: User
:
@Siblings(
through: AcronymCategoryPivot.self,
from: \.$acronym,
to: \.$category)
var categories: [Category]
这增加了一个新属性,允许你查询兄弟姐妹关系。你用@Siblings
属性包装器来注释这个新属性。@Siblings
有三个参数:
- 枢轴的模型类型
- 引用根模型的
pivot
的关键路径。在这种情况下,你使用AcronymCategoryPivot
的acronym
属性。 - 来自引用相关模型的枢轴的关键路径。在这种情况下,你使用
AcronymCategoryPivot
的category
属性。
像@Parent
一样,@Siblings
允许你将相关模型指定为一个属性,而不需要他们初始化一个实例。该属性包装器还告诉Fluent
在数据库中执行查询时如何映射兄弟姐妹。
当@Parent
使用数据库中的父级ID
列时,@Siblings
必须在两个不同的模型和数据库中的透视之间进行连接。值得庆幸的是,Fluent
为你抽象出了这一点,让你轻松搞定
打开AcronymsController.swift
,在getUserHandler(_:)
下面添加以下路由处理程序,以设置缩写和类别之间的关系:
// 1
func addCategoriesHandler(_ req: Request)
-> EventLoopFuture<HTTPStatus> {
// 2
let acronymQuery =
Acronym.find(req.parameters.get("acronymID"), on: req.db)
.unwrap(or: Abort(.notFound))
let categoryQuery =
Category.find(req.parameters.get("categoryID"), on: req.db)
.unwrap(or: Abort(.notFound))
// 3
return acronymQuery.and(categoryQuery)
.flatMap { acronym, category in
acronym
.$categories
// 4
.attach(category, on: req.db)
.transform(to: .created)
}
}
以下是路线处理程序的工作:
- 定义一个新的路由处理程序,
addCategoriesHandler(_:)
,它返回EventLoopFuture<HTTPStatus>
。 - 定义两个属性来查询数据库,并从提供给请求的
ID
中获得缩写和类别。每个属性都是一个`EventLoopFuture'。 - 使用
and(_:)
来等待两个future
的返回。 - 使用
attach(_:on:)
来建立acronym
和category
之间的关系。这将创建一个透视模型,并将其保存在数据库中。将结果转化为201 Created
响应。像Fluent
的许多操作一样,你在属性包装器上调用attach(_:on:)
预测值,而不是属性本身。
在boot(routes:)
的底部注册这个路由处理程序:
acronymsRoutes.post(
":acronymID",
"categories",
":categoryID",
use: addCategoriesHandler)
这将HTTP POST
请求路由到/api/acronyms/<ACRONYM_ID>/categories/<CATEGORY_ID>
到addCategoriesHandler(_:)
。
构建并运行应用程序,启动RESTed
。如果你的数据库中没有任何缩略语,现在就创建一个。然后,创建一个新的请求,配置如下:
- URL: http://localhost:8080/api/acronyms/
/categories/ - method: POST
这就在缩写和具有所提供的ID
的类别之间建立了兄弟姐妹关系。你在本章的早些时候创建了这个类别。
点击Send Request
,你会看到一个201 Created
的响应:
查询关系¶
缩略语和类别现在是以兄弟姐妹的关系联系起来的。但如果你不能查看这些关系,这就不是很有用了! Fluent
提供了一些函数,允许你查询这些关系。你已经使用了上面的一个函数来创建关系。
缩略语的类别¶
打开AcronymsController.swift
,在addCategoriesHandler(:_)
后面添加一个新的路由处理器:
// 1
func getCategoriesHandler(_ req: Request)
-> EventLoopFuture<[Category]> {
// 2
Acronym.find(req.parameters.get("acronymID"), on: req.db)
.unwrap(or: Abort(.notFound))
.flatMap { acronym in
// 3
acronym.$categories.query(on: req.db).all()
}
}
下面是这个的作用:
- 定义路由处理程序
getCategoriesHandler(_:)
返回EventLoopFuture<[Category]>
。 - 使用提供的
ID
从数据库中获取首字母缩写,并解开返回的未来。 - 使用新的属性包装器来获取类别。然后使用
Fluent
查询来返回所有的类别。
在boot(routes:)
的底部注册这个路由处理程序:
acronymsRoutes.get(
":acronymID",
"categories",
use: getCategoriesHandler)
这将HTTP GET
请求路由到/api/acronyms/<ACRONYM_ID>/categories
到getCategoriesHandler(:_)
。
建立并运行应用程序,并启动RESTed
。创建一个具有以下属性的请求:
- URL: http://localhost:8080/api/acronyms/
/categories - method: GET
发送请求,你就会收到该首字母缩写所处的类别数组:
类别的首字母缩写¶
打开Category.swift
,在var name: String
下面添加一个新的属性,注释为@Siblings
:
@Siblings(
through: AcronymCategoryPivot.self,
from: \.$category,
to: \.$acronym)
var acronyms: [Acronym]
像以前一样,这增加了一个新属性,允许你查询兄弟姐妹关系。@Siblings
提供了所有必要的语法糖来设置、查询和处理兄弟姐妹关系。
打开CategoriesController.swift
,在getHandler(_:)
后面添加一个新的路由处理程序:
// 1
func getAcronymsHandler(_ req: Request)
-> EventLoopFuture<[Acronym]> {
// 2
Category.find(req.parameters.get("categoryID"), on: req.db)
.unwrap(or: Abort(.notFound))
.flatMap { category in
// 3
category.$acronyms.get(on: req.db)
}
}
下面是这个的作用:
- 定义一个新的路由处理程序,
getAcronymsHandler(_:)
,返回EventLoopFuture<[Acronym]>
。 - 使用提供给请求的
ID
从数据库中获取类别。确保返回一个,并解开未来。 - 使用新的属性包装器来获取首字母缩写。这使用
get(on:)
来为你执行查询。这与前面的query(on: req.db).all()
相同。
在boot(routes:)
的底部注册这个路由处理程序:
categoriesRoute.get(
":categoryID",
"acronyms",
use: getAcronymsHandler)
这将HTTP GET
请求路由到/api/categories/<CATEGORY_ID>/acronyms
到getAcronymsHandler(_:)
。
建立并运行应用程序,并启动RESTed
。创建一个请求,如下:
- URL: http://localhost:8080/api/categories/
/acronyms - method: GET
发送请求,你就会收到该类别中的缩略语阵列:
移除关系¶
删除一个缩写和一个类别之间的关系与添加关系非常相似。打开AcronymsController.swift
,在getCategoriesHandler(req:)
下面添加以下内容:
// 1
func removeCategoriesHandler(_ req: Request)
-> EventLoopFuture<HTTPStatus> {
// 2
let acronymQuery =
Acronym.find(req.parameters.get("acronymID"), on: req.db)
.unwrap(or: Abort(.notFound))
let categoryQuery =
Category.find(req.parameters.get("categoryID"), on: req.db)
.unwrap(or: Abort(.notFound))
// 3
return acronymQuery.and(categoryQuery)
.flatMap { acronym, category in
// 4
acronym
.$categories
.detach(category, on: req.db)
.transform(to: .noContent)
}
}
以下是新的路径处理程序的作用:
- 定义一个新的路由处理程序,
removeCategoriesHandler(_:)
,返回一个EventLoopFuture<HTTPStatus>
。 - 执行两个查询,从提供的
ID
中获得缩写和类别。 - 使用
and(_:)
来等待两个future
的返回。 - 使用
detach(_:on:)
删除首字母缩写
和类别
之间的关系。这可以找到数据库中的枢轴模型并删除它。将结果转化为204 No Content
响应。
最后,在boot(route:)
的底部注册路由:
acronymsRoutes.delete(
":acronymID",
"categories",
":categoryID",
use: removeCategoriesHandler)
这将HTTP DELETE
请求路由到/api/acronyms/<ACRONYM_ID>/categories/<CATEGORY_ID>
到removeCategoriesHandler(_:)
。
构建并运行应用程序,启动RESTed
。创建一个具有以下属性的请求:
- URL: http://localhost:8080/api/acronyms/
/categories/ - method: DELETE
发送请求,你会收到204 No Content
的回复:
如果你再次发送请求来获取缩写的类别,你会收到一个空数组。
接下来去哪?¶
在本章中,你学习了如何使用Fluent
在Vapor
中实现兄弟姐妹关系。在这一节中,你学会了如何使用Fluent
对所有类型的关系进行建模并执行高级查询。TIL
的API
功能齐全,可以供客户使用。
在下一章,你将学习如何为应用程序编写测试,以确保你的代码是正确的。然后,本书的下一节将向你展示如何创建强大的客户端来与API
进行交互--无论是在iOS
上还是在网络上。