跳转至

第31章:高级Fluent

在本书的前几节中,你学到了如何使用Fluent来对数据库进行查询。你还学习了如何对模型进行CRUD操作。在这一章中,你将了解到Fluent的一些更高级的功能。你将看到如何用enum保存模型,并使用Fluent的软删除和时间戳功能。你还将学习如何使用原始SQL和连接,以及如何"急于加载"关系。

开始学习

本章的入门项目是基于第21章末尾的TIL应用程序。你可以使用该项目中的代码,也可以使用本章书中的启动项目。这个项目依赖于一个在本地运行的PostgreSQL数据库。

清理现有数据库

如果你在前面的章节中一直跟随,你需要删除现有的数据库。本章包含了模型的改变,需要恢复你的数据库或删除它。在终端,键入:

docker rm -f postgres

如果名为postgresDocker容器正在运行,这将停止它并删除它。

创建一个新的数据库

Docker中创建一个新的数据库供TIL应用程序使用。在终端,键入:

docker run --name postgres \
  -e POSTGRES_DB=vapor_database \
  -e POSTGRES_USER=vapor_username \
  -e POSTGRES_PASSWORD=vapor_password \
  -p 5432:5432 -d postgres

以下是这样做的:

  • 运行一个名为postgres的新容器。
  • 通过环境变量指定数据库的名称、用户名和密码。
  • 允许应用程序连接到PostgreSQL服务器的默认端口:5432.
  • 在后台作为一个守护程序运行服务器。
  • 为这个容器使用名为postgresDocker镜像。如果你的机器上没有这个镜像,Docker会自动下载它。

关于如何在项目中配置数据库的更多信息,见第6章,"配置数据库"。

软删除

在第7章,"CRUD数据库操作"中,你学会了如何从数据库中删除模型。然而,虽然你可能希望模型对用户来说是被删除的,但你可能不希望实际删除它们。你也可以有法律或公司的要求,强制保留数据。Fluent提供了软删除功能,允许你这样做。在Xcode中打开TIL应用程序,进入User.swift。寻找:

var acronyms: [Acronym]

并在下面添加以下定义:

@Timestamp(key: "deleted_at", on: .delete)
var deletedAt: Date?

这为Fluent增加了一个新的属性,用于存储你对模型进行软删除的日期。你用@Timestamp来注解这个属性。当你调用delete(on:)时,Fluent会检查这个属性的封装器。如果.delete动作的属性存在,Fluent会在该属性上设置当前日期并保存更新的模型。否则,它将从数据库中删除该模型。这就是在Fluent中实现软删除所需要的全部内容!

接下来,打开CreateUser.swift。在prepare(on:)中,在.unique(on: "username")前添加:

.field("deleted_at", .datetime)

这就为迁移添加了一个字段,这样Fluent就为新属性创建了正确的列。

打开UsersController.swift,创建一个路由来使用新功能。在loginHandler(_:)下面,添加以下内容:

func deleteHandler(_ req: Request) 
  -> EventLoopFuture<HTTPStatus> {
    User.find(req.parameters.get("userID"), on: req.db)
      .unwrap(or: Abort(.notFound)).flatMap { user in
        user.delete(on: req.db).transform(to: .noContent)
    }
}

这将删除作为参数传递的用户,并返回一个204 No Content的响应。最后,你需要注册这个路由。在boot(routes:)的末尾添加以下内容:

tokenAuthGroup.delete(":userID", use: deleteHandler)

这就把对/api/users/<user_ID>DELETE请求路由到deleteHandler(_:)。建立并运行Vapor应用程序。在RESTed中,使用预先定义的admin用户,用正确的HTTP基本认证凭证向http://localhost:8080/api/users/login发送请求,以获得一个令牌。请参阅第18章,"API认证,第一部分",以了解如何做到这一点的复习情况。

接下来,创建一个新的请求,并进行如下配置:

  • URL: http://localhost:8080/api/users
  • method: POST
  • Parameter encoding: JSON-encoded
  • header: Authorization: Bearer

