Rails joins or preload belongs_to association from polymorphic model - ruby-on-rails

my problem is following. How can I joins belongs_to association from polymorphic model
There is situation
opinion.rb
class Opinion < ActiveRecord::Base
belongs_to :opinionable, :polymorphic => true
belongs_to :category
end
answer.rb
class Answer < ActiveRecord::Base
has_many :opinions, :as => :opinionable
end
How can i do following
Opinion.joins(:opinionabe).all
it will throw
ArgumentError: You can't create a polymorphic belongs_to join without specifying the polymorphic class!
How can i specific which class i want to join?
Second question. How to preload it?
Opinion.preload(:opinionable).all
works fine. It will do query for each class in belongs_to.
But. if i want to do something like
Opinion.preload(:opinionable => :answer_form).all
there is problem because one model has this association and second hasn't. So it will throw exception.
So how i can do something like
Opinion.preload(:answer => :answer_form, :another_belongs_to_model).all
?
Thanks, David!

Actually if you just do
belongs_to :opinionable_answer, :foreign_key => :opinionable_id, :class_name => "Answer", conditions: { opinions: { opinionable_type: "Answer"}}
then you can do
Opinion.joins(:opinionable_answer).where(answers: { awesome: true})

It looks like you have not specified opinionable_type:string column for your Opinion model.
Try to update your migration in this manner:
class CreateOpinions < ActiveRecord::Migration
def self.up
create_table :opinions do |t|
t.integer :opinionable_id
t.string :opinionable_type
# ... other fields
t.timestamps
end
end
def self.down
drop_table :opinions
end
end
This will solve your second question and Opinion.preload(:opinionable).all should work well.
You cann't do joins on polymorphic association because they can be located in different tables, which are detected after Opinion model is loaded. That why model needs column opinionable_type.
If you try to do this you'll get next exception
ActiveRecord::EagerLoadPolymorphicError: Can not eagerly load the polymorphic association :opinionable
UPD: Added magic join ^_^
class Opinion < ActiveRecord::Base
belongs_to :opinionable, :polymorphic => true
belongs_to :opinionable_answer, :foreign_key => :opinionable_id, :class_name => "Answer"
scope :by_type, lambda { |type| joins("JOIN #{type.table_name} ON #{type.table_name}.id = #{Opinion.table_name}.opinionable_id AND #{Opinion.table_name}.opinionable_type = '#{type.to_s}'") }
end
Example:
Opinion.by_type(Answer).to_sql
=> "SELECT \"opinions\".* FROM \"opinions\" JOIN answers ON answers.id = opinions.opinionable_id AND opinions.opinionable_type = 'Answer'"

I know this question is old but I just spent an hour looking for the solution to a similar problem (Rails 3) and the only way I got it to work was the solution stated here: https://stackoverflow.com/a/25966630/6878997
Which, in your case would be:
class Opinion < ActiveRecord::Base
# The true polymorphic association
belongs_to :opinionable, polymorphic: true
# The trick to solve this problem
has_one :self_ref, :class_name => self, :foreign_key => :id
has_one :answer, :through => :self_ref, :source => :opinionable, :source_type => Answer
end
Seems tricky but this way you will be able to do multiple chained joins such as:
joins(answer: :other_model).
And whenever opinion.opinionable is not an Answer, opinion.answer will return nil.
Hope it helps somebody!

Related

Polymorphic association confusion, get resource through association

I might be totally confused and off target here.
I have an Event model, which has_one a Timeline that belongs_to Event. The Timeline has_many TimelineItems that belong_to Timeline.
def Event
has_one :timeline
end
def Timeline
belongs_to :event
has_many :timeline_items
end
def TimelineItem
belongs_to :timeline
end
create_table :admin_timeline_items do |t|
t.references :admin_timeline
t.references :user
t.references :resource, :polymorphic => true
t.string :method
end
So through from a TimelineItem object I would be able to generate for example this output:
eml created the Post "Hi this is from eml" on [DATE]
The title "Hi this is from eml" would come from the Post object's .title. So eg. this TimelineItem has these settings
item.user = User.where(:username => "eml")
item.resource_type = "Post"
item.resource_id = 1
item.created_at = 27/082...13:37
So the problem is fetching said Post object, or rather the correct syntax for this particular association. Seems terribly simple to me, but I am not finding the information I need to properly write it.
Thanks for your help & time.
Sorry... I was just confusing myself because I had set resource_id in my own test item to a faulty value. My first guess was correct. It is quite simple:
class TimelineItem < ActiveRecord::Base
belongs_to :timeline
belongs_to :user
belongs_to :resource, :polymorphic => true
end
Access resource like so:
TimelineItem.first.resource
=> Post(....)

