0

The app I'm developing has 3 main models and many single table inheritance models:

  1. Question
  2. User
    1. Professional
    2. Representant
  3. Taxonomy
    1. Category
    2. Topic
    3. Profession
    4. Locality
    5. Region
    6. 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...

  1. 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.)

  2. 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?

Jonathan Roy
  • 903
  • 11
  • 20
  • Could my problem be that I don't have the `has_many :through` statements inside all my taxonomy classes (Topics, Professions, etc...)? If this is the problem, it would require a very non-DRY solution... – Jonathan Roy Dec 31 '12 at 16:32

1 Answers1

3

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
mesozoic
  • 700
  • 4
  • 11
  • 1
    After pecking through GitHub for [Rails issues referring to base_class](https://github.com/rails/rails/issues/search?q=base_class), I found the [store_base_sti_class](https://github.com/appfolio/store_base_sti_class) gem. It changes this behavior, but does so across your entire Rails app (which may or may not be what you want). – mesozoic Dec 31 '12 at 18:21
  • Thanks! I have installed the gem. It half-worked. Please see my update. – Jonathan Roy Dec 31 '12 at 19:20
  • I've updated the answer with some more research and another link. But I might need to see more of your application in order to help. Based on the code you posted in your example, the gem I referenced should take care of the association scope for you. – mesozoic Dec 31 '12 at 21:08
  • Awesome! Everything works perfectly now. Thank you so much @Alex! – Jonathan Roy Jan 02 '13 at 14:50