添加三个带有名称和值的参数:

  • username: a username of your choice
  • name: a name of your choice
  • password: a password of your choice

点击Send Request。这将在应用程序中创建一个用户:

img

接下来,发送一个删除新用户的请求。配置该请求如下:

  • URL: http://localhost:8080/api/users/
  • method: DELETE
  • header: Authorization: Bearer

点击Send Request。你应该看到一个204 No Content的响应,表明你成功地对用户进行了软删除。最后,配置一个请求来获取所有的用户:

  • URL: http://localhost:8080/api/users/
  • method: GET

点击Send Request 。你会注意到,尽管你只是软删除了该用户,但它并没有出现在所有用户的列表中:

img

恢复用户

尽管现在应用程序允许你软删除用户,但你可能想在未来的某一天恢复它们。首先,在UsersController.swift的顶部import Vapor下面添加以下内容:

import Fluent

这样你就可以使用Fluent的过滤函数。接下来,在deleteHandler(_:)下面创建一个新的路由处理程序来恢复一个用户:

func restoreHandler(_ req: Request) 
  throws -> EventLoopFuture<HTTPStatus> {
    // 1
    let userID = 
      try req.parameters.require("userID", as: UUID.self)
    // 2
    return User.query(on: req.db)
      .withDeleted()
      .filter(\.$id == userID)
      .first()
      .unwrap(or: Abort(.notFound))
      .flatMap { user in
        // 3
        user.restore(on: req.db).transform(to: .ok)
    }
}

以下是发生的情况:

  1. 从请求的参数中获取用户的ID作为UUID
  2. 执行一个查询,找到具有该ID的用户。withDeleted()告诉Fluent包括软删除的模型。
  3. 对用户调用restore(on:)来恢复该用户。将响应转换为200 OK

最后,注册路由处理程序。在boot(route:)的末尾添加以下内容:

tokenAuthGroup.post(":userID", "restore", use: restoreHandler)