Accessing singular_association_ids from model in Rails

I've been using the association_collection method "other_ids" throughout my Rails app with no issues. However whenever I try to access it from within the model defining the association, Rails has no idea what I'm taking about. For example:
class Membership < ActiveRecord::Base
belongs_to :course, :touch => true
belongs_to :person, :touch => true
end
class Day < ActiveRecord::Base
belongs_to :course, :touch => true, :counter_cache => true
has_many :presents, :dependent => :delete_all
has_many :people, :through => :presents
before_destroy :clear_attendance
def clear_attendance
mems = Membership.where(:course_id => course.id, :person_id => person_ids)
mems.update_all(["attendance = attendance - ?", (1 / course.days.size.to_f)])
end
end
In this case, person_ids is always null. I've tried self.person_ids, people.ids, etc. All nothing. I have used day.person_ids elsewhere with no issues, so why can't I use it here?
I am using Ruby 1.9.1 and Rails 3.0.3. Here is the SQL call from my log:
[1m[36mAREL (0.0ms)[0m [1mUPDATE "memberships" SET attendance = attendance - 0.3333333333333333 WHERE ("memberships"."course_id" = 4) AND ("memberships"."person_id" IN (NULL))[0m
edit: added more code to clarify question
What you really want there is:
def a_method
self.people.all
end
But to answer your question, person_ids is the correct method, and it should return an empty array, not nil. I just tried an association like that out in 2.3.10. Maybe you can post some more of your code, rails version, etc.
Thanks for your help - I figured it out myself. The problem was the order of my callbacks. I was trying to call person_ids after the association had been deleted. Changing the order to this solved my issues.
class Day < ActiveRecord::Base
before_destroy :clear_attendance
belongs_to :course, :touch => true, :counter_cache => true
has_many :presents, :dependent => :delete_all
has_many :people, :through => :presents

Belongs_to based on value of a field

I have a table with entries, and each entries can have different account-types. I'm trying to define and return the account based on the value of cindof
Each account type has one table, account_site and account_page. So a regular belongs_to won't do.
So is there any way to return something like:
belongs_to :account, :class_name => "AccountSite", :foreign_key => "account_id" if cindof = 1
belongs_to :account, :class_name => "AccountPage", :foreign_key => "account_id" if cindof = 2
Have tried to do that in a method allso, but no luck. Really want to have just one accountand not different belongs_to names.
Anyone that can figure out what I want? Hard to explain in English.
Terw
You should be able to do what you want with a polymorphic association. This won't switch on cindof by default, but that may not be a problem.
class ObjectWithAccount < ActiveRecord::Base
belongs_to :account, :polymorphic => true
end
class AccountSite < ActiveRecord::Base
has_many :objects_with_accounts,
:as => :account,
:class_name => 'ObjectWithAccount'
end
class AccountPage < ActiveRecord::Base
has_many :objects_with_accounts,
:as => :account,
:class_name => 'ObjectWithAccount'
end
You will need both an account_id column and a account_type column. The type of the account object is then stored in the extra type column.
This will let you do:
obj.account = AccountPage.new
or
obj.account = AccountSite.new
I would look into Single Table Inheritance. Not 100% sure, but I think it would solve your problem http://code.alexreisner.com/articles/single-table-inheritance-in-rails.html
If that isn't good, this isn't too hard to implement yourself.
def account
case self.cindof
when 1 then AccountSite.find self.account_id
when 2 then AccountPage.find self.account_id
end
end

Multiple counter_cache in Rails model

I'm learning Rails, and got into a little problem. I'm writing dead simple app with lists of tasks, so models look something like that:
class List < ActiveRecord::Base
has_many :tasks
has_many :undone_tasks, :class_name => 'Task',
:foreign_key => 'task_id',
:conditions => 'done = false'
# ... some validations
end
Table for List model has columns tasks_counter and undone_tasks_counter.
class Task < ActiveRecord::Base
belongs_to :list, :counter_cache => true
# .. some validations
end
With such code there is attr_readonly :tasks_counter for List instances but I would like to have a counter for undone tasks as well. Is there any way of having multiple counter cached automagically by Rails.
So far, I've managed to create TasksObserver that increments or decrements Task#undone_tasks_counter, but maybe there is a simpler way.
Have you tried it with a custom-counter-cache column?
The doc here:
http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html
It suggests that you can pass a column-name to the counter_cache option, which you may well be able to call twice eg
belongs_to :list, :counter_cache => true # will setup tasks_count
belongs_to :list, :counter_cache => :undone_tasks_count
Note: not actually tested.
ez way.
1) first counter - will do automatically
2) Manually "correct"
AnotherModelHere
belongs_to :user, counter_cache: :first_friends_count
after_create :provide_correct_create_counter_2
after_destroy :provide_correct_destroy_counter_2
def provide_correct_create_counter_2
User.increment_counter(:second_friends_count, another_user.id)
end
def provide_correct_destroy_counter_2
User.decrement_counter(:second_friends_count, another_user.id)
end
Most probably you will need counter_culture gem, as it can handle counters with custom conditions and will update counter value not only on create and destroy, but for updates too:
class CreateContainers < ActiveRecord::Migration[5.0]
create_table :containers, comment: 'Our awesome containers' do |t|
t.integer :items_count, default: 0, null: false, comment: 'Caching counter for total items'
t.integer :loaded_items_count, default: 0, null: false, comment: 'Caching counter for loaded items'
end
end
class Container < ApplicationRecord
has_many :items, inverse_of: :container
has_many :loaded_items, -> { where.not(loaded_at: nil) },
class_name: 'Item',
counter_cache: :loaded_items_count
# Notice that you can specify custom counter cache column name
# in has_many definition and AR will use it!
end
class Item < ApplicationRecord
belongs_to :container, inverse_of: :items, counter_cache: true
counter_culture :container, column_name: proc { |model| model.loaded_at.present? ? 'loaded_items_count' : nil }
# But this column value will be handled by counter_culture gem
end
I'm not aware of any "automagical" method for this. Observers seems good for this, but I personally prefer using callbacks in model (before_save, after_save).

In Ruby on Rails, how do I make a polymorphic model associate to a namespaced model?

I have the following models.
# app/models/domain/domain_object.rb
class Domain::DomainObject < ActiveRecord::Base
has_many :links_from, :class_name => "Link", :as => :from, :dependent => :destroy
end
# app/models/link.rb
class Link < ActiveRecord::Base
belongs_to :from, :polymorphic => true
belongs_to :object_value, :polymorphic => true
end
Problem is, when I do the following, the from_type doesn't prefix the Domain namespace to the model e.g.
Domain::DomainObject.all(:include=> :links_from )
That causes the following SELECT:
SELECT `links`.* FROM `links` WHERE (`links`.`from_id` IN (5,6,12,13,18,24,25,27,29,30,31,32,34,35,39) and `links`.`from_type` = 'DomainObject')
The query should be:
SELECT `links`.* FROM `links` WHERE (`links`.`from_id` IN (5,6,12,13,18,24,25,27,29,30,31,32,34,35,39) and `links`.`from_type` = 'Domain::DomainObject')
because Rails automatically saves the model with the namespace.
I've seen a few recommendations on Rails sites about doing something like this:
belongs_to :from, :polymorphic => true, :class_name => "Domain::DomainObject"
However, that doesn't appear to work either.
So, is there a better way to do this? Or is this not supported?
To fix this, I did a include Domain in the DomainObject model and set ActiveRecord::Base.store_full_sti_class = true in config/environment.rb.
hoyhoy's response is the solution. This solved my problem , too (I actually wanted the namespace to be stripped off).
However, I'd recommend that x.store_full_sti_class = true be added to config/environment.rb, only if it is desired globally. I imagine it might not always be required, in which case, we can easily translate the solution to the class level.
class User < ActiveRecord::Base
self.store_full_sti_class = true
...
end

Resources