Writing to has_many :through associations and callbacks - ruby-on-rails

Consider a "Name" model which has a required "label" attribute and an arbitrary Rails 3 model "Foo" with the following associations:
has_many :names, :dependent => :destroy
has_many :special_names, :through => :names, :source => :label, :conditions => { 'special_names.label' => 'special' }, :dependent => :destroy
Now it's possible to access the "special_names" attribute for reading the association, but writing to it fails because AR cannot infer from the condition that the "label" attribute needs to be set to "special" for all members of the "special names" association.
I attempted to use the "add_before" association callback, but that never gets called with the join model (instead the ":source" and "Foo" are used).
Any ideas on how to handle this in the model (as opposed to: using special logic in the controller to deal with this - that's how I handle it currently)?
Edit: (regarding the answer from Ray Baxter)
The relationship expressed is actually a "has_many :through" association. I'll try again, this time with a (hopefully) better example:
# Label is a shared entity which is used in many contexts
has_many :labels, :through => :user_labels
# UserLabel is the join model which qualifies the usage of a Label
has_many :user_labels, :dependent => :destroy
# special_user_labels is the topic of this question
has_many :special_user_labels, :through => :user_labels, :source => :label, :conditions => { 'user_labels.descriptor' => 'special' }, :dependent => :destroy

If my comment above is correct, and you aren't doing a has_many :through, this works:
has_many :special_names, :class_name => 'Name', :conditions => {:label => 'special'}, :dependent => :destroy
so now you can do
foo = Foo.create
foo.special_name.build
and ActiveRecord will correctly instantiate your special_name with the label attribute having the value "special".

I found the solution (thanks x0f#Freenode) - one needs to split the 'special' associations in two. has_many :special_user_labels, :through => :user_labels, :source => :label, :conditions => { 'user_labels.descriptor' => 'special' }, :dependent => :destroy becomes
1) has_many :special_labels, :class_name => 'UserLabel', :conditions => { :descriptor => 'special' }, :dependent => :destroy
2) has_many :special_user_labels, :through => :special_labels, :source => :label, :dependent => :destroy
Works for reading & writing as well as a seamless replacement for (scoped) hbtm associations.

Related

Rails 3: Many to many relationship with polymorphic association

I have 2 models e.g task model and task_relation model
Task has many parent tasks and child tasks.
Have added following associations -
Task.rb
has_many :from_tasks, :as => :relation, :class_name => "TaskRelation",
:foreign_key => "task_from_id", :source => :parent,
:conditions => {:relation_type => 'Parent'}, :dependent => :destroy
has_many :to_tasks , :as => :relation, :class_name => "TaskRelation",
:foreign_key => "task_to_id", :source => :child,
:conditions => {:relation_type => 'Child'}, :dependent => :destroy
has_many :child_tasks, :through => :from_tasks, :dependent => :destroy
has_many :parent_tasks, :through => :to_tasks, :dependent => :destroy
accepts_nested_attributes_for :to_tasks, :reject_if => :all_blank, :allow_destroy => true
accepts_nested_attributes_for :from_tasks, :reject_if => :all_blank, :allow_destroy => true
TaskRelation.rb
belongs_to :parent_task, :class_name => "Task", :foreign_key => "task_from_id"
belongs_to :child_task, :class_name => "Task", :foreign_key => "task_to_id"
belongs_to :relation, :polymorphic => true
When I save task form, it also saves parent_tasks and child tasks in task_relations table with relation_type as 'Task' but I want to store relation_type as 'Parent' for parent tasks and 'Child' for child tasks.
Can anyone please help me on this.
Firstly, remove the relation polymorphic association - it's not needed. Now, modify your Task model to look like this:
# This association relates to tasks that are parents of self
has_many :parent_task_relations, class_name: 'TaskRelation', foreign_key: 'child_task_id'
# And this association relates to tasks that are children of self
has_many :child_task_relations, class_name: 'TaskRelation', foreign_key: 'parent_task_id'
has_many :child_tasks, :through => :child_task_relations
has_many :parent_tasks, :through => :parent_task_relations
And you should be done.
To illustrate how this might be used - say you have a Task a and need to assign task B as a parent, and task C as a child. You could accomplish this like so:
a.parent_tasks << b
a.child_tasks << c
This would have the same effect on your database as this code:
a.parent_task_relations.create(parent_task: b)
a.child_task_relations.create(child_task: c)
Which is the same (to the database) as:
TaskRelation.create(parent_task: b, child_task: a)
TaskRelation.create(parent_task: a, child_task: c)

