学习ActiveModel:Dirty的使用方法
根据你的项目,你可能需要准确地知道你的模型中什么东西发生了变化,以做你必须做的事情,如果你需要知道在你的模型中定义的关联中发生了什么变化,这一点特别重要。
那么,当修改一个模型时,我们如何知道该模型内部发生了什么变化?
最简单的答案是。ActiveModel::Dirty,这很有效,除了它不考虑关联(这也是这篇文章的重点);那么我们如何获得关联中的变化?
首先介绍一下背景,在Ruby on Rails中,我使用Services和Wisper来保持我的模型的纤细,以及模型之外的外部逻辑,虽然与业务有关,但与实际模型本身无关。
既然如此,我们假设我有一个模型,根据我的业务规则,我必须在这个模型更新后创建/更新一个文件。我们怎样才能做到这一点呢?提示:不要使用回调,像after_save ,这是肯定的!
同样,让我们假设我的控制器使用一个服务来更新模型,在这个服务里面我使用Wispper来广播保存valid? 记录后的变化。到目前为止,一切都很好,很顺利。
但是......我如何确定关联中的变化?我如何知道在服务中,如果某个特定关联中的一个项目发生了变化,我应该广播什么?因为不管是什么原因,这个模型对一个特定的关联做了一些疯狂的事情,而且说实话,我只想在该关联或该关联中的任何项目发生变化时做出这些改变。
好吧,这就是我的解决方案,一点点的思考和关注。
module Dirtyable
extend ActiveSupport::Concern
WhatChanged = Struct.new(:object, :changes) do
def changed?
self != NoChanges
end
delegate :has_key?, to: :changes
end
NoChanges = WhatChanged.new(nil, nil)
def what_changed?
what_changed = nil
self.previous_changes.delete('updated_at')
unless self.previous_changes.empty?
what_changed = WhatChanged.new(self, self.previous_changes)
end
associations = self.class.reflections.select { |k, v| v.collection? }.keys
associations.each do |association_name|
catch :next_association_name do
association_collection = self.send(association_name)
throw :next_association_name unless association_collection
throw :next_association_name if association_collection.empty?
association_collection.each do |item|
throw :next_association_name unless item.respond_to?(:what_changed?)
association_changes = item.what_changed?
if association_changes != NoChanges
what_changed ||= WhatChanged.new(self, {})
what_changed.changes[association_name] ||= []
what_changed.changes[association_name] << association_changes
end
end
end
end
what_changed || NoChanges
end
end
上面的模块非常简单,它使用ActiveModel::Dirty 和反射来获取有变化的关联,然而重要的是,为了使其工作,所有你想跟踪的模型必须包括这个模块。
因此,举例来说,如果你有几个这样的模型。
class FancyModel < ActiveRecord::Base
include Dirtyable
has_many :things
end
class Thing < ActiveRecord::Base
include Dirtyable
belongs_to :fancy_model
end
当更新FancyModel时,你可以很容易地得到变化(并且在任何things ,例如在你的服务中,有变化),只需执行这样的调用,就可以得到什么变化。
what_changed = fancy_model.what_changed?
if what_changed.changed?
if what_changed.has_key?(:things)
# I do whatever I have to do... maybe call "broadcast(:on_things_changed)"?
end
end
非常酷。有了这个,你就可以在特定的事情发生时轻松地广播特定的事件,如果没有必要,就不需要广播一个全局的on_model_changed。