Using Delegate With has_many In Rails?
delegate
is just a shorthand as equivalent instance method. It's not a solution for all, and there are even some debate that it's not so explicit.
You can use an instance method when simple delegate
can't fit.
I reviewed and found any association is unnecessary is this case. The ImageMessage's class method caption is more like a constant, you can refer it directly.
def image_message_caption
ImageMessage.caption
end
Richard Peck
15+ yrs programming (VB/PHP/Ruby/C#), 5+ years graphic design (Photoshop/ZBrush/C4D/AE). Welcome to contact directly at [email protected] if you have specific questions.
Updated on June 07, 2022Comments
-
Richard Peck almost 2 years
We've got 2 models & a join model:
#app/models/message.rb Class Message < ActiveRecord::Base has_many :image_messages has_many :images, through: :image_messages end #app/models/image.rb Class Image < ActiveRecord::Base has_many :image_messages has_many :messages, through: :image_messages end #app/models/image_message.rb Class ImageMessage < ActiveRecord::Base belongs_to :image belongs_to :message end
Extra Attributes
We're looking to extract the extra attributes from the join model (
ImageMessage
) and have them accessible in theMessage
model:@message.image_messages.first.caption # -> what happens now @message.images.first.caption #-> we want
We've already achieved this using the
select
method when declaring the association:#app/models/message.rb has_many :images, -> { select("#{Image.table_name}.*", "#{ImageMessage.table_name}.caption AS caption") }, class_name: 'Image', through: :image_messages, dependent: :destroy
Delegate
We've just found the
delegate
method, which does exactly what this needs. However, it only seems to work forhas_one
andbelongs_to
associationsWe just got this working with a single association, but it seems it does not work for collections (just takes you to a public method)
Question
Do you know any way we could return the
.caption
attribute from theImageMessage
join model through theImage
model?We have this currently:
#app/models/image.rb Class Message < ActiveRecord::Base has_many :image_messages has_many :messages, through: :image_messages delegate :caption, to: :image_messages, allow_nil: true end #app/models/image_message.rb Class ImageMessage < ActiveRecord::Base belongs_to :image belongs_to :message def self.caption # -> only works with class method #what do we put here? end end
Update
Thanks to Billy Chan (for the instance method idea), we have got it working very tentatively:
#app/models/image.rb Class Image < ActiveRecord::Base #Caption def caption self.image_messages.to_a end end #app/views/messages/show.html.erb <%= @message.images.each_with_index do |i, index| %> <%= i.caption[index][:caption] %> #-> works, but super sketchy <% end %>
Any way to refactor, specifically to get it so that each time
.caption
is called, it returns theimage_message.caption
value for that particular record? -
Richard Peck over 10 yearsThanks for the heads-up! I have put this into the
Image
model, but it's returningundefined method
image_message'` error. I've tried referencingself.image_message
but no luck -
Billy Chan over 10 years@RichPeck, the method should be in Message model! It's for replacing
delegate
. -
Billy Chan over 10 yearsI updated code, it should be
image_messages
, the plural. My opinion is,delegate
is a shorthand to simplify and beautify code, when things getting unconventional, a normal instance method would be better. -
Richard Peck over 10 yearsThanks for the update! I had the
delegate
method in theImage
model - it seems you need to put the method into the model in which you want to call the method; for us, that will be@message.images.first.caption
, henceImage
. I'll see if I can get it working - big thanks for your help so far! -
Billy Chan over 10 yearsSorry I answered too quick. I checked it again and found any association is unnecessary in this case. The simplest way it
def image_message_caption; ImageMessage.caption; end;
-
Richard Peck over 10 yearsI think you still need the association - you're calling an associated object. We've done this before with validation, but this is different - I thought we had it working with
self.image_messages.first.caption
but that just brings back the first item in the collection (not the specific caption attribute) -
Richard Peck over 10 yearsGot it working very sketchily - I've updated my answer if you want to refactor?
-
Billy Chan over 10 yearsI'm not very aware of your point. But if that is the case, another workaround is to set such instance method in ImageMessages and force it to return the class method.
def caption; self.class.caption; end
-
Richard Peck over 10 yearsThanks Billy - how would you call the ImageMessage caption method?
-
Billy Chan over 10 yearsI misunderstood your question. The fact would be each image_message has a unique caption and you failed to fetch that. If that is the case, "image_messages" is the collection, you can only use
pluck
or iterate the collection to get captions. -
Richard Peck over 10 yearsThanks for your reply again buddy. Although this did not work as I hoped, it definitely gave us some ideas!