跳转至

第7章:CRUD数据库操作

第5章,"Fluent与持久化模型",解释了模型的概念以及如何使用Fluent将它们存储在数据库中。本章集中讨论如何与数据库中的模型进行交互。你将学习CRUD操作以及它们与REST APIs的关系。你还会看到如何利用Fluent来对你的模型进行复杂的查询。

Note

本章要求你使用PostgreSQL。按照第5章,"Fluent与持久化模型"的步骤,在Docker中设置PostgreSQL,并配置你的Vapor应用程序。

CRUDREST

CRUD操作--创建、检索、更新、删除--构成了持久性存储的四个基本功能。通过这些,你可以执行你的应用程序所需的大部分操作。你在第5章中实际实现了第一个功能,即创建。

RESTful APIs为客户提供了一种方法来调用你应用程序中的CRUD功能。通常,你有一个用于你的模型的资源URL。对于TIL应用程序,这就是缩写的资源。http://localhost:8080/api/acronyms。然后你在这个资源上定义路由,与适当的HTTP请求方法配对,以执行CRUD操作。比如说。

创建-C

在第5章"Fluent与持久化模型"中,你实现了Acronym的创建路线。你可以继续你的项目,或者打开本章的启动文件夹中的TILApp。回顾一下,你在routes.swift中创建了一个新的路由处理器:

// 1
app.post("api", "acronyms") { 
  req -> EventLoopFuture<Acronym> in
  // 2
  let acronym = try req.content.decode(Acronym.self)
  // 3
  return acronym.save(on: req.db).map { acronym }
}

这是它的作用:

  1. /api/acronyms/注册一个新的路由,接受POST请求并返回EventLoopFuture<Acronym>
  2. 将请求的JSON解码成一个Acronym。这很简单,因为Acronym符合Content的要求。
  3. 使用Fluent保存模型。当保存完成后,你在map(_:)的完成处理程序中返回模型,这将返回一个EventLoopFuture--在本例中是EventLoopFuture<Acronym>

构建并运行该应用程序,然后打开RESTed。对请求进行如下配置:

  • URL: http://localhost:8080/api/acronyms/
  • method: POST
  • Parameter encoding: JSON-encoded

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

  • short: OMG
  • long: Oh My God

发送请求,你会看到包含创建的首字母缩写的响应:

img

检索-R

对于TILApp来说,检索包括两个独立的操作:检索所有的首字母缩写和检索一个单一的、特定的首字母缩写。Fluent使这两个任务变得简单。

检索所有的首字母缩写词

要检索所有的首字母缩写,请创建一个GET请求的路由处理器到/api/acronyms/。打开routes.swift,在routes(_:)后面添加以下内容:

// 1
app.get("api", "acronyms") { 
  req -> EventLoopFuture<[Acronym]> in
  // 2
  Acronym.query(on: req.db).all()
}

这是它的作用:

  1. 注册一个新的路由处理程序,接受一个GET请求,返回EventLoopFuture<[Acronym]>,一个Acronym的未来数组。
  2. 执行查询以获得所有的首字母缩写。

Fluent为模型添加函数,以便能够对其进行查询。你必须给查询一个Database。这几乎总是请求中的数据库,并为查询提供一个连接。all()返回数据库中该类型的所有模型。这等同于SQL查询SELECT * FROM Acronyms;

建立并运行你的应用程序,然后在RESTed中创建一个新的请求。配置该请求如下:

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

发送请求,查看已经在数据库中的首字母缩写:

img

检索单个首字母缩写词

Vapor的参数与Fluent的查询功能集成,可以很容易地通过ID获得首字母缩写。为了获得一个缩写,你需要一个新的路由处理程序。打开routes.swift,在routes(_:)后面添加以下内容:

// 1
app.get("api", "acronyms", ":acronymID") { 
  req -> EventLoopFuture<Acronym> in
  // 2
  Acronym.find(req.parameters.get("acronymID"), on: req.db)
    // 3
    .unwrap(or: Abort(.notFound))
}

这是它的作用:

  1. /api/acronyms/<ID>注册一个路由,以处理一个GET请求。该路由将首字母缩写的id属性作为最后的路径段。这将返回EventLoopFuture<Acronym>
  2. 获取以acronymID为名传入的参数。使用find(_:on:)在数据库中查询具有该ID的Acronym

Note

因为find(_:on:)需要一个UUID作为第一个参数(因为Acronymid类型是UUID),get(_:)推断返回类型为UUID。默认情况下,它返回String。你可以用get(_:as:)指定类型。

  1. find(_:on:)返回EventLoopFuture<Acronym?>,因为数据库中可能不存在该ID的缩写。使用unwrap(or:)来确保返回一个首字母缩写。如果没有找到首字母缩写,unwrap(or:)会返回一个失败的未来,并提供错误。在这种情况下,它返回一个404 Not Found错误。

建立并运行你的应用程序,然后在RESTed中创建一个新的请求。配置该请求如下:

发送请求,你会收到第一个缩写作为回应:

img

更新 - U

