1

I have two models Playlist and Song. The playlist has an ordered list of songs through a join table playlist_song_associations with a position attribute, e.g.

class Playlist < ApplicationRecord
  has_many :playlist_song_associations, dependent: :destroy
  has_many :songs, through: :playlist_song_associations
end

class Song < ApplicationRecord
  has_many :playlist_song_associations, dependent: :destroy
  has_many :playlists, through: :playlist_song_associations
end

class PlaylistSongAssociation < ApplicationRecord
  belongs_to :playlist, touch: true
  belongs_to :song
  default_scope { order(position: :asc) }
end

Now I have an update controller method, where I do something like

playlist.songs = get_ordered_songs_from_params

And I want the associations to be ordered according to the order within the get_ordered_songs_from_params array.

Is there a way to set this up, so that rails automagically sets the position attribute on the associations?

Johannes Stricker
  • 1,701
  • 13
  • 23
  • SInce the Playlist does not have a position attribute, setting the songs wouldn't set the position. In the controller you should create/update the PlaylistSongAssociation model. – R. Sierra Aug 07 '23 at 15:03
  • But that means I will lose the ability to create associations by setting the `playlist.songs` attribute, which would make the `has_many_through` useless. Isn't there some rails magic to keep this working? – Johannes Stricker Aug 07 '23 at 15:13
  • No, you wouldn't lose anything. Doing `playlist.songs = [song_1]` is actually syntactic sugar for `PlaylistSongAssociation.new(playlist: playlist, song: song_1)`. – R. Sierra Aug 07 '23 at 15:44
  • And there's no way to "improve" that syntactic sugar? – Johannes Stricker Aug 07 '23 at 18:10

2 Answers2

2

The answer to whether or not you can get rails to do that automagically is maybe no, but maybe that's not what you really want?

The way you have things set up, PlaylistSongAssociation is a real object. It exists like it has meaning. But it really kind of doesn't have any meaning outside the context of the playlist. You never want to edit it alone.

A better model might be to have an array of song_ids in the Playlist and drop the PlaylistSongAssociation. The downside is that you almost certainly lose referential integrity to songs - but that might depend on your database of choice, and maybe that isn't a real concern for you (if song deletion is unlikely or super rare or you're fine recovering from it).

Add an array column in Rails

or using a json column. Again, depending on your database.

kwerle
  • 2,225
  • 22
  • 25
  • Thanks, I thought about that as well and it might be a good option, cause I can live without the referential integrity. I assume I can't get it to work like regular associations then, though? Meaning `.joins()` and `.includes()` won't work and I will always have to manually make a second request to fetch the plays? – Johannes Stricker Aug 08 '23 at 05:27
  • 1
    Yeah, I don't see a way to do that. If you will need to do things like track how many playlists a song is in then this method is probably not for you! – kwerle Aug 08 '23 at 16:43
1

You can automatically fill the "position" field by using callbacks when creating PlaylistSongAssociation.

before_save :ensure_position
def ensure_position
  self.position = playlist.songs.count + 1
end

If you recently added the possibility of ordering your playlist and there was no "position" field before, you need to fill this field with data by using either migration or rake task. You can fill the field easily:

Playlist.all.each do |playlist|
  playlist.playlist_song_associations.each_with_index do |psa, i|
    psa.update(position: i + 1)
  end
end

The idea here is to automate position setting when it gets added to playlist only once. If you will try to update position on the fly each time you call the playlist songs - you will ruin loading speed and will spam the db with requests. Always try to make only absolutely necessary amount of requests to preserve resources, cause every little thing counts.

Allacassar
  • 159
  • 6