如何跟踪块内“被叫”的对象?

时间:2017-08-29 12:02:47

标签: ruby-on-rails ruby

问题:

  • 我需要知道在块内调用的记录属性(比如我需要类似下面的内容):

    def my_custom_method(&block)
      some_method_that_starts_tracking
    
      block.call
    
      some_method_that_stops_tracking
    
      puts some_method_that_returns_called_records_attributes
      do_something_about(some_method_that_returns_called_records_attributes)
    end
    
    my_custom_method { somecodethatcallsauthorofbook1andemailandfirstnameofuser43 }
    
    # this is the `puts` output above (just as an example)
    # => {
    #      #<Book id:1...>  => [:author],
    #      #<User id:43...> => [:email, :first_name]
    #    }
    
  • 块内的代码可以是任何

  • 具体来说,我的意思是跟踪ApplicationRecord的子类的任何实例,因此它可以是BookUser等任何模型的实例......

尝试:

  • 根据我的理解,这类似于rspec在预期调用方法时的工作方式。它以某种方式跟踪该方法的任何调用。所以,我最初的尝试是做类似以下的事情(这还没有完全奏效):

    def my_custom_method(&block)
      called_records_attributes = {}
    
      ApplicationRecord.descendants.each do |klass|
        klass.class_eval do
          attribute_names.each do |attribute_name|
            define_method(attribute_name) do
              called_records_attributes[self] ||= []
              called_records_attributes[self] << attribute_name
              self[attribute_name]
            end
          end
        end
      end
    
      block.call
    
      # the above code will work but at this point, I don't know how to clean the methods that were defined above, as the above define_methods should only be temporary
    
      puts called_records_attributes
    end
    
    my_custom_method { Book.find_by(id: 1).title }
    # => {
    #      #<Book id: 1...> => ['title']
    #    }
    
    • 上面的.descendants可能不是一个好主意,因为如果我没弄错的话,Rails会使用autoload
    • 正如上面在评论中已经说过的那样,我不知道如何删除这些“defined_methods”,它们只是在这个“块”的持续时间内只是暂时的。
    • 此外,我上面的代码可能会覆盖模型的“实际”属性getter,如果已经定义了任何一个,那就不好了。

背景:

  • 我正在编写一个gem live_record,我正在添加一个新功能,允许开发人员只需编写类似

    的内容
    <!-- app/views/application.html.erb -->
    <body>
      <%= live_record_sync { @book.some_custom_method_about_book } %>
    </body>
    

...将在页面上呈现@book.some_custom_method_about_book,但同时live_record_sync包装器方法会记录在该块内调用的所有属性(即在some_custom_method_about_book内调用@book.title,然后将这些属性设置为块自己的“依赖关系”,稍后当该特定书籍的属性更新时,我也可以直接更新HTML该属性是“依赖”的页面,如上所述。我知道这不是一个准确的解决方案,但我想先通过实验来开辟我的机会。

- Rails 5

1 个答案:

答案 0 :(得分:0)

免责声明:我认为这只是一个平庸的解决方案,但希望可以帮助任何有同样问题的人。

我尝试阅读 rspec 源代码,但因为我无法轻易理解幕后发生的事情,而且我发现 rspec & #39; s(ie)expect(Book.first).to receive(:title)与我真正想要的不同,因为已经指定了方法(即:title),而我想要的是跟踪任何属性的方法,因为在这两个原因中,我跳过了进一步的阅读,并尝试了我自己的解决方案,希望以某种方式工作;见下文。

请注意,我在这里使用Thread local-storage,因此此代码应该是线程安全的(尚未测试)。

# lib/my_tracker.rb
class MyTracker
  Thread.current[:my_tracker_current_tracked_records] = {}

  attr_accessor :tracked_records

  class << self
    def add_to_tracked_records(record, attribute_name)
      Thread.current[:my_tracker_current_tracked_records][{model: record.class.name.to_sym, record_id: record.id}] ||=  []
      Thread.current[:my_tracker_current_tracked_records][{model: record.class.name.to_sym, record_id: record.id}] << attribute_name
    end
  end

  def initialize(block)
    @block = block
  end

  def call_block_while_tracking_records
    start_tracking

    @block_evaluated_value = @block.call
    @tracked_records = Thread.current[:my_tracker_current_tracked_records]

    stop_tracking
  end

  def to_s
    @block_evaluated_value
  end

  # because I am tracking record-attributes, and you might want to track a different object / method, then you'll need to write your own `prepend` extension (look for how to use `prepend` in ruby)
  module ActiveRecordExtensions
    def _read_attribute(attribute_name)
      if Thread.current[:my_tracker_current_tracked_records] && !Thread.current[:my_tracker_is_tracking_locked] && self.class < ApplicationRecord
        # I added this "lock" to prevent infinite loop inside `add_to_tracked_records` as I am calling the record.id there, which is then calling this _read_attribute, and then loops.
        Thread.current[:my_tracker_is_tracking_locked] = true
        ::MyTracker.add_to_tracked_records(self, attribute_name)
        Thread.current[:my_tracker_is_tracking_locked] = false
      end
      super(attribute_name)
    end
  end

  module Helpers
    def track_records(&block)
      my_tracker = MyTracker.new(block)
      my_tracker.call_block_while_tracking_records
      my_tracker
    end
  end

  private

  def start_tracking
    Thread.current[:my_tracker_current_tracked_records] = {}
  end

  def stop_tracking
    Thread.current[:my_tracker_current_tracked_records] = nil
  end
end

ActiveSupport.on_load(:active_record) do
  prepend MyTracker::ActiveRecordExtensions
end

ActiveSupport.on_load(:action_view) do
  include MyTracker::Helpers
end

ActiveSupport.on_load(:action_controller) do
  include MyTracker::Helpers
end

使用示例

<强> some_controller.rb

book = Book.find_by(id: 1)
user = User.find_by(id: 43)

my_tracker = track_records do
  book.title
  if user.created_at == book.created_at
    puts 'same date'
  end
  'thisisthelastlineofthisblockandthereforewillbereturned'
end

puts my_tracker.class
# => #<MyTracker ... >

puts my_tracker.tracked_records
# => {
#      {model: :Book, record_id: 1} => ['title', 'created_at'],
#      {model: :User, record_id: 43} => ['created_at']
#    }

puts my_tracker
# => 'thisisthelastlineofthisblockandthereforewillbereturned'
# notice that `puts my_tracker` above prints out the block itself
# this is because I defined `.to_s` above.
# I need this `.to_s` so I can immediately print the block as-is in the views.
# see example below

<强> some_view.html.erb

<%= track_records { current_user.email } %>

P.S。也许我把它作为一个宝石包装起来会更好。如果您有兴趣,请告诉我

相关问题