首页 iOS.& Swift Books 服务器端迅速与蒸气

9
亲子关系 由Tim Condon撰写

第5章,“流利&持久的模型“,介绍了模型的概念。在本章中,您将学习如何在两个模型之间设置父子关系。您还将学习这些关系的目的,如何将它们模拟蒸气和如何使用路由使用它们。

笔记:本章要求您已设置和配置PostgreSQL。按照第6章“配置数据库”的步骤“配置数据库”,在Docker中设置PostgreSQL并配置蒸气应用程序。

亲子关系

亲子关系 描述一个模型具有一个或多个模型的“所有权”的关系。它们也被称为 一对一一对多 relationships.

例如,如果您建模人与宠物之间的关系,则一个人可以拥有一个或多个宠物。宠物只能有一个所有者。在TIL应用程序中,用户将创建首字母缩略词。用户(父级)可以有许多缩略语,并且只能由一个用户创建缩略词(孩子)。

创建用户

In Xcode, create a new file for the User class called user.swift.源/应用/型号。接下来,创建迁移文件, createUser.swift., 在 源/应用/迁移。最后,创建一个名为 Userscontroller.swift.来源/应用程序/控制器 for the UsersController.

用户模型

在Xcode,打开 user.swift. 并为用户创建基本模型:

import Fluent
import Vapor

final class User: Model, Content {
  static let schema = "users"

  @ID
  var id: UUID?
   
  @Field(key: "姓名")
  var name: String
   
  @Field(key: "用户名")
  var username: String
    
  init() {}
    
  init(id: UUID? = nil, name: String, username: String) {
    self.name = name
    self.username = username
  }
}

The model contains two String properties to hold the user’s name and username. It also contains an optional id property that stores the ID of the model assigned by the database when it’s saved. You annotate each property with the relevant property wrapper.

接下来,开放 createUser.swift. 并插入以下内容:

import Fluent

// 1
struct CreateUser: Migration {
  // 2
  func prepare(on database: Database) -> EventLoopFuture<Void> {
    // 3
    database.schema("users")
      // 4
      .id()
      // 5
      .field("姓名", .string, .required)
      .field("用户名", .string, .required)
      // 6
      .create()
  }
  
  // 7
  func revert(on database: Database) -> EventLoopFuture<Void> {
    database.schema("users").delete()
  }
}

这是您的迁移所做的:

  1. 为迁移创建新类型以在数据库中创建用户表。
  2. Implement prepare(on:) as required by Migration.
  3. Set up the schema for User with the name of the table as users.
  4. 使用默认属性创建ID列。
  5. Create the columns for the two other properties. These are both String 和 required. The name of the columns match the keys defined in the property wrapper for each property.
  6. 创建表格。
  7. Implement revert(on:) as required by Migration. This deletes the table named users.

最后,开放 configure.swift. to add CreateUser to the migration list. Insert the following after app.migrations.add(CreateAcronym()):

app.migrations.add(CreateUser())

这将新模型添加到迁移,因此流利地在下一个应用程序开始时准备数据库中的表。

用户控制器

打开 Userscontroller.swift. 并创建一个可以创建用户的新控制器:

import Vapor

// 1
struct UsersController: RouteCollection {
  // 2
  func boot(routes: RoutesBuilder) throws {
    // 3
    let usersRoute = routes.grouped("api", "users")
    // 4
    usersRoute.post(use: createHandler)
  }

  // 5
  func createHandler(_ req: Request) 
    throws -> EventLoopFuture<User> {
    // 6
    let user = try req.content.decode(User.self)
    // 7
    return user.save(on: req.db).map { user }
  }
}

这应该看起来很熟悉;这是它所做的:

  1. Define a new type UsersController that conforms to RouteCollection.
  2. Implement boot(routes:) as required by RouteCollection.
  3. 为路径创建一个新的路由组 / API /用户.
  4. Register createHandler(_:) to handle a POST request to / API /用户.
  5. 定义路由处理程序函数。
  6. 解码来自请求正文的用户。
  7. Save the decoded user. save(on:) returns EventLoopFuture<Void> so use map(_:) to wait for the save to complete and return the saved user.

最后,开放 路线.swift. 和 add the following to the end of routes(_:):

// 1
let usersController = UsersController()
// 2
try app.register(collection: usersController)