这将一个POST请求映射到/api/users/**<**UID>/restorerestoreHandler(_:)。建立并运行应用程序,打开RESTed。配置一个请求,如下所示,使用你上面删除的用户的UUID

  • URL: http://localhost:8080/api/users//restore
  • method: POST
  • header: Authorization: Bearer

点击Send Request。你会收到一个200 OK的响应,表明你已经恢复了用户。

img

如果你不再有用户的UUID,你可以在终端中使用以下魔法来检索它:

docker exec -it postgres psql -U vapor_username vapor_database
select id from "users" where username = ’<your username>’;
\q

配置一个最终请求,如下所示:

  • URL: http://localhost:8080/api/users/
  • method: GET

点击Send Request。被恢复的用户现在出现在用户列表中:

img

强制删除

现在你可以软删除和恢复用户,你可能想增加正确删除用户的能力。你可以使用强制删除来实现这个目的。回到Xcode中,仍然在UsersController.swift中,创建一个新的路由来完成这个任务。在restoreHandler(_:)下面添加以下内容:

func forceDeleteHandler(_ req: Request) 
  -> EventLoopFuture<HTTPStatus> {
    User.find(req.parameters.get("userID"), on: req.db)
      .unwrap(or: Abort(.notFound))
      .flatMap { user in
        user.delete(force: true, on: req.db)
          .transform(to: .noContent)
  }
}

你的代码与deleteHandler(_:)相似。然而,这一次你在模型上调用delete(force:on:)。将force设置为true可以绕过软删除,将模型从数据库中删除。

最后,注册路由处理程序。在boot(routes:)的末尾添加以下内容:

tokenAuthGroup.delete(
  ":userID", 
  "force", 
  use: forceDeleteHandler)

这将一个DELETE请求路由到/api/users/<user_ID>/forceforceDeleteHandler(_:)。构建并运行应用程序,回到RESTed。配置一个新的请求,如下所示:

  • URL: http://localhost:8080/api/users//force
  • method: DELETE
  • header: Authorization: Bearer

点击Send Request,你会收到一个204 No Content的回应。配置一个最后的请求如下:

  • URL: http://localhost:8080/api/users//restore
  • method: POST
  • header: Authorization: Bearer

点击Send Request。你会收到一个404 Not Found的错误,因为模型在要恢复的数据库中已不存在:

img

时间戳

Fluent内置了对模型的创建时间和更新时间的时间戳功能。事实上,你在上面使用了一个来实现软删除功能。如果你配置了这些,Fluent会自动设置和更新这些时间。要启用这一点,在Xcode中打开Acronym.swift。在var categories: [Category]为日期添加两个新属性:

@Timestamp(key: "created_at", on: .create)
var createdAt: Date?

@Timestamp(key: "updated_at", on: .update)
var updatedAt: Date?

就像软删除一样,Fluent在创建和更新模型时寻找这些时间戳。如果它们存在,Fluent就会设置这些日期。现在,打开CreateAcronym.swift。在prepare(on:)中,在.create()之前添加以下内容:

.field("created_at", .datetime)
.field("updated_at", .datetime)

这样就把这两个新字段添加到了迁移中,这样Fluent就在数据库中创建了这些列。这就是所需的全部内容! 创建一个新的路由处理程序来使用该功能。

打开AcronymsController.swift,在removeCategoriesHandler(_:)下面添加以下内容:

func getMostRecentAcronyms(_ req: Request) 
  -> EventLoopFuture<[Acronym]> {
    Acronym.query(on: req.db)
      .sort(\.$updatedAt, .descending)
      .all()
}

这条路由返回所有缩写,按updatedAt排序。该排序使用降序,以确保最新的出现在前面。关于如何使用sort(_:)的更多信息,请参阅第7章,"CRUD数据库操作"。当你创建模型时,Fluent会设置createdAt。当你创建模型时,Fluent也会设置updatedAt,并在你更新模型时设置。在boot(routes:)下面的acronymsRoutes.get(":acronymID", "categories", use: getCategoriesHandler)中注册这个路由,内容如下:

acronymsRoutes.get("mostRecent", use: getMostRecentAcronyms)

这将一个GET请求路由到/api/acronyms/mostRecentgetMostRecentAcronyms(_:)。在你运行应用程序之前,你必须更新或重置数据库,为Acronym添加新字段。为了节省时间,本章将重置Docker数据库。要使用迁移来改变表,请参阅第27章,"数据库/API的版本和迁移"。在终端,运行以下命令:

docker rm -f postgres
docker run --name postgres \
  -e POSTGRES_DB=vapor_database \
  -e POSTGRES_USER=vapor_username \
  -e POSTGRES_PASSWORD=vapor_password \
  -p 5432:5432 -d postgres

这些命令在Docker中停止、删除和重新创建PostgreSQL数据库,如本章开头所述。最后,构建并运行应用程序,打开RESTed。创建一些缩写,记住你需要先登录,如第18章"API认证,第1部分"中所述。

Tips

你可能会发现使用Web界面来添加缩略语更简单,在你的浏览器中访问http://localhost:8080

接下来,在RESTed中配置一个新的请求,如下所示:

  • URL: http://localhost:8080/api/acronyms/
  • method: PUT
  • header: Authorization: Bearer

这就更新了创建的第一个缩写。添加两个有名称和值的参数:

  • short:与原首字母缩写相同的短语,例如:OMG
  • long:缩写的最新含义,例如:Oh My Gosh

点击Send Request来更新缩写。最后,在RESTed中配置一个新的请求,如下:

  • URL: http://localhost:8080/api/acronyms/mostRecent
  • method: GET

点击Send Request,获得所有缩略语的列表,按最近更新排序。你会看到第一个缩略语出现在列表的首位,因为你最后一次更新了它:

img

枚举

对数据库列的一个常见要求是将值限制在一个预定义的集合中。FluentPostgreSQLFluentMySQL都支持enum的功能。为了证明这一点,你将为用户添加一个类型来定义基本的用户访问级别。在Xcode中,在Sources/App/Models创建一个名为UserType.swift的新文件。打开这个新文件并添加以下内容:

import Foundation

// 1
enum UserType: String, Codable {
  // 2
  case admin
  case standard
  case restricted
}

以下是新代码的作用:

  1. 创建一个新的String枚举类型,UserType,符合Codable。该类型必须是一个String枚举,以符合Codable
  2. 定义三种类型的用户访问,在Vapor应用程序中使用。

接下来,打开User.swift,在下面添加一个新属性var deletedAt: Date?来存储用户的类型:

@Enum(key: "userType")
var userType: UserType

这为User增加了一个新的属性。你用@Enum来注解这个属性。这是一种特殊的Field属性包装器,用于存储本地数据库的enum。改变初始化器以支持新属性:

init(
  id: UUID? = nil,
  name: String,
  username: String,
  password: String,
  userType: UserType = .standard
) {
  self.name = name
  self.username = username
  self.password = password
  self.userType = userType
}

这样就把新创建的用户类型默认为标准用户。最后,在CreateAdminUser.swift中,将let user = User(...)改为如下:

let user = User(
  name: "Admin", 
  username: "admin", 
  password: passwordHash, 
  userType: .admin)

这使得管理员用户成为admin类型。然后,打开CreateUser.swift。将prepare(on:)的主体替换为以下内容:

// 1
database.enum("userType")
  // 2
  .case("admin")
  .case("standard")
  .case("restricted")
  // 3
  .create()
  .flatMap { userType in
    database.schema("users")
      .id()
      .field("name", .string, .required)
      .field("username", .string, .required)
      .field("password", .string, .required)
      .field("deleted_at", .datetime)
      // 4
      .field("userType", userType, .required)
      .unique(on: "username")
      .create()
}

以下是新代码的作用:

  1. 使用enum(_:)设置一个数据库枚举。这类似于使用schema(_:_)来设置一个表。
  2. 为你的枚举定义不同的情况。
  3. 调用create()来在数据库中创建枚举。使用flatMap(_:)等待创建完成。flatMap(_:)的闭包接收创建的枚举类型。
  4. 使用枚举类型在users表中为新属性定义一个新字段。

接下来,打开UsersController.swift来使用这个新属性。将deleteHandler(_:)的函数签名替换为以下内容:

func deleteHandler(_ req: Request) 
  throws -> EventLoopFuture<HTTPStatus> {

这允许你在函数体中抛出错误。接下来,将deleteHandler(_:)的主体替换为以下内容:

// 1
let requestUser = try req.auth.require(User.self)
// 2
guard requestUser.userType == .admin else {
  throw Abort(.forbidden)
}
// 3
return User.find(req.parameters.get("userID"), on: req.db)
  .unwrap(or: Abort(.notFound))
  .flatMap { user in
    user.delete(on: req.db)
      .transform(to: .noContent)
}

所做的改变是:

  1. 从请求中获取认证的用户。
  2. 确保认证的用户是一个管理员。这确保只有管理员可以删除其他用户。否则,抛出一个403 Forbidden响应。
  3. 像以前一样,删除请求参数中指定的用户。

使用前面的命令重置数据库,然后建立并运行应用程序。打开RESTed,以管理员用户身份登录,获得一个令牌。配置一个新的请求,如下所示:

  • URL: http://localhost:8080/api/users
  • method: POST
  • Parameter encoding: JSON-encoded
  • header: Authorization: Bearer

添加四个带有名称和值的参数:

  • username: a username of your choice
  • name: a name of your choice
  • password: a password of your choice
  • userType: standard

点击Send Request来创建用户。改变数值以创建另一个要删除的用户,并点击Send Request。记下第二个用户的ID。以你创建的第一个用户的身份登录,并按如下方式配置另一个请求:

  • URL: http://localhost:8080/api/users/
  • method: DELETE
  • Parameter encoding: JSON-encoded
  • header: Authorization: Bearer

点击Send Request,你会收到一个403 Forbidden的回应:

img

改变Authorization标题,使用来自管理用户的令牌,并再次点击Send Request。这次请求成功了,你会收到一个204 No Content的响应:

img

Note

为了更加完整,你应该对forceDeleteHandler(_:)restoreHandler(_:)做同样的修改。这是给读者留下的一个练习。

生命周期钩子

Fluent允许你使用模型中间件来钩住模型生命周期的各个环节。这些中间件的工作方式与其他中间件类似,允许你在不同事件前后执行代码。关于中间件的更多信息,请参见第29章,"中间件"。Fluent允许你为以下事件添加中间件。

  • create:当Fluent创建一个模型时被调用。
  • update:当Fluent更新一个模型时被调用。
  • delete:当Fluent删除一个模型时被调用。
  • softDelete:当Fluent软删除一个模型时被调用。
  • restore:当Fluent恢复一个模型时被调用。

这些钩子允许你为你的模型添加额外的检查,填充或删除字段或添加额外的步骤,如日志信息。为了证明这一点,在Sources/App/Models中创建一个新文件,名为UserMiddleware.swift。打开新文件并添加以下内容:

import Fluent
import Vapor

// 1
struct UserMiddleware: ModelMiddleware {
  // 2
  func create(
    model: User, 
    on db: Database, 
    next: AnyModelResponder) -> EventLoopFuture<Void> {
    // 3
    User.query(on: db)
      .filter(\.$username == model.username)
      .count()
      .flatMap { count in
        // 4
        guard count == 0 else {
          let error = 
            Abort(
              .badRequest, 
              reason: "Username already exists")
          return db.eventLoop.future(error: error)
        }
        // 5
        return next.create(model, on: db).map {
          // 6
          let errorMessage: Logger.Message = 
            "Created user with username \(model.username)"
          db.logger.debug(errorMessage)
        }
    }
  }
}

以下是新代码的作用:

  1. 创建一个符合ModelMiddleware的新类型。
  2. 实现create(model:on:next:),在创建用户之前进行额外检查。
  3. 查询数据库以获得具有新用户的用户名的用户数量。
  4. 确保没有具有该用户名的用户,否则返回一个失败的未来,并给出AbortError和原因。这将向客户端返回一个比数据库约束违反信息更好的错误信息。返回一个失败的未来将取消保存。你仍然应该使用数据库约束来断言一个用户名是唯一的,以防两个用户在同一时间试图用相同的用户名注册。
  5. 将下一个响应者链起来,以允许其他中间件运行。
  6. 保存完成后,向控制台记录一条信息。你可以在Fluent保存模型后在此运行额外的代码。

使用ModelMiddleware来验证唯一的用户名是很有用的,因为你只需要在一个地方做这件事。TIL应用程序包含两个创建用户的地方 - API和网站。通过使用ModelMiddleware,你不需要重复的逻辑来确保用户名是唯一的。

最后,打开configure.swift来注册这个中间件。在app.migrations.add(CreateAdminUser())下面添加以下内容:

app.databases.middleware.use(UserMiddleware(), on: .psql)

这将UserMiddleware注册到psql,以确保每当你创建一个User时它都会运行。

构建并运行应用程序,如果你还没有一个令牌的话,请登录以获得一个令牌。在RESTed中,配置一个新的请求,如下所示:

  • URL: http://localhost:8080/api/users
  • method: POST
  • Parameter encoding: JSON-encoded
  • header: Authorization: Bearer

添加四个带有名称和值的参数:

  • username: admin
  • name: Admin
  • password: password
  • userType: admin

点击Send Request,你会看到返回的错误信息,因为管理员的用户名已经存在:

img

急于加载和嵌套模型

如果你遵循一个严格的REST API,你应该在一个单独的请求中检索一个模型的子代。然而,这并不总是理想的,你可能希望能够发送一个单一的请求来获取所有模型及其所有子模型。例如,在TIL应用程序中,你可能想要一个返回所有类别及其所有缩写的路由。你甚至可能想要返回所有类别及其所有用户的首字母缩写。这通常被称为N+1问题,Fluent通过急于加载的方式使其变得简单。打开CategoriesController.swift,在文件的底部添加以下内容:

struct AcronymWithUser: Content {
  let id: UUID?
  let short: String
  let long: String
  let user: User.Public
}

struct CategoryWithAcronyms: Content {
  let id: UUID?
  let name: String
  let acronyms: [AcronymWithUser]
}

这定义了两个新的类型,在返回所有类别及其首字母缩写和首字母缩写的用户时使用。在getAcronymsHandler(_:)下面,添加代码来执行查询:

func getAllCategoriesWithAcronymsAndUsers(_ req: Request) 
  -> EventLoopFuture<[CategoryWithAcronyms]> {
    // 1
    Category.query(on: req.db)
      // 2
      .with(\.$acronyms) { acronyms in
        // 3
        acronyms.with(\.$user)
      // 4
      }.all().map { categories in
        // 5
        categories.map { category in
          // 6
          let categoryAcronyms = category.acronyms.map {
            AcronymWithUser(
              id: $0.id, 
              short: $0.short, 
              long: $0.long, 
              user: $0.user.convertToPublic())
          }
          // 7
          return CategoryWithAcronyms(
            id: category.id, 
            name: category.name, 
            acronyms: categoryAcronyms)
        }
      }
}

以下是新的路径处理程序的作用:

  1. Category进行查询,以获得所有的类别。
  2. 使用with(_:)急于加载类别的首字母缩写。with(_:)接受一个关键路径来急于加载关系 - 在这个例子中,$acronyms
  3. with(_:)也接受一个可选的闭包,允许你嵌套急切的负载。这允许你同时在Acronym上急于加载$userFluent会计算出它需要为你执行的查询。
  4. 使用all()来完成查询并获得所有的结果。
  5. 循环浏览所有返回的类别,将它们转换为CategoryWithAcronyms
  6. 将所有类别的首字母转换成AcronymWithUser。当你急于加载一个模型的关系时,你可以直接访问该属性。你不需要像前几章那样通过属性包装器。请注意。如果你在没有急于加载关系的情况下这样做,你会得到一个致命的错误。
  7. 返回转换为CategoryWithAcronyms的类别。

最后,在boot(routes:)下面注册路由categoriesRoute.get(":categoryID", "acronyms", use: getAcronymsHandler)

categoriesRoute.get(
  "acronyms", 
  use: getAllCategoriesWithAcronymsAndUsers)

/api/categories/acronymsGET请求路由到getAllCategoriesWithAcronymsAndUsers(_:)。建立并运行应用程序,并创建一些用户和首字母缩写和类别。在RESTed中,配置一个新的请求,如下:

  • URL: http://localhost:8080/api/categories/acronyms
  • method: GET

点击Send Request,你会看到所有的类别都有它们的首字母缩写,首字母缩写也有它们的用户:

img

连接

有时,你想在检索信息时查询其他表。例如,你可能想获得创建最近的缩写的用户。你可以通过急于加载和Swift来完成这个任务。你可以通过获取所有的用户并急于加载他们的首字母缩写来做到这一点。然后,你可以按照创建日期对缩写进行排序,得到最新的缩写并返回其用户。然而,这意味着将所有的用户和他们的首字母缩写加载到内存中,即使你不需要它们,这也是低效的。连接允许你通过指定共同值将一个表的列与另一个表的列结合起来。例如,你可以用用户的ID将缩写表和用户表结合起来。然后你可以在不同的表中进行排序,甚至过滤。

打开UsersController.swift,在forceDeleteHandler(_:)下面添加一个路由处理程序,以获得最近创建缩略语的用户:

func getUserWithMostRecentAcronym(_ req: Request) 
  -> EventLoopFuture<User.Public> {
    // 1
    User.query(on: req.db)
      // 2
      .join(Acronym.self, on: \Acronym.$user.$id == \User.$id)
      // 3
      .sort(Acronym.self, \Acronym.$createdAt, .descending)
      // 4
      .first()
      .unwrap(or: Abort(.internalServerError))
      .convertToPublic()
}

以下是新代码的作用:

  1. User进行查询。
  2. 通过连接用户的ID和缩写的用户的$id值,将UserAcronym连接起来。
  3. Acronym进行排序,并对createdAt属性进行排序,以获得最新的首字母缩写。你可以使用排序和过滤器与连接。
  4. 返回第一个用户,如果不存在则返回一个内部服务器错误。数据库应该总是包含至少一个具有管理员身份的用户。注意这只是返回User模型,而不是缩略语。

boot(routes:)下注册路由usersRoute.get(":userID", "acronyms", use: getAcronymsHandler)

usersRoute.get(
  "mostRecentAcronym", 
  use: getUserWithMostRecentAcronym)

这就把对/api/users/mostRecentAcronymGET请求路由到getUserWithMostRecentAcronym(_:)。建立并运行应用程序,并启动RESTed。配置一个新的请求,如下所示:

  • URL: http://localhost:8080/api/users/mostRecentAcronym
  • method: GET

点击Send Request,你会看到创建最近的缩写的用户:

img

原始SQL

虽然Fluent提供的工具可以让你建立很多不同的行为,但有一些高级的功能它并没有提供。Fluent不支持查询不同的模式或聚合函数。在一个复杂的应用程序中,你可能会发现有些情况下Fluent并不能提供你需要的功能。在这种情况下,你可以使用原始的SQL查询来直接与数据库交互。这使得你可以执行数据库支持的任何类型的查询。

AcronymsController.swift中,在文件顶部import Fluent下面添加以下内容:

import SQLKit

这使你可以看到原始查询的必要方法。接下来,在getMostRecentAcronyms(_:)下面,添加:

func getAllAcronymsRaw(_ req: Request) 
  throws -> EventLoopFuture<[Acronym]> {
    // 1
    guard let sql = req.db as? SQLDatabase else {
      throw Abort(.internalServerError)
    }
    // 2
    return sql.raw("SELECT * FROM acronyms")
      // 3
      .all(decoding: Acronym.self)
}

以下是代码的作用:

  1. Request上的数据库转换为SQLDatabase,允许你执行原始查询。如果转换失败,返回500 Internal Server Error
  2. 使用raw(_:)在数据库上创建一个原始查询。

Note

你必须小心谨慎,对查询中的任何输入进行消毒,以避免注入攻击。raw(_:)在必要时支持参数绑定。

  1. 获取所有的结果并将行解码为Acronym。尽管这使用了一个原始查询,你仍然使用Codable来转换数据库中的数据,从而提供了类型安全。

boot(routes:)下面的acronymsRoutes.get("mostRecent", use: getMostRecentAcronyms)中注册新路线,内容如下:

acronymsRoutes.get("raw", use: getAllAcronymsRaw)

这就把一个GET请求路由到/api/acronyms/rawgetAllAcronymsRaw(_:)。建立并运行你的应用程序,并前往RESTed。配置一个最终请求,如下所示:

  • URL: http://localhost:8080/api/acronyms/raw
  • method: GET

点击Send Request,你会看到所有的缩略语返回:

img

接下来去哪?

在本章中,你学到了如何使用Fluent提供的一些高级功能来执行复杂的查询。你还看到了如果Fluent不能满足你的需要,如何发送原始SQL查询。

有了高级功能的知识,你现在应该可以用VaporFluent来构建任何东西了!