7

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?

strivedi183
  • 4,749
  • 2
  • 31
  • 38
Richard Peck
  • 76,116
  • 9
  • 93
  • 147

1 Answers1

8

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
Undo
  • 25,519
  • 37
  • 106
  • 129
Billy Chan
  • 24,625
  • 4
  • 52
  • 68
  • 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 – Richard Peck Dec 28 '13 at 15:30
  • @RichPeck, the method should be in Message model! It's for replacing `delegate`. – Billy Chan Dec 28 '13 at 15:33
  • 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. – Billy Chan Dec 28 '13 at 15:37
  • 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! – Richard Peck Dec 28 '13 at 15:39
  • 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;` – Billy Chan Dec 28 '13 at 15:43
  • 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 Dec 28 '13 at 15:49
  • Got it working very sketchily - I've updated my answer if you want to refactor? – Richard Peck Dec 28 '13 at 16:04
  • 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` – Billy Chan Dec 28 '13 at 16:05
  • Thanks Billy - how would you call the ImageMessage caption method? – Richard Peck Dec 28 '13 at 16:09
  • 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. – Billy Chan Dec 28 '13 at 16:15
  • Thanks for your reply again buddy. Although this did not work as I hoped, it definitely gave us some ideas! – Richard Peck Dec 28 '13 at 17:55