这是它的所作所为:

  1. Create a UsersController instance.
  2. 使用路由器注册新的控制器实例以连接路由。

打开 Userscontroller.swift. again and add the following to the end of UsersController. These functions return a list of all users and a single user, respectively:

// 1
func getAllHandler(_ req: Request) 
  -> EventLoopFuture<[User]> {
  // 2
  User.query(on: req.db).all()
}

// 3
func getHandler(_ req: Request) 
  -> EventLoopFuture<User> {
  // 4
  User.find(req.parameters.get("用户身份"), on: req.db)
      .unwrap(or: Abort(.notFound))
}

这是它的所作所为:

  1. Define a new route handler, getAllHandler(_:), that returns EventLoopFuture<[User]>.
  2. 使用流利查询返回所有用户。
  3. Define a new route handler, getHandler(_:), that returns EventLoopFuture<User>.
  4. Return the user specified by the request’s parameter named 用户身份.

Register these two route handlers at the end of boot(routes:):

// 1
usersRoute.get(use: getAllHandler)
// 2
usersRoute.get(":userID", use: getHandler)

这是它的所作所为:

  1. Register getAllHandler(_:) to process GET requests to / api /用户/.
  2. Register getHandler(_:) to process GET requests to / api /用户/<USER ID>. This uses a dynamic path component that matches the parameter you search for in getHandler(_:).

构建并运行应用程序,然后在休息中创建新请求。配置请求,如下所示:

使用名称和值添加两个参数:

  • 姓名: 你的名字
  • 用户名:您选择的用户名

发送请求,您将在响应中看到已保存的用户:

建立关系

在蒸气中建模父子关系匹配数据库如何模拟关系,但以“越过”的方式。由于用户拥有每个缩写,因此您将用户属性添加到缩写。该数据库表示这是对缩略语表中用户的引用。这允许有效地搜索数据库。

要获取用户的所有首字母缩字,请检索包含该用户参考的所有首字母缩略词。要获取首字母缩写的用户,请使用该缩略语中的用户。 Fluent使用属性包装器来使所有这一切成为可能。

打开 缩写 和 add a new property after var long: String:

@Parent(key: "用户身份")
var user: User

This adds a User property of to the model. It uses the @Parent property wrapper to create the link between the two models. Note this type is not optional, so an acronym must have a user. @Parent is another special Fluent property wrapper. It tells Fluent that this property represents the parent of a parent-child relationship. Fluent uses this to query the database. @Parent also allows you to create an Acronym using only the ID of a User, without needing a full User object. This helps avoid additional database queries.

用以下内容替换初始化程序以反映:

// 1
init(
  id: UUID? = nil, 
  short: String, 
  long: String,
  userID: User.IDValue
) {
  self.id = id
  self.short = short
  self.long = long
  // 2
  self.$user.id = userID
}

这是你改变的东西:

  1. Add a new parameter to the initializer for the user’s ID of type User.IDValue. This is a typealias defined by Model, which resolves to UUID.

  2. Set the ID of the projected value of the user property wrapper. As discussed above, this avoids you having to perform a lookup to get the full User model to create an Acronym.

最后,开放 createacronmw.swift.. Before .create() add the following line:

.field("用户身份", .uuid, .required)

This adds the new column for user using the key provided to the @Parent property wrapper. The column type, uuid, matches the ID column type from CreateUser.

域传输对象(DTO)

You can send a request with a JSON payload to match the new Acronym model. However, it looks like:

{
  "短的": "OMG",
  "长": "Oh My God",
  "user": {
    "id": "2074AD1A-21DC-4238-B3ED-D076BBE5D135"
  }
}

Because Acronym has a user property, the JSON must match this. The property wrapper allows you to only send an id for user, but it’s still complex to create. To solve this, you use a 域传输对象 或dto。 DTO是一种代表客户应该发送或接收的类型的类型。然后,您的路由处理程序接受DTO并将其转换为您的代码可以使用的内容。在底部 亚焦焦控制器.swift.,添加以下代码:

struct CreateAcronymData: Content {
  let short: String
  let long: String
  let userID: UUID
}

这DTO代表了我们期望客户的JSON:

{
  "短的": "OMG",
  "长": "Oh My God",
  "用户身份": "2074AD1A-21DC-4238-B3ED-D076BBE5D135"
}

