【问题标题】:Many-to-many relationship where one entity has multiple properties of the same type一个实体具有多个相同类型的属性的多对多关系
【发布时间】:2021-09-12 02:10:10
【问题描述】:

我不知道如何表达这一点,这可能是我找不到任何信息的原因,但我有一个多对多关系,其中一个实体具有许多其他实体的多个属性。

例如,采用ArtistSong 的关系。一个艺术家可以有很多歌曲,而一首歌可以有很多艺术家。但是,一首歌可以有(许多)主要艺术家、(许多)特色艺术家和(许多)专辑艺术家。所以这些都来自同一张表,但我不确定如何建模。

在代码中,我在 Swift 中使用 GRDB,所以我关注 the docs

import GRDB
    
struct Artist: TableRecord {
    let id: Int
    let name: String
    ...
    
    static let artistSong = hasMany(ArtistSong.self)
    static let songs = hasMany(Song.self, through: artistSong, using: ArtistSong.song)
}

struct ArtistSong: TableRecord {
    static let artist = belongsTo(Artist.self)
    static let song = belongsTo(Song.self)
}

struct Song: TableRecord {
    let id: Int
    let name: String
    ...
    
    static let artistSong = hasMany(ArtistSong.self)
    static let artists = hasMany(Artist.self, through: artistSong, using: ArtistSong.artist)
}

我想如果一首歌中只有“艺术家”的话,这会很好。但是我有一首歌曲有 3 种不同类型的艺术家(主要、精选、专辑艺术家),但它们都来自同一个 Artist 表。

解决这个问题的最佳方法是什么?

