【问题标题】:Memoized class variable returning inconsistent valuesMemoized 类变量返回不一致的值
【发布时间】:2017-08-31 02:45:53
【问题描述】:

如果这个问题看起来很乱,我真的很抱歉;我会尽量简明扼要。

我正在构建一个模拟 ActiveRecord 模型的类,但它的数据来自名为 Airtable 的服务,而不是数据库。 Airtable 就像 Excel 和数据库之间的交叉 - 它允许您创建数据电子表格,但支持不同表之间的“外键”,因此您可以链接表之间的数据。这对于我正在开发的应用程序来说非常有效。

为了使其具有可扩展性和灵活性,我创建了一个父类AirtableModel,它定义了类继承时将填充的常用方法和属性。继承类的名称将帮助父方法从正确的 Airtable 表中访问数据并检索正确的属性。相关位如下(未提及的位不言自明或与问题无关):

class AirtableModel
  def initialize(hash)
    hash.each do |attribute_name, attribute_value|
      attribute_value = self.class.first_value_from_arrays_with_singular_key_name(attribute_name, attribute_value)
      # ^^^ Airtable always returns references as Arrays. If the relationship is a belongs_to, we pull out the value from the Array.

      begin
        attribute_name_as_class = attribute_name.to_s.singularize.camelize.constantize
        # ^^^ Converts the attribute's name to a class constant. Used to make the generated method retrieve class records instead of ids. If the class doesn't exist, its NameError is caught below.
        instance_variable_set("@#{attribute_name}_airtable_ids", attribute_value)

        self.class.send(:define_method, attribute_name.to_sym) do
          result = attribute_name_as_class.find_all_by_airtable_id(instance_variable_get("@#{attribute_name}_airtable_ids"))
          result.length <= 1 ? result.first : result
        end
      rescue NameError
        # Triggered if `attribute_name_as_class` doesn't match an existing class
        instance_variable_set("@#{attribute_name}", attribute_value)
        self.class.send(:define_method, attribute_name.to_sym) do
          instance_variable_get("@#{attribute_name}")
        end
      end
    end
  end

  # Reaches out to Airtable to get all records for this class's table (the Airtable table matches the class name). Collects the resulting data into an array of Hashes.
  # One such hash might look like this:
  #   {
  #     'id' => <unique string ID assigned by Airtable>,
  #     'fields' => {
  #       'db_id' => <Unique integer ID. I added this to emulate a database record>,
  #       ...
  #     }
  #   }
  def self.airtable
    @airtable_records ||= AirtableService.records_from_table(table_name: "#{self}s").each.map do |raw|
      object_properties = raw['fields']
      object_properties['airtable_id'] = raw['id']
      object_properties['id'] = object_properties['db_id']

      Hash[object_properties.collect { |k, v| [k.snakecase.parameterize.underscore.to_sym, v] }]
      # ^^^ Converts parameter name to snake-case symbol, i.e. :db_id
    end
  end

  def self.all
    @all_records ||= airtable.map { |b| new(b) }
  end

  def self.find_by_airtable_id(airtable_id)
    objects = all.select { |b| b.airtable_id == airtable_id }
    raise "non unique airtable_id found" if objects.size > 1
    objects.first
  end

  def self.find_all_by_airtable_id(airtable_ids)
    [airtable_ids].flatten.map { |aid| find_by_airtable_id(aid) }
    # ^^^ Accomodates airtable_ids as an Array or a single value
  end

  def self.first
    all.first
  end

  def self.last
    all.last
  end
end

如果以上任何内容没有意义,请告诉我,我很乐意更新。

这对于我从AirtableModel 继承的大多数类都非常有效,但是我遇到了一个特定表 (FooBar) 的问题,该表应该像其他两个表之间的连接表一样。看起来像这样:

[Table Foo]                   [Table FooBar]                  [Table Bar]
fooBars <==========---------> foo     bar <---------========> fooBars

他们的类定义很简单:

class Foo < AirtableModel
end

class FooBar < AirtableModel
end

class Bar < AirtableModel
end

感谢上面的构造函数,我可以像Foo.first.foo_bars 一样调用并返回一个包含与此Foo 相关的所有FooBar 实例的数组。这在控制台中没有问题,但我在我的 Rails 应用程序中尝试上述 sn-p 时遇到了问题。

foo_bars 在单个控制器创建操作中被调用两次。这恰好调用了两次self.all。第一次,我得到了预期的结果——@all_records 等于我在 Airtable 中的记录数,具有正确的属性值,包括外键关系。但是,第二次输入该方法时,@all_records 的值变为一个空数组。调用foo_bars 的对象没有改变,仍然包含正确的airtable_ids,用于查找关联的FooBar 实例。 @airtable_records - self.airtable 方法的返回值 - 仍然具有相同的值。

我不确定是什么导致记忆化的@all_records 变量改变值。我一直在努力解决它,使用调试器逐步跟踪函数调用,但我看不出是什么导致值发生变化。任何人都可以就如何进一步调试提供任何建议吗?我将不胜感激。

【问题讨论】:

  • 我承认我没有彻底阅读您的问题(它有点冗长),但有一点跳出来有点不合常规 - 您正在记忆类方法中的实例变量。尽管您可以在 ruby​​ 中做到这一点,因为类也是对象,这通常不是本意。相反,我想知道您是否打算使用类变量,例如@@airtable_records。注意@@ 用于类变量,而不是@ 用于实例变量。
  • Class 实例的@SeanHuber 实例变量(自定义类都是Class 类的实例)是一个完全有效的模式。此外,不鼓励在 ruby​​ 中使用 @@ 类变量,因为它们在派生类上的行为很奇怪。
  • 很公平,我收回我的评论。记忆化的类级别变量对我来说似乎仍然像代码气味。如果目标是让单个对象保存一些信息,我认为您应该采用单例方法。
  • @SeanHuber Singleton。为什么我没有想到呢?好主意啊。我会试一试并在这里更新。谢谢!
  • @SeanHuber 也是,是的,很抱歉这个冗长的问题。这个实现有很多深度。我实际上使用@@ 尝试了类变量,但它产生了..奇怪的结果。

标签: ruby-on-rails ruby airtable


【解决方案1】:

事实证明这个答案真的很愚蠢。

all 正在返回一个对象数组。在类的其他地方,我们有这个方法:

def self.where(filter = {})
    filtered_objects = all

    filter.each do |filter_property, filter_value|
      # filter_value = filter_value.airtable_id if filter_value.respond_to?(:airtable_id)
      filtered_objects.select! do |object|
        object_value = object.send(filter_property)

        match_check = lambda do |value|
          if object_value.is_a?(Array)
            object_value.include?(value)
          else
            object_value == value
          end
        end

        filter_value.is_a?(Array) ? filter_value.any? { |v| match_check.call(v) } : match_check.call(filter_value)
      end
    end

    filtered_objects
  end

如果filtered_objects == all,我们在filtered_objects 上调用select!,会发生什么?

是的。它直接修改了对象引用。让all 返回一个.dup'd 版本的数组可以解决问题。

【讨论】:

  • 很高兴你知道了!
  • 感谢@SeanHuber!希望这对其他人有所帮助。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-11-21
  • 2020-06-10
  • 2015-08-29
  • 2019-01-28
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多