1

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

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

在代码中,我在 Swift 中使用 GRDB,因此我遵循以下文档

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 表。

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

4

3 回答 3

1

For Many to Many relationship you need additional tables to define the relationship. Like with addition to Song and Artist tables you need additional table to define main, featured, album artist relationships.

These are join table which contains common fields from two or more other tables. In this way, it creates a many-to-many relationship between data.

You can go ahead and have only one extra table with a column defining relationship between song and artist. But this will create redundancy and several anomalies. For which you need Normalization. And need to have additional tables.

Normalization is the process of minimizing redundancy from a relation or set of relations.

Note: Minimum of three tables are required in the Many to Many relationships. If we try to merge it will create redundant data.

于 2021-09-12T02:42:51.630 回答
1

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

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

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

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

在上面的示例中,我更改了mainArtists关联的键,因此:

  • 您可以在需要时在同一个请求中同时使用artistsmainArtists关联。
  • 例如,当您从请求中获取时,关联会在复合记录类型mainArtists的属性中解码。mainArtistsinclude(all: Song.mainArtists)
于 2021-09-15T15:28:37.010 回答
0

我对此也很感兴趣,因为这是一个很好的HasManyThrough关联示例以及在类图数据结构中实现边的示例。我以为你心里有这个模式:

架构

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

这是我学到的:

  • 不同的 has-many- 和 has-many-through-association是必要的,以确保请求独立处理它们。否则,例如,将无法使用相同的请求获取主要艺术家和特色艺术家。如果没有不同的关联,请求将组合过滤器并仅返回定义mainfeature使用相同的艺术家SongArtist(这是不可能的,因此返回空数组)。
    • 这就是为什么我的代码为每个ArtistRelation. 我的Song结构有五个 has-many- 和另外五个 has-many-through-associations。
  • 在处理连接请求时,最好包含关闭表的整行。可以获取仅包含独立列的结构(例如,当我们只对 感兴趣时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]]
}
于 2021-09-28T05:32:25.920 回答