Edit: In retrospect this isn't that great of an idea. You are putting functionality that belongs to ZipWithCBSA
into the models of others. The models receiving the concern act as they are supposed to, and the fact that ZipWithCBSA
responds to :store_locations
should be obvious in some capacity from ZipWithCBSA
. It has nothing to do with the other models/concern. Props to Robert Nubel for making this obvious with his potential solutions.
Is it possible to both has_many and belongs_to relationships in a single concern?
I have a table ZipWithCBSA
that essentially includes a bunch of zip code meta information.
I have two models that have zip codes: StoreLocation
and PriceSheetLocation
. Essentially:
class ZipWithCBSA < ActiveRecord::Base
self.primary_key = :zip
has_many :store_locations, foreign_key: :zip
has_many :price_sheet_locations, foreign_key: :zip
end
class StoreLocation< ActiveRecord::Base
belongs_to :zip_with_CBSA, foreign_key: :zip
...
end
class PriceSheetLocation < ActiveRecord::Base
belongs_to :zip_with_CBSA, foreign_key: :zip
...
end
There are two properties from ZipWithCBSA
that I always want returned with my other models, including on #index
pages. To prevent joining this table for each item every time I query it, I want to cache two of these fields into the models themselves -- i.e.
# quick schema overview~~~
ZipWithCBSA
- zip
- cbsa_name
- cbsa_state
- some_other_stuff
- that_I_usually_dont_want
- but_still_need_access_to_occasionally
PriceSheetLocation
- store
- zip
- cbsa_name
- cbsa_state
StoreLocation
- zip
- generic_typical
- location_address_stuff
- cbsa_name
- cbsa_state
So, I've added
after_save :store_cbsa_data_locally, if: ->(obj){ obj.zip_changed? }
private
def store_cbsa_data_locally
if zip_with_cbsa.present?
update_column(:cbsa_name, zip_with_cbsa.cbsa_name)
update_column(:cbsa_state, zip_with_cbsa.cbsa_state)
else
update_column(:cbsa_name, nil)
update_column(:cbsa_state, nil)
end
end
I'm looking to move these into concerns, so I've done:
# models/concerns/UsesCBSA.rb
module UsesCBSA
extend ActiveSupport::Concern
included do
belongs_to :zip_with_cbsa, foreign_key: 'zip'
after_save :store_cbsa_data_locally, if: ->(obj){ obj.zip_changed? }
def store_cbsa_data_locally
if zip_with_cbsa.present?
update_column(:cbsa_name, zip_with_cbsa.cbsa_name)
update_column(:cbsa_state, zip_with_cbsa.cbsa_state)
else
update_column(:cbsa_name, nil)
update_column(:cbsa_state, nil)
end
end
private :store_cbsa_data_locally
end
end
# ~~models~~
class StoreLocation < ActiveRecord::Base
include UsesCBSA
end
class PriceSheetLocation < ActiveRecord::Base
include UsesCBSA
end
This is all working great-- but I still need to manually add the has_many relationships to the ZipWithCBSA model:
class ZipWithCBSA < ActiveRecord::Base
has_many :store_locations, foreign_key: zip
has_many :price_sheet_locations, foreign_key: zip
end
Is it possible to re-open ZipWithCBSA
and add the has_many
relationships from the concern? Or in any other more-automatic way that allows me to specify one single time that these particular series of models are bffs?
I tried
# models/concerns/uses_cbsa.rb
...
def ZipWithCBSA
has_many self.name.underscore.to_sym, foregin_key: :zip
end
...
and also
# models/concerns/uses_cbsa.rb
...
ZipWithCBSA.has_many self.name.underscore.to_sym, foregin_key: :zip
...
but neither worked. I'm guessing it has to do with the fact that those relationships aren't added during the models own initialization... but is it possible to define the relationship of a model in a separate file?
I'm not very fluent in metaprogramming with Ruby yet.
Your best bet is probably to add the relation onto your ZipWithCBSA model at the time your concern is included into the related models, using class_exec
on the model class itself. E.g., inside the included
block of your concern, add:
relation_name = self.table_name
ZipWithCBSA.class_exec do
has_many relation_name, foreign_key: :zip
end