has_many :through issue with new records - ruby-on-rails

I'm modeling "featuring" based on my plan in this question and have hit a bit of a stumbling block.
I'm defining a Song's primary and featured artists like this:
has_many :primary_artists, :through => :performances, :source => :artist,
:conditions => "performances.role = 'primary'"
has_many :featured_artists, :through => :performances, :source => :artist,
:conditions => "performances.role = 'featured'"
This works fine, except when I'm creating a new song, and I give it a primary_artist via new_song.performances.build(:artist => some_artist, :role => 'primary'), new_song.primary_artists doesn't work (since the performance I created isn't yet saved in the database).
What's the best approach here? I'm thinking of going with something like:
has_many :artists, :through => :performances
def primary_artists
performances.find_all{|p| p.role == 'primary'}.map(&:artist)
end

I think you're overcomplicating it. Just because things have similarities doesn't mean you should put them all in the same box.
class Song < ActiveRecord::Base
has_one :artist # This is your 'primary' artist
has_and_belongs_to_many :featured_artists, :source => :artist # And here you make a featured_artists_songs table for the simple HABTM join
validates_presence_of :artist
end
Poof, no more confusion. You still have to add song.artist before you can save, but that's what you wanted. Right?

There's not much that you can do about the association not being recognized until you save. Arguably, it doesn't really exist until you save, validations pass, and the relevant transaction(s) are completed.
Regarding your question of cleaning up your primary_artist method, you could model it something like this.
class Song < ActiveRecord::Base
has_many :performances
has_many :artists, :through => :performances
has_one :primary_artist, :through => :performances, :conditions => ["performances.roll = ?", "primary"], :source => :artist
end
It's unclear if you want one or many primary artists, but you can easily switch that has_one to has_many as needed.

You've nailed the source of your problem with build vs. create.
As for finding the primary artist of a song. I would add a named_scope on artist to select only featured/primary artists.
class Artist < ActiveRecord::Base
...
named\_scope :primary, :joins => :performances, :conditions => "performances.role = primary"
named\_scope :featured, :joins => :performances, :conditions => "performances.role = featured"
end
To get the primary artist for a song, you would do #song.artists.primary or if you prefer your primary_artists method in song.
def primary_artists
artists.primary
end
However, after looking at your initial question, I think your database layout is insufficient. It's workable but not clear, I've posted my suggestions there, where it belongs.
These named scopes I would also work under my proposed scheme as well.

Related

Rails associations - ideas for best practice

A question regarding Rails associations.
Consider the following models:
People
Events
Images
People and events can have many images.
Images uploaded to an event, need to have the ability to be associated with multiple people.
This means that there are two relationships between people and images.
One where images are uploaded directly on the person. And one where a person is tagged in an event.
Can there also be a relationship between a person and an event based on the fact they were tagged in one (or multiple) images in an event? In this regard, it's a sort of image tagging system where associations are created based on what event people are tagged in.
Wondering what is the best practice in Rails to create this association? Any help or advice is greatly appreciated!
I am not absolutely sure about best practice,
but there is a chance to solve your case in simple way, as far as i understand you:
We have:
Event ... has_many :photos; Photo ... belongs_to :event
and solution is to create some intermediary ActiveRecord class
People ... has_many :photos, :through photo_mappings
PhotoMapping ... belongs_to :user; belongs_to :photo
Have a nice AR associations best practice1
In rails it is definitely possible to define a join-table that has extra fields. So in this case I would define the following table:
class LinkedImage
belongs_to :person
belongs_to :image
OWNER=1
TAGGED=2
validates :relation_type, :inclusion => {:in => [OWNER, TAGGED]}
end
This table would link an image to a person, and has an extra field relation_type (you could think of a more appropriate name maybe), which now can have two values: 1 (for the OWNER, meaning the image was directly uploaded to the person), and 2 (the person is tagged in the image). Aside from the relation, maybe you want to store something extra, like the position in the image, an extra comment, then you could easily add that here as well.
And the person would look like:
class Person
has_many :linked_images
has_many :uploaded_images, :class_name => Image,
:through => :linked_images,
:conditions => "relation_type=#{LinkedImage::OWNER}"
has_many :tagged_in_images, :class_name => Image,
:through => :linked_images,
:conditions => 'relation_type=#{LinkedImage::TAGGED}"
end
and the code for Image could be similar.
I hope this helps.
I don't know if this is the best approach, but I would do something like this:
class User < ActiveRecord::Base
has_many :images, :as => :owner
has_many :tags
has_many :tagged_images, :through => :tags, :source => :image
has_many :tagged_events, :finder_sql => %q( SELECT `events`.* FROM `events`, `images` WHERE `events`.id = `images`.owner_id AND `images.owner_type ="Event" `
WHERE (`images`.id IN (#{tagged_image_ids.join(',')}) ))
end
class Event < ActiveRecord::Base
has_many :images, :as => :owner
has_many :tagged_users, :finder_sql => %q( SELECT `users`.* FROM `users`, `images` WHERE `users`.id = `images`.owner_id AND `images.owner_type ="User" `
WHERE (`images`.id IN (#{image_ids.join(',')}) ))
end
class Tag < ActiveRecord::Base
belongs_to :user
belongs_to :image
end
class Image < ActiveRecord::Base
belongs_to :owner, :polymorphic => true
has_many :tags
has_many :tagged_users, :through => :tags, :source => :user
end
Note: Using finder_sql is not the best in Rails, because for example it doesn't allow you to add new tagged images just using the relationship, but it works fine for reading.

Joining friends to other models

I'm still having trouble building complex joins in ActiveRecord.
I have a User model that is using the HasManyFriends plugin by Steve Ehrenberg (http://dnite.org).
Then I have a UserFeedEvent model that links users to a FeedEvent model.
What I'd like to achieve is to find all the FeedEvents linked to the friends of a User.
How should I write my ActiveRecord query?
Here are my models:
class User < ActiveRecord::Base
has_many_friends
has_many :feed_events, :through => :user_feed_events, :dependent => :destroy
has_many :user_feed_events, :dependent => :destroy
end
class UserFeedEvent < ActiveRecord::Base
belongs_to :feed_event, :dependent => :destroy
belongs_to :user
end
class FeedEvent < ActiveRecord::Base
has_many :user_feed_events, :dependent => :destroy
has_many :users, :through => :user_feed_events
serialize :data
end
Thanks in advance!
Augusto
Digging through HasManyFriends source leads me to believe that the following should work (or be half-way through):
EDIT: found out that source cannot point to another :has_many :through association. So you could try the updated version.
class User < ActiveRecord::Base
#...
has_many :user_feed_events_of_friends_by_me, :through => :friends_by_me,
:source => :user_feed_events
has_many :feed_events_of_friends_by_me, :through => :user_feed_events_by_me
has_many :user_feed_events_of_friends_for_me, :through => :friends_for_me,
:source => :user_feed_events
has_many :feed_events_of_friends_for_me, :through => :user_feed_events_for_me
# A wrapper to return full list of two-way friendship events
def feeds_events_of_my_friends
self.feed_events_of_friends_by_me + self.feed_events_of_friends_for_me
end
end
Unfortunately the HMF plugin has two one-way friendship links, which means full list requires 2 DB queries.
I found a working and more traditional SQL solution:
friends_id = current_user.friends.collect {|f| f.id}.join(",")
sql = "SELECT feed_events.*, user_feed_events.user_id FROM feed_events LEFT JOIN user_feed_events ON feed_events.id = user_feed_events.feed_event_id WHERE user_feed_events.user_id IN (#{friends_id}) GROUP BY feed_events.id ORDER BY feed_events.created_at DESC"
friend_feed_events = FeedEvent.paginate_by_sql(sql, :page => params[:page], :per_page => 30)
If you have a more efficient / more elegant way of doing the same, please let me know!

Self-referencing models in Rails 3

I have an Entity model and I want to display connections between the Entities. ie, Entity 1 is connected to Entity 2.
My thinking, right now, is to create a join model between the two called Connection and have it work like a traditional rails join table. Except have the columns be entity_one_id and entity_two_id, then establish a many-to-many relationship between Entity and Connection.
This seems like a really not-elegant way to do this. I was wondering if anyone had any better ideas? Maybe something more rails-esque that I'm just not seeing?
That's the most-common way to do it. If an entity is only ever connected to one other model, you could use a linked-list, tree-like structure.
Check out Ryan Bates' Railscast on self-joining models. It deals with a social-network-like system, but it still has principles you'll need and provides a great starting point
You could use this implementation:
class User < ActiveRecord::Base
has_many :friends, :through => :friendships, :conditions => "status = 'accepted'"
has_many :requested_friends, :through => :friendships, :source => :friend, :conditions => "status = 'requested'", :order => :created_at
has_many :pending_friends, :through => :friendships, :source => :friend, :conditions => "status = 'pending'", :order => :created_at
has_many :friendships, :dependent => :destroy
end
class Friendship < ActiveRecord::Base
belongs_to :user
belongs_to :friend, :class_name => "User"
end

Accessing two sides of a user-user relationship in rails

Basically, I have a users model in my rails app, and a fanship model, to facilitate the ability for users to become 'fans' of each other.
In my user model, I have:
has_many :fanships
has_many :fanofs, :through => :fanships
In my fanship model, I have:
belongs_to :user
belongs_to :fanof, :class_name => "User", :foreign_key => "fanof_id"
My fanship table basically consists of :id, :user_id and :fanof_id. This all works fine, and I can see what users a specific user is a fan of like:
<% #user.fanofs.each do |fan| %>
#things
<% end %>
My question is, how can I get a list of the users that are a fan of this specific user?
I'd like it if I could just have something like #user.fans, but if that isn't possible what is the most efficient way of going about this?
Thanks!
Add in User model:
has_many :my_fanclubs, :class_name => 'Fanship', :foreign_key => 'fanof_id'
has_many :fans, :through => :my_fanclubs, :source => :user, :class_name => 'User'
(not tested)

Recreating this custom query using has_many

I'm hoping this will be an easy one :) I've been stuffing around for hours playing with the has_many options trying to emulate this:
has_many :pages, :finder_sql => %q(SELECT * FROM `pages` LEFT OUTER JOIN `component_instances` ON `component_instances`.instance_id = `pages`.id AND `component_instances`.instance_type = 'Page' WHERE `component_instances`.parent_id = #{id})
It's basically a polymorphic join so there is the component_instances table that acts as a central structure and has different types of things hanging off of it. It's a nested set (not that that matters in this case).
The problem seems to be that has_many doesn't allow me to manipulate the join conditions. And I can't nullify the foreign key join condition that's automatically made.
The above code works but I want to use scopes on the results, and that's not possible with a custom query.
Any help would be greatly appreciated :)
Cheers,
Brendon
You can do this with the :through option.
has_many :pages, :through => :component_instances, :source => :parent, :source_type => 'Page'
Thanks for the lead Michael, in the end this worked:
has_one :page_set, :foreign_key => :parent_id
belongs_to :instance, :polymorphic => true, :dependent => :destroy
has_many :pages, :through => :page_set, :source => :instance, :source_type => 'Page', :extend => LearningCaveFilterExtension
but it's a little bit sketchy as the :page_set method actually returns something completely wrong. Ideally it should return self but I needed to put :parent_id as the foreign key so that the SQL generated from the has_many pages declaration was correct (using :id would be the correct way but then that screws up the :pages method. :) My mind hasn't quite gotten around what's going on here, but at least it works and scoping works too :)
Thanks for the help, and if you have any explanations as to why that works, please let me know :)
Lol, sleeping on it gave me the answer:
class PageSet < ActiveRecord::Base
unloadable
set_table_name "component_instances"
has_many :children, :foreign_key => :parent_id, :class_name => 'PageSet'
belongs_to :instance, :polymorphic => true, :dependent => :destroy
has_many :pages, :through => :children, :source => :instance, :source_type => 'Page'
end
The entities with the link through parent_id are rightly children, I was just referring to them in the wrong way, but AR didn't raise any errors :)

Resources