【问题讨论】:

    标签: swift database database-design many-to-many grdb


    【解决方案1】:

    但我有一首歌曲有 3 种不同类型的艺术家(主要、精选、专辑艺术家),但它们都来自同一个 Artist 表。

    解决这个问题的最佳方法是什么?

    您可以过滤Song.artists 关系。您还可以定义过滤关联:

    extension Song {
      static let artists = hasMany(...)
      static let mainArtists = artists
        .filter(...) // Column("kind") == "main", maybe
        .forKey("mainArtists")
    }
    

    在上面的例子中,我改变了mainArtists关联的键,这样:

    • 如果需要,您可以在同一个请求中同时使用 artistsmainArtists 关联。
    • 例如,当您从 include(all: Song.mainArtists) 的请求中获取时,mainArtists 关联在复合记录类型的 mainArtists 属性中被解码。

    【讨论】:

      【解决方案2】:

      对于多对多关系,您需要additional tables 来定义关系。除了歌曲和艺术家表之外,您还需要额外的表来定义主要、精选、专辑艺术家关系。

      这些是join table,其中包含来自两个或多个其他表的公共字段。通过这种方式,它在数据之间创建了多对多的关系。

      您可以只保留一个额外的表格,其中有一列定义歌曲和艺术家之间的关系。但这会造成冗余和一些异常情况。您需要Normalization。并且需要有额外的表格。

      规范化是最小化关系冗余的过程 或一组关系。

      注意:在多对多关系中至少需要三个表。 如果我们尝试合并它将创建冗余数据。

      【讨论】:

        【解决方案3】:

        我也对此感兴趣,因为这是 HasManyThrough 关联和在类图数据结构中实现边的一个很好的例子。我以为你已经想到了这个架构:

        我首先尝试使用 Gwendal Roué 的 answer 解决它,但并没有完全发挥作用。我打开了一个 GRDB issue 寻求更多帮助。他在几个小时内回答,解决了我所有的问题并详细解释了解决方案。

        这是我学到的:

        • 区分 has-many- 和 has-many-through-associations are necessary 以确保请求独立处理它们。否则,例如,将无法使用相同的请求获取主要艺术家和特色艺术家。如果没有不同的关联,请求将组合过滤器并仅返回使用相同SongArtist 定义的mainfeature 的艺术家(这是不可能的,因此返回空数组)。
          • 这就是我的代码为每个ArtistRelation 定义关联的原因。我的 Song 结构有五个 has-many- 和另外五个 has-many-through-associations。
        • 在处理关闭表的连接请求it is better to include the entire row 时。可以获取仅包含隔离列的结构(例如,当我们只对relation 感兴趣时),但这会使代码更加复杂。 Gwendal Roué 不建议这样做。
          • 这就是为什么我在代码示例6 中的结构包含整个SongArtist 行,尽管我只需要它的relation

        以下代码包含一个完整的场景和这些获取请求以测试实现:

        1. 所有歌曲和相应的艺术家,但没有ArtistRelation 信息
        2. 所有歌曲和相应的艺术家,根据他们的ArtistRelation 分组在单独的数组中
        3. 所有歌曲及其关系数
        4. 所有没有特色艺术家的歌曲
        5. 直接访问关闭表以获取定义特色艺术家的所有 SongArtist 行
        6. 所有歌曲与他们的艺术家和他们的关系
        import GRDB
        
        struct Artist: Codable, Hashable, FetchableRecord, MutablePersistableRecord {
            mutating func didInsert(with rowID: Int64, for column: String?) { id = rowID }
            var id: Int64?
            var name: String
            static let songArtists = hasMany(SongArtist.self)
            static let songs = hasMany(Song.self, through: songArtists, using: SongArtist.song)
        }
        
        struct Song: Codable, Hashable, FetchableRecord, MutablePersistableRecord {
            mutating func didInsert(with rowID: Int64, for column: String?) { id = rowID }
            var id: Int64?
            var name: String
            
            // Distinct has-many-associations are necessary to make sure requests treat them independently. See https://github.com/groue/GRDB.swift/issues/1068#issuecomment-927801968 for more information.
            static let songArtists = hasMany(SongArtist.self)
            static func songArtists(forRelation relation: ArtistRelation) -> HasManyAssociation<Song, SongArtist> {
                songArtists
                    .filter(Column("relation") == relation)
                    .forKey("\(relation.rawValue)SongArtists")
            }
            static let albumSongArtists = songArtists(forRelation: .album)
            static let featureSongArtists = songArtists(forRelation: .feature)
            static let mainSongArtists = songArtists(forRelation: .main)
            static let partnerSongArtists = songArtists(forRelation: .partner)
            
            // Distinct has-many-through-associations are necessary to make sure requests treat them independently. See https://github.com/groue/GRDB.swift/issues/1068#issuecomment-927801968 for more information.
            static let artists = hasMany(Artist.self, through: songArtists, using: SongArtist.artist)
            static func artists(forRelation relation: ArtistRelation) -> HasManyThroughAssociation<Song, Artist> {
                hasMany(
                    Artist.self,
                    through: songArtists(forRelation: relation),
                    using: SongArtist.artist)
                    .forKey("\(relation.rawValue)Artists")
            }
            static let albumArtists = artists(forRelation: .album)
            static let featureArtists = artists(forRelation: .feature)
            static let mainArtists = artists(forRelation: .main)
            static let partnerArtists = artists(forRelation: .partner)
        }
        
        enum ArtistRelation: String, Codable, DatabaseValueConvertible {
            case album
            case feature
            case main
            case partner
        }
        
        struct SongArtist: Codable, Hashable, FetchableRecord, PersistableRecord {
            let songId: Int64
            let artistId: Int64
            let relation: ArtistRelation
            static let song = belongsTo(Song.self)
            static let artist = belongsTo(Artist.self)
        }
        
        let queue = DatabaseQueue()
        try queue.write { db in
            
            try db.create(table: "artist") { t in
                t.autoIncrementedPrimaryKey("id")
                t.column("name", .text).notNull()
            }
            try db.create(table: "song") { t in
                t.autoIncrementedPrimaryKey("id")
                t.column("name", .text).notNull()
            }
            try db.create(table: "songArtist") { t in
                t.column("songId", .integer).notNull().indexed().references("song")
                t.column("artistId", .integer).notNull().indexed().references("artist")
                t.column("relation").notNull()
                // We do not define primary keys here using `t.primaryKey(["songId", "artistId"])` because we allow multiple `SongArtist` rows with the same id combination, e.g. when the album artist is also the main artist of a song. See https://github.com/groue/GRDB.swift/issues/1063#issuecomment-925735039 for an example that defines primary keys for a closure table.
            }
            
            // Testing real song data from https://music.apple.com/de/album/magnet/1102347168
            
            var missK8 = Artist(name: "Miss K8")
            try missK8.insert(db)
            var mcNolz = Artist(name: "McNolz")
            try mcNolz.insert(db)
            var radicalRedemption = Artist(name: "Radical Redemption")
            try radicalRedemption.insert(db)
            
            var scream = Song(name: "Scream (feat. Mc Nolz)")
            try scream.insert(db)
            try SongArtist(songId: scream.id!, artistId: missK8.id!, relation: .album).insert(db)
            try SongArtist(songId: scream.id!, artistId: mcNolz.id!, relation: .feature).insert(db)
            try SongArtist(songId: scream.id!, artistId: radicalRedemption.id!, relation: .main).insert(db)
            try SongArtist(songId: scream.id!, artistId: missK8.id!, relation: .partner).insert(db)
            
            var raidersOfRampage = Song(name: "Raiders of Rampage")
            try raidersOfRampage.insert(db)
            try SongArtist(songId: raidersOfRampage.id!, artistId: missK8.id!, relation: .album).insert(db)
            try SongArtist(songId: raidersOfRampage.id!, artistId: missK8.id!, relation: .main).insert(db)
            try SongArtist(songId: raidersOfRampage.id!, artistId: mcNolz.id!, relation: .partner).insert(db)
        }
        
        // 1: All songs and the corresponding artists, but without `ArtistRelation` info
        try queue.read { db in
            struct SongInfo: FetchableRecord, Decodable, CustomStringConvertible {
                var song: Song
                var artists: Set<Artist>
                var description: String { "\(song.name) → artists:[\(artists.map(\.name).joined(separator: ", "))]" }
            }
            let request = Song.including(all: Song.artists)
            let result = try SongInfo.fetchAll(db, request)
            print("1: \(result)")
            // > 1: [Scream (feat. Mc Nolz) → artists:[Radical Redemption, McNolz, Miss K8], Raiders of Rampage → artists:[Miss K8, McNolz]]
        }
        
        // 2: All songs and the corresponding artists, grouped in separate arrays according to their `ArtistRelation`
        try queue.read { db in
            struct SongInfo: FetchableRecord, Decodable, CustomStringConvertible {
                var song: Song
                var albumArtists: Set<Artist>
                var featureArtists: Set<Artist>
                var mainArtists: Set<Artist>
                var partnerArtists: Set<Artist>
                var description: String { "\(song.name) → albumArtists:\(albumArtists.map(\.name)), featureArtists:\(featureArtists.map(\.name)), mainArtists:\(mainArtists.map(\.name)), partnerArtists:\(partnerArtists.map(\.name))" }
            }
            let request = Song
                .including(all: Song.albumArtists)
                .including(all: Song.featureArtists)
                .including(all: Song.mainArtists)
                .including(all: Song.partnerArtists)
            let result = try SongInfo.fetchAll(db, request)
            print("2: \(result)")
            // > 2: [Scream (feat. Mc Nolz) → albumArtists:["Miss K8"], featureArtists:["McNolz"], mainArtists:["Radical Redemption"], partnerArtists:["Miss K8"], Raiders of Rampage → albumArtists:["Miss K8"], featureArtists:[], mainArtists:["Miss K8"], partnerArtists:["McNolz"]]
        }
        
        // 3: All songs with their number of relationships
        try queue.read { db in
            struct SongInfo: FetchableRecord, Decodable, CustomStringConvertible {
                var song: Song
                var albumSongArtistCount: Int
                var featureSongArtistCount: Int
                var mainSongArtistCount: Int
                var partnerSongArtistCount: Int
                var description: String { "\(song.name) → album:\(albumSongArtistCount), feature:\(featureSongArtistCount), main:\(mainSongArtistCount), partner:\(partnerSongArtistCount)" }
            }
            let result = try Song
                .annotated(with: Song.albumSongArtists.count)
                .annotated(with: Song.featureSongArtists.count)
                .annotated(with: Song.mainSongArtists.count)
                .annotated(with: Song.partnerSongArtists.count)
                .asRequest(of: SongInfo.self)
                .fetchAll(db)
            print("3: \(result)")
            // > 3: [Scream (feat. Mc Nolz) → album:1, feature:1, main:1, partner:1, Raiders of Rampage → album:1, feature:0, main:1, partner:1]
        }
        
        // 4: All songs that have no feature artists
        try queue.read { db in
            let result = try Song
                .having(Song.featureArtists.isEmpty)
                .fetchAll(db)
            print("4: \(result.map(\.name))")
            // > 4: ["Raiders of Rampage"]
        }
        
        // 5: Direct access to the closure table to get all SongArtist rows that define feature artists
        try queue.read { db in
            struct SongArtistInfo: FetchableRecord, Decodable, CustomStringConvertible {
                var song: Song
                var artist: Artist
                var relation: ArtistRelation
                var description: String { "\(song.name) → \(relation):\(artist.name)" }
            }
            let request = SongArtist
                .including(required: SongArtist.song)
                .including(required: SongArtist.artist)
                .filter(Column("relation") == ArtistRelation.feature)
            let result = try SongArtistInfo.fetchAll(db, request)
            print("5: \(result)")
            // > 5: [Scream (feat. Mc Nolz) → feature:McNolz]
        }
        
        // 6: All songs with their artists and their relationships
        try queue.read { db in
            // It is possible to fetch structs that only contain `relation` as an isolated column but that would make the code more complex. It is easier to fetch the entire `SongArtist` row and get the relation from there. See https://github.com/groue/GRDB.swift/issues/1068#issuecomment-927815515 for more information.
            struct SongInfo: Decodable, FetchableRecord, CustomStringConvertible {
                struct ArtistInfo: Decodable, Hashable, CustomStringConvertible {
                    var songArtist: SongArtist
                    var artist: Artist
                    var description: String { "\(songArtist.relation):\(artist.name)" }
                }
                var song: Song
                var artists: Set<ArtistInfo>
                var description: String { "\(song.name) → \(artists)" }
            }
            let result = try Song
                .including(all: Song.songArtists
                            .including(required: SongArtist.artist)
                            .forKey("artists"))
                .asRequest(of: SongInfo.self)
                .fetchAll(db)
            print("6: \(result)")
            // > 6: [Scream (feat. Mc Nolz) → [feature:McNolz, main:Radical Redemption, album:Miss K8, partner:Miss K8], Raiders of Rampage → [album:Miss K8, main:Miss K8, partner:McNolz]]
        }
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2014-08-11
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2011-03-24
          • 1970-01-01
          • 2018-10-05
          • 1970-01-01
          相关资源
          最近更新 更多