Using Delegate With has_many In Rails?

10,287

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
Share:
10,287
Richard Peck
Author by

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, 2022

Comments

  • Richard Peck
    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 the Message 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 for has_one and belongs_to associations

    We 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 the ImageMessage join model through the Image 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 the image_message.caption value for that particular record?

  • Richard Peck
    Richard Peck over 10 years
    Thanks for the heads-up! I have put this into the Image model, but it's returning undefined method image_message'` error. I've tried referencing self.image_message but no luck
  • Billy Chan
    Billy Chan over 10 years
    @RichPeck, the method should be in Message model! It's for replacing delegate.
  • Billy Chan
    Billy Chan over 10 years
    I 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
    Richard Peck over 10 years
    Thanks for the update! I had the delegate method in the Image 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, hence Image. I'll see if I can get it working - big thanks for your help so far!
  • Billy Chan
    Billy Chan over 10 years
    Sorry 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
    Richard Peck over 10 years
    I 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
    Richard Peck over 10 years
    Got it working very sketchily - I've updated my answer if you want to refactor?
  • Billy Chan
    Billy Chan over 10 years
    I'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
    Richard Peck over 10 years
    Thanks Billy - how would you call the ImageMessage caption method?
  • Billy Chan
    Billy Chan over 10 years
    I 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
    Richard Peck over 10 years
    Thanks for your reply again buddy. Although this did not work as I hoped, it definitely gave us some ideas!