2017-08-31 42 views
0

如果這個問題看起來很雜亂,我真的很抱歉;我會盡力使其簡潔。記憶類變量返回不一致的值

我正在構建一個模擬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並取回所有FooBar實例數組與此相關Foo。這在控制檯中沒有問題,但我遇到了一個問題,在我的Rails應用程序中嘗試上面的代碼片段。

foo_bars在單個控制器創建操作中被調用兩次。這發生兩次呼叫self.all。第一次,我得到預期的結果 - @all_records等於我在Airtable中的記錄數,並帶有正確的屬性值,包括外鍵關係。但是,第二次輸入方法時,@all_records的值更改爲空數組。調用foo_bars的對象沒有更改,並且仍包含用於查找相關聯的實例的正確airtable_ids@airtable_records - 來自self.airtable方法的返回值 - 仍然具有相同的值。

我不確定是什麼導致記憶@all_records變量改變價值。我一直在嘲弄我的頭,用調試器逐步跟蹤函數調用,但我看不到是什麼原因導致值發生改變。任何人都可以提供關於如何進一步調試的建議嗎?我將不勝感激。

+0

我承認我沒有徹底地讀過你的問題(這有點冗長),但有一件事跳出來有點不合常規 - 你正在記憶一個類方法中的實例變量。雖然你可以用ruby做到這一點,但因爲類也是對象,所以這通常不是意圖。相反,我想知道是否打算使用類變量,例如'@@ airtable_records'。注意類變量的@@'而不是'@'作爲實例變量。 –

+0

@ Class實例的@SeanHuber實例變量(自定義類是'Class'類的所有實例)是完全有效的模式。此外,在ruby中使用'@@'類變量會因派生類的奇怪行爲而受到阻礙。 – mudasobwa

+0

夠公平的,我收回我的評論。一個memoized的類級別變量對我來說仍然看起來像代碼味道。如果目標是讓一個對象擁有一些信息,我想你會想用單例方法。 –

回答

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,並呼籲select!filtered_objects,會發生什麼?

是。它直接修改了對象引用。使all返回一個.dup'd版本的數組解決了這個問題。

+0

很高興你知道了! –

+0

謝謝@SeanHuber!希望這可以幫助別人。 – Argus9