-
-
Notifications
You must be signed in to change notification settings - Fork 728
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
How to create Many-to-many association? #1063
Comments
Hello @wiencheck, In order to "create a many-to-many association" with GRDB Associations you need to:
Define the database schema that supports such a relationshipMany-to-many association, in a relational database, needs 3 database tables. In the GRBD documentation, there is a many-to-many association between 1. countries and 2. citizens through their 3. passports. In your case, there is a many-to-many association between 1. albums and 2. songs through their 3... finding a name here is difficult. A good default name is the concatenation of the two associated tables: 3. "album-song". So let's define three tables: try db.create(table: "album") { t in
t.autoIncrementedPrimaryKey("id")
t.column("title", .text).notNull()
...
}
try db.create(table: "song") { t in
t.autoIncrementedPrimaryKey("id")
t.column("title", .text).notNull()
...
}
try db.create(table: "albumSong") { t in
t.primaryKey {
t.belongsTo("album", onDelete: .cascade)
t.belongsTo("song", onDelete: .cascade)
}
} Note how this sample code defines primary keys and foreign keys that make sure the database can not contain invalid data. If, in your app, album and songs use a string or an uuid primary keys, you will have to adapt this sample code. So, how do you associate songs and albums? Given this lists of albums and songs:
To say that "Peaches en Regalia" is in album "Hot Rats", you add an entry in
If one day you want to store in the database the position of a song in an album, you'll need to add a Now everything should be clear, at the database level. Define in Swift code the record types that match this database schemaThis is abundantly documented in Recommended Practices for Designing Record Types: struct Album: Codable, Identifiable {
var id: Int64?
var title: String
...
}
struct Song: Codable, Identifiable {
var id: Int64?
var title: String
...
}
struct AlbumSong: Codable {
var albumId: Int64
var songId: Int64
}
// Add Database access
extension Album: FetchableRecord, MutablePersistableRecord {
// Update auto-incremented id upon successful insertion
mutating func didInsert(with rowID: Int64, for column: String?) {
id = rowID
}
}
extension Song: FetchableRecord, MutablePersistableRecord {
// Update auto-incremented id upon successful insertion
mutating func didInsert(with rowID: Int64, for column: String?) {
id = rowID
}
}
extension AlbumSong: FetchableRecord, PersistableRecord { } Note how records that have their ids auto-incremented by the database, That's it! Know which kind of request you want to buildLet's say you want to fetch all songs from an album. This is what you want to write: let album: Album = ...
let songs: [Song] = try dbQueue.read { db in
try album.songs.fetchAll(db)
} Fine! Let's do that! Define the GRDB associations that support those requestsSo we want to be able to write: let album: Album = ...
let songs: [Song] = try dbQueue.read { db in
try album.songs.fetchAll(db)
} We need to define At the level of the database, we can not really jump directly from the extension Album {
static let albumSongs = hasMany(AlbumSong.self)
static let songs = hasMany(Song.self, through: albumSongs, using: AlbumSong.song)
}
extension AlbumSong {
static let song = belongsTo(Song.self)
} In the above code:
Now that the extension Album {
var songs: QueryInterfaceRequest<Song> {
request(for: Album.songs)
}
} And this is the end of our journey: // It works!
let album: Album = ...
let songs: [Song] = try dbQueue.read { db in
try album.songs.fetchAll(db)
} If there is one thing to remember, it is to always follow those steps:
You also ask how to populate the database. You do it with regular persistence methods: try dbQueue.write { db in
// Insert an album
var album = Album(id: nil, title: "Hot Rats")
try album.insert(db) // Now album.id is not nil
// Insert a song
var song = Song(id: nil, title: "Peaches en Regalia")
try song.insert(db) // Now song.id is not nil
// Insert a song in an album
try AlbumSong(albumId: album.id!, songId: song.id!).insert(db)
} CHANGELOG:
|
Thank you so much @groue for such detailed explanation! Just tried that in my project and everything worked flawlessly. Happy to see Zappa fan in the wild :D |
You're welcome, this howto guide had to be written eventually, and your question was a good opportunity 👍 Happy that Frank Zappa was a well-chosen example ;-) |
Sorry to draw attention to a super old issue, but I just wanted to add on and say thank you for this write-up. It's perfect and exactly what I wanted to understand. You rock @groue |
You're welcome @gposcidonio. Your message gave the opportunity to update the answer for the latest GRDB version (the creation of the "albumSong" table). |
I'm trying to setup an association between songs and albums. Each song can appear on one or more albums and each album can contain one or more songs. I decided to go with GRDB for my database solution but I'm stuck on this issue.
Pardon me if I made a stupid mistake somewhere, I'm learning as I go
What I tried:
As documentation suggests, I created a
passport
struct, like this:Then in SPTTrack class:
And in SPTAlbum class:
I cannot find in the documentation a good example on how to build a request using those associations. In SPTAlbum class I added
linkedTracks
propertyAnd then in my database manager:
I'm getting error:
which is pretty self-explanatory, but I have no clue how and where should I create table for the
AlbumPassport
struct and if there are any additional steps I should take to actually populate this table with album/track connections.Both SPTTrack/SPTAlbum have a field called
id
which is set asprimaryKey
during first migration.The text was updated successfully, but these errors were encountered: