#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 будет лучше, но меня просто бесконечно расстраивает, что это не работает 🙂