Vapor 3: преобразовать массив объекта Future в массив других объектов Future - PullRequest
2 голосов
/ 21 марта 2019

Я попытался сделать самый простой пример, который мог бы придумать для моей проблемы. У меня есть модель 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 и flatMap, но не могу понять, как преобразовать массив фьючерсов в массив разных фьючерсов.

1 Ответ

2 голосов
/ 21 марта 2019

Логика, которую вы ищете, будет выглядеть так

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)
}

Надеюсь, это поможет вам. В запросе могут быть некоторые ошибки, потому что я написал его без проверки. Вы можете попытаться распечатать необработанный запрос, чтобы скопировать его и выполнить, например, в. Приложение Postico в postgres напрямую, чтобы понять, что не так.

...