Next, replace the body of createHandler(_:) with the following:

// 1
let data = try req.content.decode(CreateAcronymData.self)
// 2
let acronym = Acronym(
  short: data.short, 
  long: data.long,
  userID: data.userID)
return acronym.save(on: req.db).map { acronym }

以下是更新代码更改的内容:

  1. Decode the request body to CreateAcronymData instead of Acronym.
  2. Create an Acronym from the data received.

That’s all you need to do to set up the relationship! Before you run the application, you need to reset the database. Fluent has already run the CreateAcronym migration but the table has a new column now. To add the new column to the table, you must delete the database so Fluent will run the migration again. Stop the application in Xcode and then in Terminal, enter:

# 1
docker stop postgres
# 2
docker rm postgres
# 3
docker run --name postgres -e POSTGRES_DB=vapor_database \
  -e POSTGRES_USER=vapor_username \
  -e POSTGRES_PASSWORD=vapor_password \
  -p 5432:5432 -d postgres

这是它的所作所为:

  1. 停止运行的Docker容器 postgres.。这是当前运行数据库的容器。
  2. 卸下Docker容器 postgres. 删除任何现有数据。
  3. 启动运行PostgreSQL的新Docker容器。有关更多信息,请参见第6章“配置数据库”。

笔记:新迁移也可以改变表,以便在更改模型时不会丢失生产数据。第27章“数据库/ API版本控制&迁移“涵盖了这一点。

在Xcode和迁移运行中构建并运行应用程序。打开休息并在本章早期的步骤后创建用户。确保复制返回的ID。

在休息和配置中创建新请求,如下所示:

使用名称和值添加三个参数:

  • 短的: 我的天啊
  • : 我的天啊
  • 用户身份:您之前复制的ID

点击 发送请求。您的应用程序与指定的用户创建首字母缩写:

最后,开放 亚焦焦控制器.swift. 和 replace updateHandler(_:) with the following to account for the new property on Acronym:

func updateHandler(_ req: Request) throws 
    -> EventLoopFuture<Acronym> {
  let updateData = 
    try req.content.decode(CreateAcronymData.self)
  return Acronym
    .find(req.parameters.get("acronymID"), on: req.db)
    .unwrap(or: Abort(.notFound))
    .flatMap { acronym in
      acronym.short = updateData.short
      acronym.long = updateData.long
      acronym.$user.id = updateData.userID
      return acronym.save(on: req.db).map {
        acronym
      }
    }
}

使用请求中提供的新值更新缩略乳的属性,包括新用户ID。

查询关系

用户和首字母缩略词现在与父子关系相关联。但是,直到您可以查询这些关系,这不是很有用。再一次,流利使得这很容易。

获得父母

打开 亚焦焦控制器.swift. 和 add a new route handler after sortedHandler(_:):

// 1
func getUserHandler(_ req: Request) 
  -> EventLoopFuture<User> {
  // 2
  Acronym.find(req.parameters.get("acronymID"), on: req.db)
    .unwrap(or: Abort(.notFound))
    .flatMap { acronym in
      // 3
      acronym.$user.get(on: req.db)
    }
}

这是此路由处理程序所做的:

  1. Define a new route handler, getUserHandler(_:), that returns EventLoopFuture<User>.
  2. 获取请求参数中指定的缩写,并勾勒出退回的将来。
  3. Use the property wrapper to get the acronym’s owner from the database. This performs a query on the User table to find the user with the ID saved in the database. If you try to access the property with acronym.user, you’ll get an error because you haven’t retrieved the user from the database. Chapter 31, “Advanced Fluent”, discusses eager loading and working with properties.

Register the route handler at the end of boot(routes:):

acronymsRoutes.get(":acronymID", "user", use: getUserHandler)

这将HTTP GET请求连接到 / api /首字母缩略词/<ACRONYM ID>/user to getUserHandler(_:).

构建并运行应用程序,然后在休息中创建新请求。配置请求,如下所示:

发送请求,您将看到响应返回缩写的用户:

让孩子们

让模型的孩子遵循类似的模式。打开 user.swift. 和 add a new property below var username: String:

@Children(for: \.$user)
var acronyms: [Acronym]

