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

我首先尝试使用 Gwendal Roué 的答案来解决它,但并没有让它发挥作用。我打开了一个 GRDB问题寻求更多帮助。他在几个小时内回答,解决了我所有的问题,并详细解释了解决方案。
这是我学到的:
- 不同的 has-many- 和 has-many-through-association是必要的,以确保请求独立处理它们。否则,例如,将无法使用相同的请求获取主要艺术家和特色艺术家。如果没有不同的关联,请求将组合过滤器并仅返回定义
main
并feature
使用相同的艺术家SongArtist
(这是不可能的,因此返回空数组)。
- 这就是为什么我的代码为每个
ArtistRelation
. 我的Song
结构有五个 has-many- 和另外五个 has-many-through-associations。
- 在处理连接请求时,最好包含关闭表的整行。可以获取仅包含独立列的结构(例如,当我们只对 感兴趣时
relation
)但这会使代码更加复杂。Gwendal Roué 不建议这样做。
- 这就是为什么我的代码示例中的结构
6
包含一整SongArtist
行,尽管我只需要它的relation
.
以下代码包含一个完整的场景和这些获取请求以测试实现:
- 所有歌曲和相应的艺术家,但没有
ArtistRelation
信息
- 所有歌曲和相应的艺术家,根据他们的不同分组到单独的数组中
ArtistRelation
- 所有歌曲及其关系数
- 所有没有特色艺术家的歌曲
- 直接访问关闭表以获取定义特色艺术家的所有 SongArtist 行
- 所有歌曲与他们的艺术家和他们的关系
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]]
}