Reverse has_many with polymorphism

I have two models: Users and Projects. The idea is that Users can follow both projects AND other users. Naturally, Users and Projects are part of a polymorphic "followable" type. Now, using the user model, I'd like to get three things:
user.followed_users
user.followed_projects
user.followers
The first two work fine; It's the third that I'm having trouble with. This is sort of a reverse lookup where the foreign key becomes the "followable_id" column in the follows table, but no matter how I model it, I can't get the query to run correctly.
User Model
has_many :follows, :dependent => :destroy
has_many :followed_projects, :through => :follows, :source => :followable, :source_type => "Project"
has_many :followed_users, :through => :follows, :source => :followable, :source_type => "User"
has_many :followers, :through => :follows, :as => :followable, :foreign_key => "followable", :source => :user, :class_name => "User"
Follow Model
class Follow < ActiveRecord::Base
belongs_to :followable, :polymorphic => true
belongs_to :user
end
My follows table has:
user_id
followable_id
followable_type
Whenever I run the query I get:
SELECT `users`.* FROM `users` INNER JOIN `follows` ON `users`.`id` = `follows`.`user_id` WHERE `follows`.`user_id` = 7
where it should be "followable_id = 7 AND followable_type = 'User", not "user_id = 7"
Any thoughts?
Figured it out. Took a look at a sample project Michael Hartl made and noticed that the correct way to do this is to specify not only a relationship table (in this case follows) but also a reverse relationship table (which I called reverse follows).
has_many :follows,
:dependent => :destroy
has_many :followed_projects,
:through => :follows,
:source => :followable,
:source_type => "Project"
has_many :followed_users,
:through => :follows,
:source => :followable,
:source_type => "User"
has_many :reverse_follows,
:as => :followable,
:foreign_key => :followable_id,
:class_name => "Follow"
has_many :followers,
:through => :reverse_follows,
:source => :user
Hope this helps some people out down the line!
I think you need to explicitly spell out the foreign key. You have:
:foreign_key => "followable"
you need:
:foreign_key => "followable_id"
full code:
has_many :followers, :through => :follows, :as => :followable, :foreign_key => "followable_id", :source => :user, :class_name => "User"

ActiveRecord, has_many :through, Polymorphic Associations with STI

In ActiveRecord, has_many :through, and Polymorphic Associations, the OP's example requests ignoring the possible superclass of Alien and Person (SentientBeing). This is where my question lies.
class Widget < ActiveRecord::Base
has_many :widget_groupings
has_many :people, :through => :widget_groupings, :source => :person, :source_type => 'Person'
has_many :aliens, :through => :widget_groupings, :source => :alien, :source_type => 'Alien'
end
SentientBeing < ActiveRecord::Base
has_many :widget_groupings, :as => grouper
has_many :widgets, :through => :widget_groupings
end
class Person < SentientBeing
end
class Alien < SentientBeing
end
In this modified example the grouper_type value for Alien and Person are now both stored by Rails as SentientBeing (Rails seeks out the base class for this grouper_type value).
What is the proper way to modify the has_many's in Widget to filter by type in such a case? I want to be able to do Widget.find(n).people and Widget.find(n).aliens, but currently both of these methods (.people and .aliens) return empty set [] because grouper_type is always SentientBeing.
Have you tried the simplest thing - adding :conditions to the has_many :throughs?
In other words, something like this (in widget.rb):
has_many :people, :through => :widget_groupings, :conditions => { :type => 'Person' }, :source => :grouper, :source_type => 'SentientBeing'
has_many :aliens, :through => :widget_groupings, :conditions => { :type => 'Alien' }, :source => :grouper, :source_type => 'SentientBeing'
JamesDS is correct that a join is needed - but it's not written out here, since the has_many :through association is already doing it.

