2

我试图为我的问题做一个我能想到的最基本的例子。我有一个Course模型和一个多对多表,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)
  }
}

现在,当我返回一个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!
    }
  }
}

现在,我快到,但我无法studentsQuery从转换EventLoopFuture<[CourseUser]>EventLoopFuture<[Student]>. map我尝试了and的多种组合flatMap,但我不知道如何将一组 Futures 转换为一组不同的 Futures。

4

1 回答 1

2

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

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 库来构建自定义查询以在一个请求中获取所需的数据

如果您只想获得一门课程,您可以将 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 应用程序中执行以了解问题所在。

于 2019-03-21T15:08:24.983 回答