Vapor 3、Fluent 3 和多对多关系未按预期工作

Vapor 3, Fluent 3 and Many-to-Many relations not working as expected

刚开始使用 Vapor 3 和 MySQL 数据库,我很难搞清楚关系部分。

到目前为止我已经创建了 2 个模型:MovieActor。 一个 Movie 可以有很多 Actor,一个 Actor 可以有很多 Movie

Movie 型号:

import Vapor
import FluentMySQL

final class Movie: Codable {

    var id: Int?
    var name: String
    var synopsis: String
    var dateReleased: Date
    var totalGrossed: Float

    init(id: Int? = nil, name: String, synopsis: String, dateReleased: Date, totalGrossed: Float) {
        self.id = id
        self.name = name
        self.synopsis = synopsis
        self.dateReleased = dateReleased
        self.totalGrossed = totalGrossed
    }

}

extension Movie {
    var actors: Siblings<Movie, Actor, MovieActor> {
        return siblings()
    }
}

extension Movie: Content {}
extension Movie: Parameter {}
extension Movie: MySQLModel {}

extension Movie: MySQLMigration {
    static func prepare(on conn: MySQLConnection) -> Future<Void> {
        return MySQLDatabase.create(self, on: conn) { builder in
            builder.field(for: \.id, isIdentifier: true)
            builder.field(for: \.name)
            builder.field(for: \.synopsis)
            builder.field(for: \.dateReleased, type: .date)
            builder.field(for: \.totalGrossed, type: .float)
        }
    }
}

Actor 型号:

import Vapor
import FluentMySQL

final class Actor: Codable {

    var id: Int?
    var firstName: String
    var lastName: String
    var fullName: String {
        return firstName + " " + lastName
    }
    var dateOfBirth: Date
    var story: String

    init(id: Int? = nil, firstName: String, lastName: String, dateOfBirth: Date, story: String) {
        self.id = id
        self.firstName = firstName
        self.lastName = lastName
        self.dateOfBirth = dateOfBirth
        self.story = story
    }

}

extension Actor {
    var actors: Siblings<Actor, Movie, MovieActor> {
        return siblings()
    }
}

extension Actor: Content {}
extension Actor: Parameter {}
extension Actor: MySQLModel {}

extension Actor: MySQLMigration {
    static func prepare(on conn: MySQLConnection) -> Future<Void> {
        return MySQLDatabase.create(self, on: conn) { builder in
            builder.field(for: \.id, isIdentifier: true)
            builder.field(for: \.firstName)
            builder.field(for: \.lastName)
            builder.field(for: \.dateOfBirth, type: .date)
            builder.field(for: \.story, type: .text)
        }
    }
}

而且我还创建了一个 MovieActor 模型作为关系的 MySQLPivot

import Vapor
import FluentMySQL

final class MovieActor: MySQLPivot {

    typealias Left = Movie
    typealias Right = Actor

    static var leftIDKey: LeftIDKey = \.movieID
    static var rightIDKey: RightIDKey = \.actorID

    var id: Int?

    var movieID: Int
    var actorID: Int

    init(movieID: Int, actorID: Int) {
        self.movieID = movieID
        self.actorID = actorID
    }

}

extension MovieActor: MySQLMigration {}

并将它们添加到 configure.swift 文件中的迁移部分:

var migrations = MigrationConfig()
migrations.add(model: Movie.self, database: .mysql)
migrations.add(model: Actor.self, database: .mysql)
migrations.add(model: MovieActor.self, database: .mysql)
services.register(migrations)

数据库中的所有表都创建得很好,但调用 get all movies 服务时我没有收到关系。我刚刚收到 Movie 的属性:

final class MoviesController {

    func all(request: Request) throws -> Future<[Movie]> {

        return Movie.query(on: request).all()
    }

}