Complex has_many relationships and Rails

I have some accounts, and users, which are disjointed at the moment.
I need users to be able to be admins, or editors, or any (and many) accounts.
At the moment, I have this:
account.rb
has_many :memberships, :dependent => :destroy
has_many :administrators, :through => :memberships, :source => :user, :conditions => {'memberships.is_admin' => true}
has_many :editors, :through => :memberships, :source => :user, :conditions => {'memberships.is_editor' => true}
user.rb
has_many :memberships
has_many :accounts, :through => :memberships
has_many :editor_accounts, :through => :memberships, :source => :account, :conditions => {'memberships.is_editor' => true}
has_many :administrator_accounts, :through => :memberships, :source => :account, :conditions => {'memberships.is_admin' => true}
Essentially, what I am trying to acheive is a nice simple way of modelling this that works in a nice simple way. For instance, being able to do the following this would be really useful:
#account.administrators << current_user
current_user.adminstrator_accounts = [..]
etc
You should be able to do this, but it might be the notation you've used that interferes with the auto scope application:
has_many :memberships,
:dependent => :destroy
has_many :administrators,
:through => :memberships,
:source => :user,
:conditions => { :is_admin => true }
The conditions should be applied if and only if the condition keys match the column names on the association. So long as the users table doesn't have a is_admin column, this will be fine.
As a note, having multiple boolean flags for something like this can be awkward. Is it possible to be an admin and an editor? You may be better off with a simple role column and then use that:
has_many :administrators,
:through => :memberships,
:source => :user,
:conditions => { :role => 'admin' }
A multi-purpose column is often better than a multitude of single-purpose columns from an indexing perspective. You will have to index each and every one of these is_admin type columns, and often you will need to do it for several keys. This can get messy in a hurry.

Rails has_many through filtered associations

I am trying to use one join model for two separate but very similar associations. Here is what I have:
Two primary models: Package, Size
Pacakges have many sizes but there's a wrinkle. The sizes need to be allocated as a size for top or bottom. My current associations on Package are:
has_many :package_sizes
has_many :sizes, :through => :package_sizes
has_many :bottoms_sizes, :through => :package_sizes, :scope => {:package_sizes => {:bodylocation => "B"}}, :source => :size
has_many :tops_sizes, :through => :package_sizes, :scope => {:package_sizes => {:bodylocation => "T"}}, :source => :size
PackageSize is a join model with: size_id | package_id | bodylocation:string
I have a failing test to verify it is working:
#p = Package.new
#size1 = Size.first
#p.tops_sizes << #size1
#p.save
#p.reload
#p.tops_sizes.should include(#size1)
This should work properly but for some reason the bodylocation field does not get automatically set.
Any ideas?
There is (IMHO) a better solution to this in the answer to: Scope with join on :has_many :through association.
Essentially it would be something like:
has_many :package_sizes
has_many :sizes, :through => :package_sizes do
def tops
where("package_sizes.bodylocation = 'T'")
end
def bottoms
where("package_sizes.bodylocation = 'B'")
end
end
You would then query for them like:
#p.sizes.tops
Try creating two separate through associations for this.
has_many :bottom_package_sizes, :class_name => 'PackageSize', :conditions => {:bodylocation => 'B'}
has_many :top_package_sizes, :class_name => 'PackageSize', :conditions => {:bodylocation => 'T'}
has_many :bottom_sizes, :through => :bottom_package_sizes
has_many :top_sizes, :through => :top_package_sizes

Resources