I have a model which has a dependency on a separate, joined model.
class Magazine < ActiveRecord::Base
has_one :cover_image, dependent: :destroy, as: :imageable
end
class Image < ActiveRecord::Base
belongs_to :imageable, polymorphic: true
end
Images are polymorphic and can be attached to many objects (pages and articles) not just magazines.
The magazine also stores a screenshot of itself that can be used for publicising it:
class Magazine < ActiveRecord::Base
has_one :cover_image, dependent: :destroy, as: :imageable
has_one :screenshot
def generate_screenshot
# go and create a screenshot of the magazine
end
end
Now if the image changes, the magazine also needs to update its screenshot. So the magazine really needs to know when something happens to the image.
class Image < ActiveRecord::Base
belongs_to :imageable, polymorphic: true
after_save { update_any_associated_magazine }
def update_any_associated_magazine
# figure out if this belongs to a magazine and trigger
# screenshot to regenerate
end
end
However the image could be used in lots of different objects and really shouldn't be doing actions specific to the Magazine as it's not the Image's responsibility to worry about. The image might be attached to pages or articles as well and doesn't need to be doing all sorts of stuff for them.
If we were to take a Rails(y) approach then we could create a third party observer that would then trigger an event on the associated magazine:
class ImageObserver < ActiveRecord::Observer
observe :image
def after_save image
Magazine.update_magazine_if_includes_image image
end
end
However this feels like a bit of a crappy solution to me.
We've avoided the Image being burdened by updating the magazine which was great but we've really just punted the problem downstream. It's not obvious that this observer exists, it's not clear inside the Magazine object that the update to the Image will in fact trigger an update to the magazine and we've got a weird floating object which has logic that really just belongs in Magazine.
I don't want an observer - I just want one object to be able to subscribe to events on another object.
What I would much rather do is have the magazine subscribe directly to events on the image. So the code would instead look like:
class Magazine < ActiveRecord::Base
...
Image.add_after_save_listener Magazine, :handle_image_after_save
def self.handle_image_after_save image
# determine if image belongs to magazine and if so update it
end
end
class Image < ActiveRecord::Base
...
def self.add_after_save_listener class_name, method
@@after_save_listeners << [class_name, method]
end
after_save :notify_after_save_listeners
def notify_after_save_listeners
@@after_save_listeners.map{ |listener|
class_name = listener[0]
listener_method = listener[1]
class_name.send listener_method
}
end
This pattern seems sensible to me. It uses class variables and methods so doesn't make any assumptions of particular instances being available.
However, I'm old enough and wise enough now to know that if something seemingly obvious hasn't been done already in Rails there's probably a good reason for it.
This seems cool to me. What's wrong with it though? Why do all the other solutions I see all draft in a third party object to deal with things? Would this work?
I use Redis:
In an initializer I set up Redis:
# config/initializers/redis.rb
uri = URI.parse ENV.fetch("REDISTOGO_URL", 'http://127.0.0.1:6379')
REDIS_CONFIG = { host: uri.host, port: uri.port, password: uri.password }
REDIS = Redis.new REDIS_CONFIG
It'll default to my local redis installation in development but on Heroku it'll use Redis To Go.
Then I publish using model callbacks:
class MyModel < ActiveRecord::Base
after_save { REDIS.publish 'my_channel', to_json }
end
Then I can subscribe from anywhere, such as a controller I'm using to push events using Event Source
class Admin::EventsController < Admin::BaseController
include ActionController::Live
def show
response.headers["Content-Type"] = "text/event-stream"
REDIS.psubscribe params[:event] do |on|
on.pmessage do |pattern, event, data|
response.stream.write "event: #{event}\n"
response.stream.write "data: #{data}\n\n"
end
end
rescue IOError => e
logger.info "Stream closed: #{e.message}"
ensure
redis.quit
response.stream.close
end
end
Redis is great for flexible pub/sub. That code I have in the controller can be placed anywhere, let's say in an initializer:
# config/initializers/subscribers.rb
REDIS.psubscribe "image_update_channel" do |on|
on.pmessage do |pattern, event, data|
image = Image.find data['id']
image.imageable # update that shiz
end
end
Now that will handle messages when you update your image:
class Image < ActiveRecord::Base
belongs_to :imageable, polymorphic: true
after_save { REDIS.publish 'image_update_channel', to_json }
end
There is ActiveSupport Notifications mechanism for implementing pub/sub in Rails.
First, you should define instrument which will publish events:
class Image < ActiveRecord::Base
...
after_save :publish_image_changed
private
def publish_image_changed
ActiveSupport::Notifications.instrument('image.changed', image: self)
end
end
Then you should subscribe for this event (you can put this code in initializer):
ActiveSupport::Notifications.subscribe('image.changed') do |*args|
event = ActiveSupport::Notifications::Event.new(*args)
image = event.payload[:image]
# If you have no other cases than magazine, you can check it when you publish event.
return unless image.imageable.is_a?(Magazine)
MagazineImageUpdater.new(image.imageable).run
end
I'll give it a shot...
Use public_send to notify the parent class of a change:
class BaseModel < ActiveRecord::Base
has_one :child_model
def respond_to_child
# now generate the screenshot
end
end
class ChildModel < ActiveRecord::Base
belongs_to :base_model
after_update :alert_base
def alert_base
self.base_model.public_send( :respond_to_child )
end
end
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With