5

I have an Artist model that looks like this:

# app/models/artist.rb
class Artist < ActiveRecord::Base
  # Relationships
  has_many  :releases
  has_many  :songs, :through => :releases
  has_many  :featured_songs,  :through => :releases,
                              :class_name => "Song",
                              :source => :song,
                              :conditions => { 'releases.featured', true }                         

end

Retrieving the featured_songs works perfectly. The issue here is I'm unable to add a new featured_song to an artist because for some reason the 'featured' attribute is set to 'nil'.

This is what I'm attempting:

ruby-1.9.2-p180 :004 > a = Artist.first
ruby-1.9.2-p180 :005 > a.featured_songs.create(:title => "Title", :user => User.first)

The actual result of that is:

ruby-1.9.2-p180 :004 > a = Artist.first
ruby-1.9.2-p180 :005 > a.featured_songs.create(:title => "Title", :user => User.first)
  User Load (0.9ms)  SELECT `users`.* FROM `users` LIMIT 1
  SQL (1.0ms)  BEGIN
  SQL (5.5ms)  INSERT INTO `songs` (`created_at`, `title`, `updated_at`, `user_id`) VALUES (?, ?, ?, ?)  [["created_at", Thu, 11 Aug 2011 18:30:34 UTC +00:00], ["title", "Title"], ["updated_at", Thu, 11 Aug 2011 18:30:34 UTC +00:00], ["user_id", 1]]
  SQL (1.2ms)  INSERT INTO `releases` (`album_id`, `artist_id`, `created_at`, `featured`, `song_id`, `updated_at`) VALUES (?, ?, ?, ?, ?, ?)  [["album_id", nil], ["artist_id", 1], ["created_at", Thu, 11 Aug 2011 18:30:34 UTC +00:00], ["featured", nil], ["song_id", 6], ["updated_at", Thu, 11 Aug 2011 18:30:34 UTC +00:00]]
   (0.1ms)  COMMIT

Notice the: ["featured", nil]

Any idea what i'm doing wrong? How can i properly set attributes on my join without accessing it directly?

thank you!

EDIT: To make my issue more clear:

  • From an instance of artist i am unable to create new featured songs through the featured_songs relationship

  • Saves appear to be setting all of the song attributes EXCEPT for (the most important one) featured

  • The featured attribute is being set to nil for some reason and this is the real issue here.

Mario Zigliotto
  • 8,315
  • 7
  • 52
  • 71
  • try changin `:conditions` in `has_many` to `:conditions => { :releases => {:featured => true} }` – rubish Aug 13 '11 at 01:08
  • Thank you but unfortunately it did not work. The result is still `["featured", nil]` – Mario Zigliotto Aug 13 '11 at 01:11
  • Thought `has_many` concept would also apply here. Take a look at http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#method-i-has_many if you haven't, may be you will find something useful. – rubish Aug 13 '11 at 01:13
  • 1
    FWIW, none of the answers so far have worked for me. :-\ – Craig Walker Oct 30 '12 at 16:43

4 Answers4

2

I found the working solution after reading this:

https://github.com/rails/rails/issues/5178#issuecomment-4181551

The trick is to create a new first-order has_many association that contains the condition, and then run the has_many :through on that.

So, in your case, it would be:

class Artist < ActiveRecord::Base
  # Relationships
  has_many  :releases
  has_many  :songs, :through => :releases

  has_many  :featured_releases, :class_name => "Release", :conditions => { :featured => true }
  has_many  :featured_songs, :through => :featured_releases, :source => :song
end
gmcnaughton
  • 2,233
  • 1
  • 21
  • 28
Craig Walker
  • 49,871
  • 54
  • 152
  • 212
  • 1
    methinks it should be has_many :featured_songs, :through => :featured_releases, :source => :song – dymk Feb 02 '13 at 04:19
0

The key to allow save through association is to provide attr_accessible to fields on association model.

In this case, add line on Release model:

attr_accessible :song_id, :feature_id
j0k
  • 22,600
  • 28
  • 79
  • 90
leoo.lai
  • 29
  • 1
0

EDIT: Updated :releases_attributes to reflect has_many relationship.

jaydel's answer is along the right lines. This is a problem solved by accepts_nested_attributes_for. has_many only describes a mechanism to query records, not to update or create them.

The reason the attributes you listed are being updated is because they are part of the song object. The field you're trying to update is the featured field, which is part of your releases join table.

accepts_nested_attributes_for is the tool to solve the problem of updating nested relationships. If you want to add the featured attribute during creation of a song object (as you do in your example), then you need to add that as part of the nested attributes for the Song class.

class Song < ActiveRecord::Base
  has_many :releases
  has_many :artists, :through => :releases
  accepts_nested_attributes_for :releases
end

Your create would then look something like this:

a.songs.create(:title => 'Title', :user => User.first, :releases_attributes => [{ :id => 123, :featured => true}])

Note the addition of _attributes. This is exactly what Rails would do in the views if you were using nested forms. See the ActiveRecord source file nested_attributes.rb for proof.

If you really wanted a cleaner way to accomplish this, you could add a Song#create_featured method that would merge the :releases_attributes hash into the options to #create.