RESTful APIs中,对单个资源的更新使用PUT请求,请求数据中包含新的信息。

routes(_:)的末尾添加以下内容来注册一个新的路由处理程序:

// 1
app.put("api", "acronyms", ":acronymID") { 
  req -> EventLoopFuture<Acronym> in
  // 2
  let updatedAcronym = try req.content.decode(Acronym.self)
  return Acronym.find(
    req.parameters.get("acronymID"),
    on: req.db)
    .unwrap(or: Abort(.notFound)).flatMap { acronym in
      acronym.short = updatedAcronym.short
      acronym.long = updatedAcronym.long
      return acronym.save(on: req.db).map {
        acronym
      }
  }
}

这是它的分解步骤:

  1. 注册一个路由,向/api/acronyms/<ID>发出PUT请求,返回EventLoopFuture<Acronym>
  2. Acronym的请求体进行解码,以获得新的细节。
  3. 使用请求URL中的ID获取首字母缩写。如果没有找到提供的ID的首字母缩写,则使用unwrap(or:)返回404 Not Found。这将返回EventLoopFuture<Acronym>,所以使用flatMap(_:)来等待future的完成。
  4. 用新的值更新首字母缩写的属性。
  5. 保存首字母缩写,用map(_:)等待它的完成。一旦保存完成,返回更新的首字母缩写。

建立并运行应用程序,然后使用RESTed创建一个新的首字母缩写。配置请求如下:

  • URL: http://localhost:8080/api/acronyms/
  • method: POST
  • Parameter encoding: JSON-encoded

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

  • short: WTF
  • long: What The Flip

发送请求,你会看到包含创建的首字母缩写的响应:

img

事实证明,WTF的意思其实不是What The Flip,所以它需要更新。把RESTed中的请求改成如下:

  • URL: http://localhost:8080/api/acronyms/

Note: Use the ID from the returned create request.

Note

使用返回的创建请求中的ID

  • method: PUT
  • long: What The Fudge

发送请求。你将在响应中收到更新的缩写:

img

为了确保这一点,在RESTed中发送一个请求,以获得所有的首字母缩写。你会看到更新后的首字母缩写返回:

img

删除 - D

要在RESTful API中删除一个模型,你要向该资源发送一个DELETE请求。在routes(_:)的末尾添加以下内容来创建一个新的路由处理程序:

// 1
app.delete("api", "acronyms", ":acronymID") { 
  req -> EventLoopFuture<HTTPStatus> in
  // 2
  Acronym.find(req.parameters.get("acronymID"), on: req.db)
    .unwrap(or: Abort(.notFound))
    // 3
    .flatMap { acronym in
      // 4
      acronym.delete(on: req.db)
        // 5
        .transform(to: .noContent)
  }
}

这是它的作用:

  1. 注册一个DELETE请求到/api/acronyms/<ID>的路由,返回EventLoopFuture<HTTPStatus>
  2. 像以前一样从请求的参数中提取要删除的首字母缩写。
  3. 使用flatMap(_:)来等待从数据库返回的缩写。
  4. 使用delete(on:)删除该首字母缩写。
  5. 将结果转化为204 No Content响应。这告诉客户端请求已经成功完成,但没有内容可以返回。

建立并运行该应用程序。WTF的缩写有点冒失,所以要删除它。在RESTed中配置一个新的请求,如下所示:

Note

使用前一个请求中的WTF缩写的ID

  • method: DELETE

发送请求;你会收到一个204 No Content的回应。

img

发送请求获得所有的缩写,你会看到WTF的缩写已经不在数据库中。

img

Fluent查询

你已经看到Fluent使基本的CRUD操作变得如此简单。它可以同样容易地执行更强大的查询。

过滤器

搜索功能是应用程序中的一个常见功能。如果你想搜索数据库中的所有首字母缩写,Fluent使之变得简单。确保以下一行代码在routes.swift的顶部:

import Fluent

接下来,在routes(_:)的末尾添加一个新的路由处理程序用于搜索:

// 1
app.get("api", "acronyms", "search") { 
  req -> EventLoopFuture<[Acronym]> in
  // 2
  guard let searchTerm = 
    req.query[String.self, at: "term"] else {
    throw Abort(.badRequest)
  }
  // 3
  return Acronym.query(on: req.db)
    .filter(\.$short == searchTerm)
    .all()
}

下面是搜索缩略语的情况:

  1. 注册一个新的路由处理程序,接受/api/acronyms/searchGET请求,并返回EventLoopFuture<[Acronym]>
  2. URL查询字符串中检索搜索词。如果失败,抛出一个400 Bad Request错误。

Note

URL中的查询字符串允许客户向服务器传递不适合在路径中出现的信息。例如,它们通常被用来定义搜索结果的页码。

  1. 使用filter(_:)找到所有short属性与searchTerm相匹配的缩写。因为这使用了关键路径,编译器可以对属性和过滤条件执行类型安全。这可以防止因指定无效的列名或无效的类型来过滤而引起的运行时问题。Fluent使用属性包装器的预测值,而不是值本身。

