Rails has_many through filtered associations - ruby-on-rails

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

Related

Rails: Class inheritance and complex polymorphic has_many :through association

The app I'm developing has 3 main models and many single table inheritance models:
Question
User
Professional
Representant
Taxonomy
Category
Topic
Profession
Locality
Region
Country
There are multiple kinds of users (User, Professional < User, Representant < User) which all inherit from the User class with single table inheritance.
There are multiple kinds of taxonomies (Category < Taxonomy, Topic < Taxonomy, Profession < Taxonomy, Locality < Taxonomy, Region < Taxonomy, Country < Taxonomy) which all inherit from the Taxonomy class with single table inheritance.
Questions, as well as professionals are also under taxonomies via many to many relationships (they can have many topics, many professions, many categories, etc...)
Now, I'm looking for a way to establish that many to many relationship between those polymorphic objects. I've tried the has_many :through solution and created a Classification class.
Migration file:
class CreateClassifications < ActiveRecord::Migration
def change
create_table :classifications, :id => false do |t|
t.references :classifiable, :null => false, :default => 0, :polymorphic => true
t.references :taxonomy, :null => false, :default => 0, :polymorphic => true
end
add_index :classifications, [:classifiable_id, :taxonomy_id]
add_index :classifications, [:taxonomy_id, :classifiable_id]
end
end
Model file:
class Classification < ActiveRecord::Base
attr_accessible :classifiable, :classifiable_id, :classifiable_type,
:taxonomy, :taxonomy_id, :taxonomy_type
belongs_to :classifiable, :polymorphic => true
belongs_to :taxonomy, :polymorphic => true
end
I've then added has_many :through associations for Questions, Professionals, and Taxonomies.
Taxonomy.rb
has_many :classifications, :as => :taxonomy, :foreign_key => :taxonomy_id
has_many :classifiables, :through => :classifications, :source => :classifiable
has_many :users, :through => :classifications, :source => :classifiable, :source_type => "User"
has_many :professionals, :through => :classifications, :source => :classifiable, :source_type => "Professional"
has_many :representants, :through => :classifications, :source => :classifiable, :source_type => "Representant"
has_many :questions, :through => :classifications, :source => :classifiable, :source_type => "Question"
has_many :guides, :through => :classifications, :source => :classifiable, :source_type => "Guide"
Question.rb
has_many :classifications, :as => :classifiable, :foreign_key => :classifiable_id, :dependent => :destroy
has_many :taxonomies, :through => :classifications, :source => :taxonomy
has_many :topics, :through => :classifications, :source => :taxonomy, :source_type => "Topic"
Professional.rb
has_many :classifications, :as => :classifiable, :foreign_key => :classifiable_id, :dependent => :destroy
has_many :taxonomies, :through => :classifications, :source => :taxonomy
has_many :topics, :through => :classifications, :source => :taxonomy, :source_type => "Topic"
has_many :professions, :through => :classifications, :source => :taxonomy, :source_type => "Profession"
Now, after setting up all this, things do not work very well...
I can't seem to assign taxonomies to Professionals or Questions (i.e. Question.create(:title => "Lorem Ipsum Dolor Sit Amet", :author => current_user, :topics => [list of topics,...]) works well except for topics which are not saved.)
Where clauses don't work as they should (i.e. Question.joins(:topics).where(:conditions => {:topics => {:id => [list of topics,...]}}) fails with a no such column: "Topics"."id" error.
Any help? Thanks!
UPDATE
I have installed the gem 'store_base_sti_class' as indicated. It had the desired effect on the Classification model.
#<Classification classifiable_id: 1, classifiable_type: "Professional", taxonomy_id: 17, taxonomy_type: "Topic">
However, when I query topics (Professional.find(1).topics), ActiveRecord is still looking for the class "User" instead of "Professional"...
SELECT "taxonomies".* FROM "taxonomies" INNER JOIN "classifications" ON "taxonomies"."id" = "classifications"."taxonomy_id" WHERE "taxonomies"."type" IN ('Topic') AND "classifications"."classifiable_id" = 1 AND "classifications"."classifiable_type" = 'User' AND "classifications"."taxonomy_type" = 'Topic'
Any idea how to fix it for both?
For question #2, the keys in the where clause should map to table names, not association names. So I think you would want:
Question.joins(:topics).where(Topic.table_name => {:id => [...]})
For question #1, it appears that when you set question.topics = [...], the Classification objects which Rails creates are being set with a taxonomy_type of "Taxonomy" (instead of "Topic"). That appears to be due to Rails' through_association.rb:51, which takes the base_class of the model being stored, instead of just the actual class name.
I was able to get around this with a before_validation callback on the Classification model. It seems to me that the alternative is a patch to the actual Rails associations code, to make this behavior configurable.
class Classification < ActiveRecord::Base
attr_accessible :classifiable, :classifiable_id, :classifiable_type,
:taxonomy, :taxonomy_id, :taxonomy_type
belongs_to :classifiable, polymorphic: true
belongs_to :taxonomy, polymorphic: true
before_validation :set_valid_types_on_polymorphic_associations
protected
def set_valid_types_on_polymorphic_associations
self.classifiable_type = classifiable.class.model_name if classifiable
self.taxonomy_type = taxonomy.class.model_name if taxonomy
end
end
UPDATE
There appears to be another Rails decision (in preloader/association.rb:113) to use the model.base_class.sti_name instead of the model.sti_name when setting scope for associations.
That gem should take care of this for you. See store_base_sti_class_for_3_1_and_above.rb:135 for how it wraps the has_many :as option. In my local environment, this works as expected:
$ bundle exec rails console
irb(main):001:0> topics = 3.times.map { Topic.create }
irb(main):002:0> p = Professional.new
irb(main):003:0> p.topics = topics
irb(main):004:0> p.save!
irb(main):005:0> exit
$ bundle exec rails console
irb(main):001:0> puts Professional.find(1).topics.to_sql
SELECT "taxonomies".* FROM "taxonomies" INNER JOIN "classifications" ON "taxonomies"."id" = "classifications"."taxonomy_id" WHERE "taxonomies"."type" IN ('Topic') AND "classifications"."classifiable_id" = 2 AND "classifications"."classifiable_type" = 'Professional' AND "classifications"."taxonomy_type" IN ('Topic')
irb(main):002:0> Professional.find(1).topics.count
=> 3