adamlamar
  • 4,629
  • 2
  • 27
  • 22
  • Just to confirm two things: 1- `accepts_nested_attributes_for :releases` should go in the song model only? In other words, the model im actually using (artist) does not accept nested attributes (seems kind of strange?) 2- i tried the code you provided and get this error: `NoMethodError: undefined method 'to_i' for :featured:Symbol` – Mario Zigliotto Aug 12 '11 at 23:43
  • For #1 - yes. When you take an instance of Artist (your `a` variable) and call `.songs`, you are effectively getting back an `Array` of `Song` objects. Try `a.songs.first.class` and you'll see a song. In actuality, `a.songs` returns an `ActiveRelation` object, but Rails hides that fact from you through the power of ruby (i.e. try `a.songs.class`). Instead of chaining `create`, try chaining the method `build` with just the title and user parameters. Then you can play around in the console and inspect the object before it is saved. – adamlamar Aug 13 '11 at 00:21
  • For #2 - Could you post a stack trace? I might have the plurality of the some of the methods mixed up, or something else might be awry. – adamlamar Aug 13 '11 at 00:22
  • Thinking about it, for #2, I probably gave you the wrong arguments to the :releases_attributes parameter (edited above). It needs to be enclosed in an array because you can add multiple releases in one call. The `id` parameters is only required if you want to update an existing record. – adamlamar Aug 13 '11 at 00:35
  • Yes! thank you.. that got things working/saving! BUT there's one kind of crazy thing i dont understand -- the `INSERT` is happening two times... check this out: http://pastie.org/private/dsubllg5jxnclvirzdr5g – Mario Zigliotto Aug 13 '11 at 00:42
  • The double insert makes sense after thinking about it, actually. One is created implicitly by ActiveRecord to match your new song to your artist, and you are creating a new one through the :releases_attributes parameter. Offhand I don't know of a good way to access the implicit one. – adamlamar Aug 13 '11 at 00:59
  • and again `accepts_nested_attributes_for` can't help here at all. – fl00r Aug 13 '11 at 11:26
-1

You can use accepts_nested_attributes_for in the model. I would start by watching the Railscast on the topic and then peruse the documentation itself for the method

EDIT:

From the comments below it's clear I don't completely understand what you're asking for. I thought you wanted to know how to create and manipulate a joined object from a model. When I needed to do this recently I had these two models:

class User < ActiveRecord::Base
  belongs_to :address

  accepts_nested_attributes_for :address
end

class Address < ActiveRecord::Base
  has_one :user
end

Then I can do this to create a new address on a user

u = User.new
u.address.build({:city => "bozoville", :zip => "22222", ...})
u.save!

and my address is on my user and persisted. Is this what you were looking for? I will say that I'm not entirely how effectively this translates to a many to many association, but I was assuming it would be the same thing. Perhaps because the one to one relies on a single column for the foreign key that works, but when it involves a more complex assocation with an actual join table it doesn't work? At the very least you might investigate the possibility further...

jaydel
  • 14,389
  • 14
  • 62
  • 98
  • I didn't say anything about accepts_nested_sets. accepted_nested_attributes_for lets you save joined object en masse through the association. Create, in taking a hash accepts the parameters in a form, but if you're using create manually it works the same way. If you've got some further insight that will set me straight, by all means please share. – jaydel Aug 11 '11 at 21:07
  • 1
    I mean `accepted_nested_attributes_for` of course. and it is not about `accepted_nested_attributes_for`. author wrote that `condition` statement in relation didn't executed. and this is true. also it won't be executed if we will use `accepted_nested_attributes_for`, because it will call just the same method. – fl00r Aug 11 '11 at 21:09
  • so answer the question if you know the answer. You've got a bigger "unit" than me, we all agree, so now tell us what the right answer is... – jaydel Aug 11 '11 at 21:12
  • So I'm actually DOING in on my own project right now. model_1.model_2.build({...}) which is equivalent to creating one...I clearly don't understand how this is different from what he wants. can you point that out? somehow you know I'm wrong, I'd like to know more than just "you are wrong." Surely you must have SOME reason for thinking/knowing it's wrong. – jaydel Aug 11 '11 at 21:22
  • Late edit on your second comment is MUCH more helpful. thank you and I'll look forward to hearing from you tomorrow :P – jaydel Aug 11 '11 at 21:36
  • Ok, so. The main idea of topic author is that association ignores conditions statement while initializing new object. It is not about forms and `accepts_nested_attributes_for`. Also, `accepts_nested_attributes_for` will do just the same issue, because it will call right the same method: `new` or `build` wich will ignore conditions statement as well. So here is two problems. 1) `accepts_..` is not about topic issue, 2) it will have just the same problem as we have got here. – fl00r Aug 12 '11 at 06:25
  • Fair enough. That's what I wanted to know. I'm still learning and this helps me as well as possibly helping the original poster. So thank you. – jaydel Aug 12 '11 at 15:10
  • fl00r, do you know how i can initialize a new object w/ the association `:conditions` respected? – Mario Zigliotto Aug 16 '11 at 16:46