在创建模型时,Fluent大量使用了字段的属性封装器。正如Swift文档中所描述的,"属性包装器在管理属性存储方式的代码和定义属性的代码之间增加了一层隔离"。你也可以为属性包装器提供一个预测值。这允许你在属性包装器上公开额外的功能。Fluent使用投影值来提供对关系的键名和查询功能的访问。

在上面的例子中,你提供了属性包装器的预测值来进行过滤,而不是值本身。投射值为Fluent提供了它所需要的来自属性封装器的信息。例如,Fluent在执行过滤器的查询时需要列名。如果你只提供属性,Fluent将没有办法访问这些数据。在接下来的章节中,你会学到更多关于使用属性封装器的知识。

如果你需要一个属性的实际值,你可以使用该属性本身。例如,要读取一个缩写的简短版本,你只需使用acronym.short。在大多数情况下,这很好。然而在某些情况下,这个属性可能没有一个值。你可能想引用一个还没有从数据库中加载的关系。或者,你可能已经加载了记录,但只检索了选定的字段。你会在第9章"父子关系"、第10章"兄弟姐妹关系"和第31章"高级Fluent"中了解到这些不同的用例。

建立并运行你的应用程序,然后在RESTed中创建一个新的请求。配置该请求如下:

  • URL: http://localhost:8080/api/acronyms/search?term=OMG
  • method: GET

发送请求,你会看到返回的OMG首字母缩写及其含义:

img

如果你想搜索多个字段,例如短字段和长字段,你需要改变你的查询。你不能用链式的filter(_:)函数,因为那只能匹配那些shortlong属性相同的首字母词。

相反,你必须使用一个filter group。替换:

return Acronym.query(on: req.db)
  .filter(\.$short == searchTerm)
  .all()

为以下代码:

// 1
return Acronym.query(on: req.db).group(.or) { or in
  // 2
  or.filter(\.$short == searchTerm)
  // 3
  or.filter(\.$long == searchTerm)
// 4
}.all()

以下是这个额外代码的作用:

  1. 使用.or关系创建一个过滤器组。
  2. 为该组添加一个过滤器,以过滤那些short属性与搜索词相匹配的首字母缩写。
  3. 在该组中添加一个过滤器,以过滤long属性符合搜索条件的首字母缩写。
  4. 返回所有的结果。

这将返回所有符合第一个过滤器或第二个过滤器的缩略语。建立并运行应用程序,然后回到RESTed。重新发送上面的请求,你仍然会看到相同的结果。

URL改为http://localhost:8080/api/acronyms/search?term=Oh+My+God并发送请求。你会得到OMG的缩写作为响应:

img

Note

URL中的空格必须以%20+作为URL编码才有效。

第一个结果

有时一个应用程序只需要一个查询的第一个结果。为此创建一个特定的处理程序,以确保数据库只返回一个结果,而不是将所有结果加载到内存中。创建一个新的路由处理程序来返回routes(_:)结尾处的第一个首字母缩写词:

// 1
app.get("api", "acronyms", "first") { 
  req -> EventLoopFuture<Acronym> in
  // 2
  Acronym.query(on: req.db)
    .first()
    .unwrap(or: Abort(.notFound))
}

这是它的作用:

  1. /api/acronyms/first注册一个新的HTTP GET路由,返回EventLoopFuture<Acronym>
  2. 执行查询以获得第一个首字母缩写词。first()返回一个可选项,因为数据库中可能没有首字母缩写词。使用unwrap(or:)来确保首字母缩写存在,或者抛出一个404 Not Found错误。

你也可以将.first()应用于任何查询,如过滤器的结果。

建立并运行应用程序,然后打开RESTed。创建新的首字母缩写与:

  • short: IKR
  • long: I Know Right

现在创建一个新的RESTed请求,配置为:

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

发送请求,你会看到你创建的第一个首字母缩写返回:

img

对结果进行排序

应用程序通常需要在返回查询结果之前对其进行排序。出于这个原因,Fluent提供了一个排序函数。

routes(_:)函数的末尾写一个新的路由处理程序,以返回所有的首字母缩写,按其short属性升序排序:

// 1
app.get("api", "acronyms", "sorted") { 
  req -> EventLoopFuture<[Acronym]> in
  // 2
  Acronym.query(on: req.db)
    .sort(\.$short, .ascending)
    .all()
}

以下是其工作方式:`

  1. /api/acronyms/sorted注册一个新的HTTP GET路由,返回`EventLoopFuture<[Acronym]>'。

  2. Acronym创建一个查询,并使用sort(_:_:)来执行排序。这个函数接收属性包装器对该字段的预测值的关键路径来进行排序。它还接受排序的方向。最后使用all()来返回所有的查询结果。

建立并运行应用程序,然后在RESTed中创建一个新的请求:

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

发送请求,你会看到按字母顺序排列的缩写词的short字属性:

img

接下来去哪?

你现在知道如何使用Fluent来执行不同的CRUD操作和高级查询。在这个阶段,routes.swift已经被本章的所有代码弄得杂乱无章。下一章将探讨如何使用控制器来更好地组织你的代码。