This defines a new property — the user’s acronyms. You annotate the property with the @Children property wrapper. @Children tells Fluent that acronyms represents the children in a parent-child relationship. This is like @ID@Field, which you saw in Chapter 5, “Fluent & Persisting Models”.

Unlike @Parent, @Children doesn’t represent any column in the database. Fluent uses it to know what to link for the relationship. You pass the property wrapper a keypath to the parent property wrapper on the child model. In this case, you use \Acronym.$user, or just \.$user. Fluent uses this to query the database when retrieving all the children.

Fluent’s use of property wrappers also allows it to handle encoding and decoding of models. User contains a property for all the acronyms. Normally Codable would require you to provide all the acronyms to create a user from JSON. When creating an acronym, you would have to instantiate the array as well. @Children allows you to have the best of both worlds — a property to represent all the children without having to specify it to create the model.

打开 Userscontroller.swift. 和 add a new route handler after getHandler(_:):

// 1
func getAcronymsHandler(_ req: Request) 
  -> EventLoopFuture<[Acronym]> {
  // 2
  User.find(req.parameters.get("用户身份"), on: req.db)
    .unwrap(or: Abort(.notFound))
    .flatMap { user in
      // 3
      user.$acronyms.get(on: req.db)
    }
}

这是此路由处理程序所做的:

  1. Define a new route handler, getAcronymsHandler(_:), that returns EventLoopFuture<[Acronym]>.
  2. 获取请求参数中指定的用户,并勾勒出退回的将来。
  3. 使用上面创建的新属性包装器使用流畅的查询获取首字母缩略词以返回所有首字母缩略词。请记住,这使用属性包装器的预计值, 不是 the wrapped value.

Register the route handler at the end of boot(routes:):

usersRoute.get(
  ":userID", 
  "acronyms", 
  use: getAcronymsHandler)

这将HTTP GET请求连接到 / api /用户/<USER ID>/acronyms to getAcronymsHandler(_:).

构建并运行应用程序,然后在休息中创建新请求。配置请求,如下所示:

发送请求,您将看到响应返回用户的首字母缩略词:

外国钥匙约束

外国钥匙约束 描述两个表之间的链接。它们经常用于验证。目前,用户表与数据库中的缩写表之间没有链接。流利的是唯一一个了解链接的东西。

使用外国密钥约束具有许多福利:

  • 它确保您无法使用不存在的用户创建首字母缩略词。
  • 在删除所有首字母术之前,您无法删除用户。
  • 在删除缩写表之前,无法删除用户表。

在迁移中设置外键约束。打开 createacronmw.swift., and replace .field("用户身份", .uuid, .required) with the following:

.field("用户身份", .uuid, .required, .references("users", "id"))

This is the same as before but also adds a reference from the 用户身份 column to the id column in the Users table.

Finally, because you’re linking the acronym’s 用户身份 property to the User table, you must create the User table first. In configure.swift., move the User migration to before the Acronym migration:

app.migrations.add(CreateUser())
app.migrations.add(CreateAcronym())

这可确保流畅的表格以正确的顺序创建表。

在Xcode中停止应用程序,然后从早期的步骤遵循 删除数据库.

构建并运行应用程序,然后在休息中创建新请求。配置请求,如下所示:

使用名称和值添加三个参数:

  • 短的: 我的天啊
  • : 我的天啊
  • 用户身份:E92B49F2-F239-41B4-B26D-85817F0363AB

这是一个有效的UUID字符串,但由于数据库为空,因此不会引用任何用户。发送请求;你会收到一个错误说出外国钥匙约束违规:

创建用户,按照您之前进行并复制ID。再次使用有效ID发送创建首字母缩略词请求。应用程序创建缩写,没有任何错误。

然后去哪儿?

在本章中,您学习了如何使用流利的蒸气中实现父子关系。这允许您开始在数据库中的模型之间创建复杂的关系。下一章涵盖数据库中的其他类型的关系:兄弟关系。

有一个技术问题?想报告一个错误吗? 您可以向官方书籍论坛中的书籍作者提出问题和报告错误 这里.

有反馈分享在线阅读体验吗? 如果您有关于UI,UX,突出显示或我们在线阅读器的其他功能的反馈,您可以将其发送到设计团队,其中表格如下所示:

© 2021 Razeware LLC