How to retrieve ordered objects through a ordered association?

I am using Ruby on Rails 3.2.2. I have following associations:
class Article < ActiveRecord::Base
has_many :category_associations,
:class_name => 'CategoryAssociation'
# Note: Same as :category_associations but gets records ordered by :position.
has_many :positioned_category_associations,
:class_name => 'CategoryAssociation',
:order => :position
has_many :categories,
:through => :category_associations,
:source => :category
# Note: Same as :categories but gets records ordered by :position.
has_many :positioned_categories,
:through => :category_associations,
:source => :category,
:order => [:category_associations => :position]
end
With the above code I can call #article.positioned_categories and get categories properly ordered by position. However, I would like to use the :positioned_category_associations in order to retrieve :positioned_categories but if I state the following:
class Article < ActiveRecord::Base
...
has_many :positioned_categories,
:through => :positioned_category_associations,
:source => :category
# instead of
#
# has_many :positioned_categories,
# :through => :category_associations,
# :source => :category,
# :order => [:category_associations => :position]
end
it seems do not work as expected: I gets not ordered categories.
How to retrieve ordered categories through :positioned_category_associations? Is it possible?
Bonus: Since :category_associations and :positioned_category_associations have almost same statements (except for the :order => :position), is it possible to refactoring :positioned_category_associations statements so to DRY (Don't Repeat Yourself) the code?

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.

Writing to has_many :through associations and callbacks

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.

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.

Resources