Memoized类变量返回不一致的值

时间:2021-11-03 23:26:14

I'm really sorry in advance if this question is going to look messy; I'll do my best to make it concise.

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

I am building a class that emulates an ActiveRecord model, but gets its data from a service called Airtable, not a database. Airtable is like a cross between Excel and a database - it lets you create a spreadsheet of data but supports "foreign keys" between different tables so you can link data between tables. This works out really nicely for an application I'm working on.

我正在构建一个模拟ActiveRecord模型的类,但是从名为Airtable的服务获取其数据,而不是数据库。 Airtable就像Excel和数据库之间的交叉 - 它允许您创建数据电子表格,但支持不同表之间的“外键”,以便您可以在表之间链接数据。这对我正在处理的应用程序非常好。

In order to make it extensible and flexible, I created a parent class, AirtableModel, which defines common methods and attributes that will be filled out when classes inherit from it. The inheriting class's name will help the parent methods access the data from the correct Airtable table and retrieve the correct attributes. The relevant bits are below (ones that aren't mentioned are self-explanatory or are inconsequential to the problem):

为了使它具有可扩展性和灵活性,我创建了一个父类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

If anything above doesn't make sense, let me know and I'll be happy to update.

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

This has worked perfectly for the majority of my classes that inherit from AirtableModel, but I'm having an issue with a particular table (FooBar) which is supposed to act like a join table between two other tables. That would look something like this:

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

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

Their class definitions are very simple:

他们的类定义非常简单:

class Foo < AirtableModel
end

class FooBar < AirtableModel
end

class Bar < AirtableModel
end

Thanks to the constructors above, I can make a call like Foo.first.foo_bars and get back an Array of all FooBar instances related to this Foo. This works without issue in the console, but I am running into an issue trying the above snippet in my Rails application.

感谢上面的构造函数,我可以像Foo.first.foo_bars那样进行调用,并获取与此Foo相关的所有FooBar实例的数组。这在控制台中没有问题,但我遇到了在我的Rails应用程序中尝试上述代码片段的问题。

foo_bars gets called twice in a single controller create action. This happens to call self.all twice. The first time, I get the expected result back - @all_records equals the number of records I have in Airtable, with the proper attribute values, including foreign key relationships. However, the second time the method is entered, the value of @all_records changes to an empty Array. The object that invokes foo_bars hasn't changed, and still includes the correct airtable_ids which are used to look up the associated FooBar instances. @airtable_records - the return value from the self.airtable method - still has the same values too.

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

I'm not sure what's causing the memoized @all_records variable to change value. I've been banging my head against it, using a debugger to follow the function calls step by step, but I can't see what's causing the value to change. Can anyone provide any advice on how to debug this further? I'd greatly appreciate it.

我不确定是什么导致备忘的@all_records变量改变了值。我一直在抨击它,使用调试器逐步跟踪函数调用,但我看不出是什么导致值发生变化。任何人都可以提供有关如何进一步调试的建议吗?我非常感谢。

1 个解决方案

#1


1  

It turns out the answer is really stupid.

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

all is returning an Array of objects. Elsewhere in the class, we have this method:

所有都返回一个对象数组。在课堂的其他地方,我们有这个方法:

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

If filtered_objects == all, and we call select! on filtered_objects, what would happen?

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

Yup. It modified the object references directly. Making all return a .dup'd version of the Array solves the problem.

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

#1


1  

It turns out the answer is really stupid.

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

all is returning an Array of objects. Elsewhere in the class, we have this method:

所有都返回一个对象数组。在课堂的其他地方,我们有这个方法:

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

If filtered_objects == all, and we call select! on filtered_objects, what would happen?

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

Yup. It modified the object references directly. Making all return a .dup'd version of the Array solves the problem.

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