How to retrieve ordered objects through a ordered association? - ruby-on-rails

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?

Related

Model attribute contains collection of object

I have main model Page, which is container.
The page can have some to-do lists, notes, files and discussions. The idea is to have them in special order.
Page.last.container # [Todolist_obj, Note_obj, File_obj, Note_obj, Discussion_obj, File_obj, File_obj]
So I came to approach to use Mongodb
Or I also thought about using Postgres with hstore, but don't know will it help or not
Or maybe just any database and deserialize all objects when getting page, and serialize objects when saving
Or I can make superclass Item and inherit all containing objects from it using MTI and make Page has many relationship.
So I don't know which way is the best?
or perhaps there is a better way?
I have used acts_as_list for implementing sortable objects very successfully. Additionally, i would abstract the elements of a page into a separate model, here called PageElement.
I think there is no need to switch to a NoSQL database (although i have nothing against this approach). Here is a rough sketch of what i'm thinking:
class Page < ActiveRecord::Base
has_many :page_elements, :order => 'position'
has_many :todo_lists, :through => :page_elements, :source => :element, :source_type => 'TodoList'
has_many :notes, :through => :page_elements, :source => :element, :source_type => 'Note'
has_many :files, :through => :page_elements, :source => :element, :source_type => 'File'
has_many :discussions, :through => :page_elements, :source => :element, :source_type => 'Discussion'
end
class PageElement < ActiveRecord::Base
belongs_to :page
belongs_to :element, :polymorphic => true
acts_as_list :scope => :page
end
class TodoList < ActiveRecord::Base
has_one :page_element, :as => :element
has_one :page, :through => :page_elements
end
class Note < ActiveRecord::Base
has_one :page_element, :as => :element
has_one :page, :through => :page_elements
end
class File < ActiveRecord::Base
has_one :page_element, :as => :element
has_one :page, :through => :page_elements
end
class Discussion < ActiveRecord::Base
has_one :page_element, :as => :element
has_one :page, :through => :page_elements
end

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

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.

rails - using :select(distinct) with :has_many :through association produces invalid SQL

User
has_many :posts
has_many :post_tags, :through => :posts
PostTag
belong_to :post
belongs_to :tag
scope :distincttag, :select => ('distinct post_tags.tag_id')
with Rails 3.0.4, i get invalid SQL:
SELECT post_tags.*, distinct tag_id...
at least one other person experienced the same problem: http://www.ruby-forum.com/topic/484938
feature or a bug?
thanks
Does not look like the right thing to put on a scope.
Maybe you are trying to accomplish this:
class PostTag < ...
belong_to :post
belongs_to :tag
def distincttag
find(:all, :select => 'distinct tag_id')
end
end
Edit: now that I know what you need:
User
has_many :posts
has_many :post_tags, :through => :posts, :select => 'distinct tags.*'
# or, if you are not worried about database overhead:
has_many :post_tags, :through => :posts, :uniq => true
Reference: http://blog.hasmanythrough.com/2006/5/6/through-gets-uniq

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