Пара 3: преобразование массива будущего объекта в массив будущих других объектов

#vapor #vapor-fluent

#vapor #vapor-свободно

Вопрос:

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

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

 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 напрямую, чтобы понять, что не так.

Комментарии:

1. Также, если вы находитесь в Discord, вы могли бы найти меня как iMike # 3049, я помогу вам с вашим запросом. После этого я исправлю свой ответ здесь.

2. Наверняка должен быть способ сопоставить EventLoopFuture<[Пользователь курса]> с EventLoopFuture<[Студент]> , не погружаясь в SwifQL и пользовательские запросы? Даже если это может быть наиболее оптимальным решением, на данный момент мне действительно просто интересно, как сопоставить этот один массив с другим, если честно. Я еще не совсем готов также внедрить SwiftQL поверх всего остального, что я изучаю одновременно 🙂

3. затем просто сделайте .map { courseUsers in return courseUsers.map { courseUser in return Student.init(...initialize it here...) } }

4. Да, я пробовал подобные вещи, но это просто не работает : ( Либо я получаю кучу ошибок компилятора, либо я не могу извлечь объект user, который принадлежит CourseUser, поскольку это тоже объект Future, и в этот момент я работаю с обычными объектами, не относящимися к будущему, и т.д. Такой беспорядок! Итак, да, я понимаю, почему использование SwifQL будет лучше, но меня просто бесконечно расстраивает, что это не работает 🙂