Vapor 3:将 Future 对象数组转换为 Future 其他对象数组

Vapor 3: transform array of Future object to an array of Future other objects

我试着举出我能想到的最基本的例子来解决我的问题。我有一个 Course 模型和一个多对多 table 到 User,它还存储了一些额外的属性(下面示例中的 progress)。

import FluentPostgreSQL
import Vapor

final class Course: Codable, PostgreSQLModel {
  var id: Int?
  var name: String
  var teacherId: User.ID

  var teacher: Parent<Course, User> {
    return parent(\.teacherId)
  }

  init(name: String, teacherId: User.ID) {
    self.name = name
    self.teacherId = teacherId
  }
}

struct CourseUser: Pivot, PostgreSQLModel {
  typealias Left = Course
  typealias Right = User

  static var leftIDKey: LeftIDKey = \.courseID
  static var rightIDKey: RightIDKey = \.userID

  var id: Int?
  var courseID: Int
  var userID: UUID
  var progress: Int

  var user: Parent<CourseUser, User> {
    return parent(\.userID)
  }
}

现在,当我 return 一个 Course 对象时,我希望 JSON 输出是这样的:

{
  "id": 1,
  "name": "Course 1",
  "teacher": {"name": "Mr. Teacher"},
  "students": [
    {"user": {"name": "Student 1"}, progress: 10},
    {"user": {"name": "Student 2"}, progress: 60},
  ]
}

而不是我通常会得到的,这是:

{
  "id": 1,
  "name": "Course 1",
  "teacherID": 1,
}

所以我创建了一些额外的模型和一个在它们之间进行转换的函数:

struct PublicCourseData: Content {
  var id: Int?
  let name: String
  let teacher: User
  let students: [Student]?
}

struct Student: Content {
  let user: User
  let progress: Int
}

extension Course {
  func convertToPublicCourseData(req: Request) throws -> Future<PublicCourseData> {
    let teacherQuery = self.teacher.get(on: req)
    let studentsQuery = try CourseUser.query(on: req).filter(\.courseID == self.requireID()).all()

    return map(to: PublicCourseData.self, teacherQuery, studentsQuery) { (teacher, students) in
      return try PublicCourseData(id: self.requireID(),
                                  name: self.name,
                                  teacher: teacher,
                                  students: nil) // <- students is the wrong type!
    }
  }
}

现在,我几乎,但我无法将studentsQueryEventLoopFuture<[CourseUser]>转换为EventLoopFuture<[Student]>。我尝试了 mapflatMap 的多种组合,但我不知道如何将 Futures 数组转换为不同 Futures 数组。

您正在寻找的逻辑将如下所示

extension Course {
    func convertToPublicCourseData(req: Request) throws -> Future<PublicCourseData> {
        return teacher.get(on: req).flatMap { teacher in
            return try CourseUser.query(on: req)
                                 .filter(\.courseID == self.requireID())
                                 .all().flatMap { courseUsers in
                // here we should query a user for each courseUser
                // and only then convert all of them into PublicCourseData
                // but it will execute a lot of queries and it's not a good idea
            }
        }
    }
}

我建议您改用 SwifQL lib 来构建自定义查询以在一个请求中获取所需数据

如果您只想获得一门课程,您可以将 Fluent 的查询与 SwifQL 的查询混合使用,这样您将在 2 个请求中获得它:

struct Student: Content {
    let name: String
    let progress: Int
}

extension Course {
    func convertToPublicCourseData(req: Request) throws -> Future<PublicCourseData> {
        return teacher.get(on: req).flatMap { teacher in
            // we could use SwifQL here to query students in one request
            return SwifQL.select(\CourseUser.progress, \User.name)
                        .from(CourseUser.table)
                        .join(.inner, User.table, on: \CourseUser.userID == \User.id)
                        .execute(on: req, as: .psql)
                        .all(decoding: Student.self).map { students in
                return try PublicCourseData(id: self.requireID(),
                                          name: self.name,
                                          teacher: teacher,
                                          students: students)
            }
        }
    }
}

如果您想在一个请求中获得课程列表,您可以使用纯 SwifQL 查询。

我稍微简化了想要的JSON

{
  "id": 1,
  "name": "Course 1",
  "teacher": {"name": "Mr. Teacher"},
  "students": [
    {"name": "Student 1", progress: 10},
    {"name": "Student 2", progress: 60},
  ]
}

首先让我们创建一个模型,以便能够将查询结果解码到其中

struct CoursePublic: Content {
    let id: Int
    let name: String
    struct Teacher:: Codable {
        let name: String
    }
    let teacher: Teacher
    struct Student:: Codable {
        let name: String
        let progress: Int
    }
    let students: [Student]
}

好的,现在我们已准备好构建自定义查询。让我们在一些请求处理函数中构建它

func getCourses(_ req: Request) throws -> Future<[CoursePublic]> {
    /// create an alias for student
    let s = User.as("student")

    /// build a PostgreSQL's json object for student
    let studentObject = PgJsonObject()
        .field(key: "name", value: s~\.name)
        .field(key: "progress", value: \CourseUser.progress)

    /// Build students subquery
    let studentsSubQuery = SwifQL
        .select(Fn.coalesce(Fn.jsonb_agg(studentObject),
                            PgArray(emptyMode: .dollar) => .jsonb))
        .from(s.table)
        .where(s~\.id == \CourseUser.userID)


    /// Finally build the whole query
    let query = SwifQLSelectBuilder()
        .select(\Course.id, \Course.name)
        .select(Fn.to_jsonb(User.table) => "teacher")
        .select(|studentsSubQuery| => "students")
        .from(User.table)
        .join(.inner, User.table, on: \Course.teacherId == \User.id)
        .join(.leftOuter, CourseUser.table, on: \CourseUser.teacherId == \User.id)
        .build()
    /// this way you could print raw query
    /// to execute it in postgres manually
    /// for debugging purposes (e.g. in Postico app)
    print("raw query: " + query.prepare(.psql).plain)
    /// executes query with postgres dialect
    return query.execute(on: req, as: .psql)
        /// requests an array of results (or use .first if you need only one first row)
        /// You also could decode query results into the custom struct
        .all(decoding: CoursePublic.self)
}

希望对您有所帮助。查询中可能有一些错误,因为我没有检查就写了你可以尝试打印一个原始查询来复制它并执行,例如直接在 postgres 中使用 Postico 应用程序以了解问题所在。