[
    {
        "id": 1,
        "dateReleased": "2017-11-20T00:00:00Z",
        "totalGrossed": 0,
        "name": "Star Wars: The Last Jedi",
        "synopsis": "Someone with a lightsaber kills another person with a lightsaber"
    },
    {
        "id": 3,
        "dateReleased": "1970-07-20T00:00:00Z",
        "totalGrossed": 0,
        "name": "Star Wars: A New Hope",
        "synopsis": "Someone new has been discovered by the force and he will kill the dark side with his awesome lightsaber and talking skills."
    },
    {
        "id": 4,
        "dateReleased": "2005-12-20T00:00:00Z",
        "totalGrossed": 100000000,
        "name": "Star Wars: Revenge of the Sith",
        "synopsis": "Anakin Skywalker being sliced by Obi-Wan Kenobi in an epic dual of fates"
    }
]

非常感谢您的帮助!非常感谢:)

所以这里的根本问题是 Codable 响应中没有提供计算属性。您需要做的是定义一个新类型 MoviesWithActors 并填充它和 return 。或者提供第二个端点,例如 /movies/1/actors/ 获取特定电影的所有演员。这更适合 REST,但这取决于您的用例,因为您可能不想要额外的请求等

所以我相信您希望这种关系反映在您查询 Movie 模型时 returned 的内容中。例如,您希望 return 为电影编辑这样的内容:

{
    "id": 1,
    "dateReleased": "2017-11-20T00:00:00Z",
    "totalGrossed": 0,
    "name": "Star Wars: The Last Jedi",
    "synopsis": "Someone with a lightsaber kills another person with a lightsaber",
    "actors": [
        "id": 1,
        "firstName": "Leonardo",
        "lastName": "DiCaprio",
        "dateOfBirth": "1974-11-11T00:00:00Z",
        "story": "Couldn't get an Oscar until wrestling a bear for the big screen."
    ]
}

但是,将 Movie 和 Actor 模型连接为兄弟姐妹只是让您能够方便地查询电影中的演员,就好像演员是 Movie 模型的 属性 一样:

movie.actors.query(on: request).all()

returns 上面那行:Future<[Actor]>

对于从 Actor 对象访问电影,反之亦然:

actor.movies.query(on: request).all()

returns 上面那行:未来<[电影]>

如果你想让它 return 电影和它的演员在同一个响应中就像我假设你希望它在上面那样工作,我相信最好的方法是创建一个内容响应结构如下:

struct MovieResponse: Content {

    let movie: Movie
    let actors: [Actor]
}

您的 "all" 函数现在看起来像这样:

func all(_ request: Request) throws -> Future<[MovieResponse]> {

    return Movie.query(on: request).all().flatMap { movies in

        let movieResponseFutures = try movies.map { movie in
            try movie.actors.query(on: request).all().map { actors in
                return MovieResponse(movie: movie, actors: actors)
            }
        }

        return movieResponseFutures.flatten(on: request)
    }
}

此函数查询所有电影,然后遍历每部电影,然后使用 "actors" 兄弟关系查询该电影的演员。这个演员查询 returns 一个 Future<[Actor]> 为它查询演员的每部电影。映射从该关系 return 编辑的内容,以便您可以作为 [Actor] 而不是 Future<[Actor]> 访问演员,然后 return 与电影结合作为 MovieResponse。

这个movieResponseFutures实际上是由一组MovieResponse futures组成的: [未来<[MovieResponse]>]

要将那个 futures 数组变成一个由你使用 flatten(on:) 的数组组成的 future。这个等待等待每个单独的期货完成,然后 return 将它们全部作为一个单一的期货。

如果您真的想要 Movie 对象中的 Actor 数组 json,那么您可以像这样构造 MovieResponse 结构:

struct MovieResponse: Content {

    let id: Int?
    let name: String
    let synopsis: String
    let dateReleased: Date
    let totalGrossed: Float
    let actors: [Actor]

    init(movie: Movie, actors: [Actor]) {
        self.id = movie.id
        self.name = movie.name
        self.synopsis = movie.synopsis
        self.dateReleased = movie.dateReleased
        self.totalGrossed = movie.totalGrossed
        self.actors = actors
    }
}