How to retrieve attributes from has_one association without belongs_to? - ruby-on-rails

I have several nested classes that look like so:
class Presentation < ActiveRecord::Base
has_many :campaign_records, :as => :campaignable
def campaign_records_text(joiner)
return '' if self.campaign_records.blank?
self.campaign_records.map {|c| c.to_s}.join(joiner)
end
end
class CampaignRecord < ActiveRecord::Base
belongs_to :campaignable, :polymorphic => true
has_one :campaign
has_one :tier_one
has_one :tier_two
def to_s
"#{campaign.name} - #{tier_one.name} - #{tier_two.name}"
end
end
class Campaign < ActiveRecord::Base
has_many :tier_ones
attr_accessible :name
end
class TierOne < ActiveRecord::Base
has_many :tier_twos
belongs_to :campaign
attr_accessible :name
end
class TierTwo < ActiveRecord::Base
belongs_to :tier_one
attr_accessible :name
end
In summary, a Campaign has many associated TierOnes and every TierOne has it's own set of TierTwos. A Presentation has a set of CampaignRecords which link a Campaign,TierOne, and TierTwo together. Note though that a Campaign does not belong_to a CampaignRecord because many CampaignRecords can refer to it.
So here's the problem: I want to change the CampaignRecord.to_s method to return "campaign.name - tier_one.name - tier_two.name" (like shown above) but doing so results in an error when I try to call some_campaign_record.to_s:
ActionView::Template::Error (Mysql2::Error: Unknown column
'campaigns.campaign_record_id' in 'where clause': SELECT 'campaigns'.*
FROM 'campaigns' WHERE 'campaigns'.'campaign_record_id' = # LIMIT 1)
Where did I go wrong here? I know that rails auto generates a lot of getters and setters for me, but the default to_s method is just the usual so how do I override it in the proper rails way? Does a has_one require a belongs_to or is there a belongs_to_many hiding out there somewhere that I should have used instead?
Any help would be very much appreciated! Thanks in advance!
(Also, I saw that my question is very similar to this unanswered question)
EDIT
I'm seeing a bit of confusion about the model structure here so let me try to explain it differently in a way that will hopefully be clearer.
First off, just to be clear a Campaign is very different from a CampaignRecord.
Think of the Campaign-TierOne-TierTwo relationship like a three layered list:
Campaign 1
TierOne 1.1
TierTwo 1.1.1
TierTwo 1.1.2
...
TierOne 1.2
TierTwo 1.2.1
TierTwo 1.2.2
...
...
Campaign 2
TierOne 2.1
TierTwo 2.1.1
TierTwo 2.1.2
...
TierOne 2.2
TierTwo 2.2.1
TierTwo 2.2.2
...
...
Campaign 3
...
The CampaignRecord model is a representation of a Campaign, TierOne, TierTwo tuple. When its first created, you select a Campaign. Then select a TierOne from that Campaign's set of TierOnes. Then a TierTwo from that TierOne's set of TierTwos. In other words, the CampaignRecord model is a path which traverses a Campaign-TierOne-TierTwo tree.
The set of presentation.campaign_records is the set of valid Campaign-TierOne-TierTwo paths which a user has previously associated with that presentation instance. (A Presentation will have zero or more of these paths associated with it.)
The important bit of functionality is that a Presentation should have a variable size set of Campaign-TierOne-TierTwo linkages. While modifying any given Presentation, I need to be able to modify/add/remove Campaign-TierOne-TierTwo linkages to/from a Presentation. I chose to represent those Campaign-TierOne-TierTwo linkages as CampaignRecords. A Presentation can have a bunch of these CampaignRecords, but no Campaign, TierOne, or TierTwo will ever belong_to a CampaignRecord.
So, my question becomes: Why is my Campaign model throwing a "can't find specified column" error when it was never supposed to be looking for that column in the first place?
#presentations = Presentations.all
#presentations.each do |presentation|
presentation.campaign_records.each do |campaign_record|
print campaign_record.to_s # Campaign model throws error here
end
end

Based on the comments above, one error I see is the following:
class CampaignRecord < ActiveRecord::Base
...
has_one :tier_two
...
end
Take a look at the Rails Guide on Associations.. Campaign is going to expect to see a campaign_record_id in any ActiveRecord model that has a has_something association.
My best bet is that you'll want to add that column into the TierTwo database.
Alternately, if every TierTwo's CampaignRecord can be inferred through its TierOne, you could also hypothetically do:
class CampaignRecord < ActiveRecord::Base
...
has_many :tier_twos, through: :tier_one
...
end
However, that doesn't seem like the right option. You say a CampaignRecord has only one TierTwo, and TierOne can have many TierTwos.
Let me know if this helps. It's possible I'm missing some information about your business logic that would help clarify my recommendation.

Solution seems pretty obvious to me now, in hindsight. My CampaignRecord model had fields to store IDs of other model records. But the has_one in the CampaignRecord implies that other models should be storing the ID of the CampaignRecord. Solution was to change the has_ones to belongs_tos so that the lookup goes in the other direction:
class CampaignRecord < ActiveRecord::Base
belongs_to :campaignable, :polymorphic => true
belongs_to :campaign
belongs_to :tier_one
belongs_to :tier_two
def to_s
"#{campaign.name} - #{tier_one.name} - #{tier_two.name}"
end
end

Related

Rails 4 Accept nested attributes with has_one association

I have a question about Rails Nested Attributes.
I'm using Rails 4 and have this model:
model Location
has_one parking_photo
has_many cod_photos
accepts_nested_attributes_for :parking_photo
accepts_nested_attributes_for :cod_photos
end
When I use for example:
Location.find(100).update(cod_photo_ids: [1,2,3]) it works.
But Location.find(100).update(parking_photo_id: 1) doesn't works.
I don't know what difference between nested attributes has_one and has_many.
Or do we have any solution for my case, when I already have child object and want to link the parent to the child and don't want to use child update.
Thank you.
The problem has nothing to do with nested attributes. In fact you're not even using nested attributes at all in these examples.
In this example:
Location.find(100).update(cod_photo_ids: [1,2,3])
This will work even if you comment out accepts_nested_attributes_for :cod_photos as the cod_photo_ids= setter is created by has_many :cod_photos.
In the other example you're using has_one where you should be using belongs_to or are just generally confused about how you should be modeling the association. has_one places the foreign key on the parking_photos table.
If you want to place the parking_photo_id on the locations table you would use belongs_to:
class Location < ActiveRecord::Base
belongs_to :parking_photo
# ...
end
class ParkingPhoto < ActiveRecord::Base
has_one :location # references locations.parking_photo_id
end
Of course you also need a migration to actually add the locations.parking_photo_id column. I would really suggest you forget about nested attributes for the moment and just figure out the basics of how assocations work in Rails.
If you really want to have the inverse relationship and put location_id on parking_photos you would set it up like so:
class Location < ActiveRecord::Base
has_one :parking_photo
# ...
end
class ParkingPhoto < ActiveRecord::Base
belongs_to :location
validates_uniqueness_of :location_id
end
And you could reassign a photo by:
Location.find(100).parking_photo.update(location_id: 1)

Rails - ActiveRecord :has_many association is not being loaded when queried with :includes

Please have a look at this basic associations setup (Rails 4.2.1):
class Parent < ActiveRecord::Base
has_many :kids
end
class Kid < ActiveRecord::Base
belongs_to :school
end
class School < ActiveRecord::Base
end
With these models the next code works as expected and keeps the association loaded:
parent = Parent.first
parent.kids.to_a
parent.association(:kids).loaded? # => true
But: adding an .includes(:school) prevents the association being loaded/cached for some reason
parent = Parent.first
parent.kids.includes(:school).to_a
parent.association(:kids).loaded? # => false
Looking at SQL logs I see exact the same select query so I expect the association to be loaded/cached.
The question
Why is this happening?
ActiveRecord preloads/caches parent.kids in the first example. I want to understand why its not possible in the second example with includes call?
An ideal answer for me would include references to the sources.
Thanks for any help
P.S.
The workaround I'm using is setting association.target manually:
parent.association(:kids).target = parent.kids.includes(:school).to_a

Caching for model in rails

product.rb
has_many :votes
vote.rb
belongs_to :product
Every time, i use sorting in my index controller:
index_controller.rb
def index
#products = Product.all.sort { |m| m.votes.count }
end
So, i think it would be good to cache votes count for each product (create additional column votesCount in products table)?
If yes, can i preform that using before_save and before_delete callbacks in vote.rb model?
Or what is the best practice method?
Give me some examples please.
I guess you are looking for counter_cache
The :counter_cache option can be used to make finding the number of belonging objects more efficient
Consider these models:
class Order < ActiveRecord::Base
belongs_to :customer, counter_cache: true
end
class Customer < ActiveRecord::Base
has_many :orders
end
With this declaration, Rails will keep the cache value up to date, and then return that value in response to the size method.
Although the :counter_cache option is specified on the model that includes the belongs_to declaration, the actual column must be added to the associated model. In the case above, you would need to add a column named orders_count to the Customer model

Ruby On Rails ActiveRecord 3 Way Join

I have 3 models:
class ProductLine < ActiveRecord::Base
has_many :specifications
has_many :specification_categories, :through => :specifications,
end
class Specification < ActiveRecord::Base
belongs_to :product_line
belongs_to :specification_category
end
class SpecificationCategory < ActiveRecord::Base
has_many :specifications
has_many :product_lines, :through => :specifications
end
Basically, we are showing the specifications as a subset of data on the product line page and we would like to do something like (example only, yes I'm aware of N+1):
Controller:
#product_line = ProductLine.find(params[:id])
#specification_categories = #product_line.specification_categories)
View:
#specification_categories.each do |specification_category|
...
specification_category.specifications.each do |specification|
...
end
end
The issue here is getting rails to filter the specifications by ProductLine. I've tried constructing queries to do this but it always generates a separate NEW query when the final association is called. Even though we aren't using the code above now (not a good idea here, since we could potentially run into an N+1 problem), I'd like to know if it's even possible to do a 3 way join with association filtering. Has anyone run across this scenario? Can you please provide an example of how I would accomplish this here?
Prevent the N+1 by altering your line to:
#specification_categories = #product_line.specification_categories).include(:specifications)
OR
Construct your own query using .joins(:association), and do the grouping yourself.

Why polymorphic association doesn't work for STI if type column of the polymorphic association doesn't point to the base model of STI?

I have a case of polymorphic association and STI here.
# app/models/car.rb
class Car < ActiveRecord::Base
belongs_to :borrowable, :polymorphic => true
end
# app/models/staff.rb
class Staff < ActiveRecord::Base
has_one :car, :as => :borrowable, :dependent => :destroy
end
# app/models/guard.rb
class Guard < Staff
end
In order for the polymorphic assocation to work, according to the API documentation on Polymorphic Assocation, http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#label-Polymorphic+Associations that I have to set borrowable_type to the base_classof STI models, that is in my case is Staff.
The question is: Why doesn't it work if the borrowable_type set to STI class?
Some test to prove it:
# now the test speaks only truth
# test/fixtures/cars.yml
one:
name: Enzo
borrowable: staff (Staff)
two:
name: Mustang
borrowable: guard (Guard)
# test/fixtures/staffs.yml
staff:
name: Jullia Gillard
guard:
name: Joni Bravo
type: Guard
# test/units/car_test.rb
require 'test_helper'
class CarTest < ActiveSupport::TestCase
setup do
#staff = staffs(:staff)
#guard = staffs(:guard)
end
test "should be destroyed if an associated staff is destroyed" do
assert_difference('Car.count', -1) do
#staff.destroy
end
end
test "should be destroyed if an associated guard is destroyed" do
assert_difference('Car.count', -1) do
#guard.destroy
end
end
end
But it seems to be true only with Staff instance. The results are:
# Running tests:
F.
Finished tests in 0.146657s, 13.6373 tests/s, 13.6373 assertions/s.
1) Failure:
test_should_be_destroyed_if_an_associated_guard_is_destroyed(CarTest) [/private/tmp/guineapig/test/unit/car_test.rb:16]:
"Car.count" didn't change by -1.
<1> expected but was
<2>.
Thanks
Good question. I had exactly the same problem using Rails 3.1. Looks like you can not do this, because it does not work. Probably it is an intended behavior. Apparently, using polymorphic associations in combination with Single Table Inheritance (STI) in Rails is a bit complicated.
The current Rails documentation for Rails 3.2 gives this advice for combining polymorphic associations and STI:
Using polymorphic associations in combination with single table
inheritance (STI) is a little tricky. In order for the associations to
work as expected, ensure that you store the base model for the STI
models in the type column of the polymorphic association.
In your case the base model would be "Staff", i.e. "borrowable_type" should be "Staff" for all items, not "Guard". It is possible to make the derived class appear as the base class by using "becomes" : guard.becomes(Staff). One could set the column "borrowable_type" directly to the base class "Staff", or as the Rails Documentation suggests, convert it automatically using
class Car < ActiveRecord::Base
..
def borrowable_type=(sType)
super(sType.to_s.classify.constantize.base_class.to_s)
end
An older question, but the issue in Rails 4 still remains. Another option is to dynamically create/overwrite the _type method with a concern. This would be useful if your app uses multiple polymorphic associations with STI and you want to keep the logic in one place.
This concern will grab all polymorphic associations and ensure that the record is always saved using the base class.
# models/concerns/single_table_polymorphic.rb
module SingleTablePolymorphic
extend ActiveSupport::Concern
included do
self.reflect_on_all_associations.select{|a| a.options[:polymorphic]}.map(&:name).each do |name|
define_method "#{name.to_s}_type=" do |class_name|
super(class_name.constantize.base_class.name)
end
end
end
end
Then just include it in your model:
class Car < ActiveRecord::Base
belongs_to :borrowable, :polymorphic => true
include SingleTablePolymorphic
end
Just had this issue in Rails 4.2. I found two ways to resolve:
--
The problem is that Rails uses the base_class name of the STI relationship.
The reason for this has been documented in the other answers, but the gist is that the core team seem to feel that you should be able to reference the table rather than the class for a polymorphic STI association.
I disagree with this idea, but am not part of the Rails Core team, so don't have much input into resolving it.
There are two ways to fix it:
--
1) Insert at model-level:
class Association < ActiveRecord::Base
belongs_to :associatiable, polymorphic: true
belongs_to :associated, polymorphic: true
before_validation :set_type
def set_type
self.associated_type = associated.class.name
end
end
This will change the {x}_type record before the creation of the data into the db. This works very well, and still retains the polymorphic nature of the association.
2) Override Core ActiveRecord methods
#app/config/initializers/sti_base.rb
require "active_record"
require "active_record_extension"
ActiveRecord::Base.store_base_sti_class = false
#lib/active_record_extension.rb
module ActiveRecordExtension #-> http://stackoverflow.com/questions/2328984/rails-extending-activerecordbase
extend ActiveSupport::Concern
included do
class_attribute :store_base_sti_class
self.store_base_sti_class = true
end
end
# include the extension
ActiveRecord::Base.send(:include, ActiveRecordExtension)
####
module AddPolymorphic
extend ActiveSupport::Concern
included do #-> http://stackoverflow.com/questions/28214874/overriding-methods-in-an-activesupportconcern-module-which-are-defined-by-a-cl
define_method :replace_keys do |record=nil|
super(record)
owner[reflection.foreign_type] = ActiveRecord::Base.store_base_sti_class ? record.class.base_class.name : record.class.name
end
end
end
ActiveRecord::Associations::BelongsToPolymorphicAssociation.send(:include, AddPolymorphic)
A more systemic way to fix the issue is to edit the ActiveRecord core methods which govern it. I used references in this gem to find out which elements needed to be fixed / overridden.
This is untested and still needs extensions for some of the other parts of the ActiveRecord core methods, but seems to work for my local system.
There is a gem.
https://github.com/appfolio/store_base_sti_class
Tested and it works on various versions of AR.
You can also build a custom scope for a has_* association for the polymorphic type:
class Staff < ActiveRecord::Base
has_one :car,
->(s) { where(cars: { borrowable_type: s.class }, # defaults to base_class
foreign_key: :borrowable_id,
:dependent => :destroy
end
Since polymorphic joins use a composite foreign key (*_id and *_type) you need to specify the type clause with the correct value. The _id though should work with just the foreign_key declaration specifying the name of the polymorphic association.
Because of the nature of polymorphism it can be frustrating to know what models are borrowables, since it could conceivably be any model in your Rails application. This relationship will need to be declared in any model where you want the cascade deletion on borrowable to be enforced.
This is how I solved that problem using aforementioned hints:
# app/models/concerns/belongs_to_single_table_polymorphic.rb
module BelongsToSingleTablePolymorphic
extend ActiveSupport::Concern
included do
def self.belongs_to_sti_polymorphic(model)
class_eval "belongs_to :#{model}, polymorphic: true"
class_eval 'before_validation :set_sti_object_type'
define_method('set_sti_object_type') do
sti_type = send(model).class.name
send("#{model}_type=", sti_type)
end
end
end
end
and with that, for any model in which I would find belongs_to :whatever, polymorphic: true I do:
class Reservation < ActiveRecord::Base
include BelongsToSingleTablePolymorphic
# .....
belongs_to_sti_polymorphic :whatever
# .....
end
I agree with the general comments that this ought to be easier. That said, here is what worked for me.
I have a model with Firm as the base class and Customer and Prospect as the STI classes, as so:
class Firm
end
class Customer < Firm
end
class Prospect < Firm
end
I also have a polymorphic class, Opportunity, which looks like this:
class Opportunity
belongs_to :opportunistic, polymorphic: true
end
I want to refer to opportunities as either
customer.opportunities
or
prospect.opportunities
To do that I changed the models as follows.
class Firm
has_many opportunities, as: :opportunistic
end
class Opportunity
belongs_to :customer, class_name: 'Firm', foreign_key: :opportunistic_id
belongs_to :prospect, class_name: 'Firm', foreign_key: :opportunistic_id
end
I save opportunities with an opportunistic_type of 'Firm' (the base class) and the respective customer or prospect id as the opportunistic_id.
Now I can get customer.opportunities and prospect.opportunities exactly as I want.

Resources