Rails: delete polymorphic STI has_many through association fails - ruby-on-rails

I have a model called User which has many "taxonomies" associated through a Classification model. One of these taxonomies is a model called Topic (inheriting from Taxonomy). My model User is also called a "classifiable".
EDIT: Added more models to clarify the problem
class User < ActiveRecord::Base
has_many :classifications, :as => :classifiable, :foreign_key => :classifiable_id
has_many :topics, :through => :classifications, :source => :taxonomy, :source_type => "Topic"
end
class Taxonomy < ActiveRecord::Base
end
class Topic < Taxonomy
has_many :classifications, :as => :taxonomy, :foreign_key => :taxonomy_id, :source_type => "Topic"
has_many :professionals, :through => :classifications, :source => :classifiable, :source_type => "User", :conditions => {:is_a_professional => true}
has_many :questions, :through => :classifications, :source => :classifiable, :source_type => "Question"
has_many :guides, :through => :classifications, :source => :classifiable, :source_type => "Guide"
end
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
Everything works well except when I want to delete an association.
user = User.find(12) # any user
topic = user.topics.last # any of his topics
user.topics.delete(topic)
The SQL ActiveRecord runs is the following:
DELETE FROM "classifications" WHERE "classifications"."classifiable_id" = 12 AND "classifications"."classifiable_type" = 'User' AND "classifications"."taxonomy_id" = 34 AND "classifications"."taxonomy_type" = 'Taxonomy'
Clearly, the taxonomy_type is wrong, it should be 'Topic' and not 'Taxonomy'.
Since I am using polymorphic associations and STI, I had to config ActiveRecord as such:
ActiveRecord::Base.store_base_sti_class = false
However, it does not seem to trigger on collection.delete. Is this a bug with rails?

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

Ruby on Rails has_many_through with validation and logic to ensure only one record in the join table which is updated as required

I have the Course model which has a number of has many through associations with the User model with join table CourseUser. The join table has an attribute type_str which specifies which role the user takes on. I have added validation to ensure that only one record is present in the join table for each course, user pair. The problem is ensuring that this record is updated if it is already present, rather than adding a new one which of course makes validation fail.
User class:
class User < ActiveRecord::Base
...
has_many :courses_enrolled_on, :through => :course_enrollees, :source => :course, :conditions => { :course_users => { :type_str => "enrollee" } }
has_many :course_users
has_many :courses, :through => :course_users, :source => :course, :readonly => true
end
Course class
class Course < ActiveRecord::Base
has_many :course_enrollees, :conditions => { :type_str => "enrollee" }, :class_name => CourseUser
has_many :enrollees, :through => :course_enrollees, :source => :user
has_many :course_users
has_many :users, :through => :course_users, :source => :user, :readonly => true
end
Course class:
class CourseUser < ActiveRecord::Base
belongs_to :course
belongs_to :user
validates_uniqueness_of :course_id, :scope => :user_id
end

has_many through relationship with conditions based on relationship table (Rails 2.3.5)

I have a relationship that is as follows.
companies_employee.rb
belongs_to :employee
belongs_to :company
validates_presence_of :role
employee.rb
has_many :companies_employees
has_many :companies, :through => :companies_employees
company.rb
has_many :companies_employees
has_many :managers, :through => :companies_employees, :source => :employee, conditions => {:role => "Manager"}
has_many :owners, :through => :companies_employees, :source => :employee, :conditions => {:role => "Owner"}
My problem is that when it checks the conditions, it tries to find the role column in the employees table, but the role column is in the companies_employees table.
Is there a way to make it use things in this table for the conditions?
Try something like this:
has_many :managers, :through => :companies_employees, :source => :employee, conditions => ["employees.role = 'Manager"]
has_many :owners, :through => :companies_employees, :source => :employee, conditions => ["employees.role = 'Owner"]

User (model) hierarchy, self-referential join

I'm trying to figure out how to have a two level user relationship.
Photographers have clients. Clients have one photographer. Both are Users.
I've got a User model that looks like this:
class User < ActiveRecord::Base
#authlogic
has_many :client_associations,
:foreign_key => 'client_id',
:class_name => 'Association',
:dependent => :destroy
has_many :clients, :through => :client_associations
has_one :photographer_association,
:foreign_key => 'photographer_id',
:class_name => 'Association',
:dependent => :destroy
has_one :photographer, :through => :photographer_association
end
And an Association model that looks like:
create_table "associations", :id => false, :force => true do |t|
t.integer "photographer_id"
t.integer "client_id"
end
class Association < ActiveRecord::Base
belongs_to :client, :class_name => 'User'
belongs_to :photographer, :class_name => 'User'
end
When I fill it with some data and fire up the console, running user.clients.all or user.photographer just gives me an empty array.
What am I doing wrong?
You should switch the foreign_keys:
has_many :client_associations,
:foreign_key => 'photographer_id',
:class_name => 'Association',
:dependent => :destroy
has_many :clients, :through => :client_associations
has_one :photographer_association,
:foreign_key => 'client_id',
:class_name => 'Association',
:dependent => :destroy
has_one :photographer, :through => :photographer_association

Resources