在Ruby on Rails中最佳化ActiveRecord的方法_ruby專題

來源:互聯網
上載者:User

 Ruby on Rails 編程常常會將您寵壞。這一不斷髮展的架構會讓您從其他架構的沉悶乏味中解脫出來。您可以用習以為常的幾行代碼片斷表達自己的意圖。而且還可以使用 ActiveRecord。

對於我這樣的一個老 Java? 程式員而言,ActiveRecord 多少有點生疏。通過 Java 架構,我通常都會在獨立的模型和模式之間構建一種映射。像這樣的架構就是映射架構。通過 ActiveRecord,我只定義資料庫模式:或者用 SQL 或者用稱為遷移(migration)的 Ruby 類。將物件模型設計建立於資料庫結構之上的那些架構稱為封裝架構。與大多數封裝架構不同,Rails 能通過查詢資料庫表發現物件模型的特徵。與構建複雜查詢不同,我使用模型在 Ruby(而非 SQL)中遍曆關係。這樣一來,我既獲得了封裝架構的簡單性,又具備了映射架構的大部分功能。ActiveRecord 便於使用和擴充。有時,甚至有些過於簡單。

與任何資料庫架構一樣,ActiveRecord 讓我極易做出很多惹麻煩的事。我所能擷取的列太多,又很容易遺漏重要的結構化資料庫特性,比如索引或空約束。我並不是說 ActiveRecord 是個不好的架構。只不過若是需要擴充,您需要知道如何堅固自己的應用程式。在本篇文章中,我將帶您親曆在使用 Rails 這一獨樹一幟的持久性架構時可能需要的一些重要最佳化。
基礎管理

產生受模式支援的模型異常容易,只需很少的代碼,即 script/generate model model_name。正如您所知,該命令可產生模型、遷移、單元測試甚至一個預設的 fixture。在該遷移中填上一些資料列,並輸入一些測試資料、編寫幾個測試、添加幾個驗證就算大功告成,這樣做真是很有誘惑力。但請您三思而行。您應該考慮總體的資料庫設計,要特別注意以下這些事情:

  •     Rails 不會讓您擺脫基本的資料庫效能問題。資料庫需要資訊,這些資訊經常以索引的格式才能有不錯的效能。
  •     Rails 不會讓您擺脫資料完整性問題。雖然大多數 Rails 開發人員都不喜歡在資料庫中保留限制,但您應該考慮像空列這樣的事情。
  •     Rails 為很多元素提供了方便的預設屬性。有時,像文字欄位的長度這樣的預設屬性對於大多數實用的應用程式而言都會過大。
  •     Rails 不會強制您建立有效資料庫設計。

在您繼續跋涉,深入學習 ActiveRecord 之前,應該首先確保您已經打好了足夠的基礎。請確保索引結構可以為您所用。如果給定的表很大,如果將在列上而不是 id 上搜尋,如果索引能對您有所協助(更多細節,請參見資料庫管理員文檔 —— 不同的資料庫以不同方式使用索引),那麼就需要建立索引。無需採用 SQL 建立索引 —— 可以簡單地使用遷移建立。可以輕鬆地使用 create_table 遷移建立索引,也可以建立一個額外的遷移來建立索引。以下是一個遷移樣本,可用來為 ChangingThePresent.org (請參見 參考資料)建立索引:
清單 1. 在遷移中建立索引

class AddIndexesToUsers < ActiveRecord::Migration def self.up  add_index :members, :login  add_index :members, :email  add_index :members, :first_name  add_index :members, :last_name end def self.down  remove_index :members, :login  remove_index :members, :email  remove_index :members, :first_name  remove_index :members, :last_name endend

ActiveRecord 會負責 id 上的索引,我顯式地添加了可在各種搜尋中使用的索引,原因是此表很大、不經常更新卻經常被搜尋。通常,我們會等到對給定的查詢中的問題有一定的把握後才會採取相應動作。這種策略可以讓我們不必二次猜測資料庫引擎。但從使用者這方面來看,我們知道該表將會很快具有數百萬的使用者,如果在經常搜尋的列上沒有索引,該表的效率會很低。

另外兩個常見問題也與遷移有關。如果字串和列都不應該為空白,那麼就請確保正確編寫了遷移。大多數 DBA(資料庫管理員)都會認為 Rails 為空白列提供了錯誤的預設屬性:預設情況下列可以為空白。如果希望建立一個不可為空的列,您必須顯式地添加參數 :null => false。如果具有字串列,請務必確保編寫應用程式的限值。預設地,Rails 遷移會將 string 列按 varchar(255) 編碼。通常,這個值過於龐大。應該盡量保持能如實反應應用程式的資料庫結構。與提供無任何限制的 login 相反,如果應用程式限制 login 只能為 10 個字元,那麼就應該相應地編寫資料庫,如清單 2 所示:
清單 2. 用限值和非空列編寫遷移

t.column :login, :string, :limit => 10, :null => false

此外,還應該考慮預設值以及其他任何能安全提供的資訊。通過一點預備工作,就可以節省日後跟蹤資料完整性問題的大量時間。在考慮資料庫基礎的同時,還應該注意哪些頁是靜態且容易緩衝的。在最佳化查詢和快取頁面面這兩個選項當中,如果您能 “消受” 複雜性,快取頁面面將會帶來更大的回報。有時,頁面或片段都是純靜態,比如一列狀態或一組經常問到的問題。在這種情況下,緩衝更勝一籌。而在其他的一些時候,您可能會決定犧牲資料庫效能,以減少複雜性。對於 ChangingThePresent,根據問題和環境的具體情況,我們二者都嘗試了。如果您也決定要犧牲查詢效能,就請繼續閱讀吧。
N+1 問題

預設情況下,ActiveRecord 關係十分懶散。這意味著架構會一直等待訪問關係直到您實際訪問了該關係。比方說,每個成員都會有一個地址。可以開啟一個控制台並輸入如下命令:member = Member.find 1。可以看到追加到日誌的如下內容,如清單 3 所示:
清單 3. 從 Member.find(1) 登入

^[[4;35;1mMember Columns (0.006198)^[[0m  ^[[0mSHOW FIELDS FROM members^[[0m^[[4;36;1mMember Load (0.002835)^[[0m  ^[[0;1mSELECT * FROM members WHERE (members.`id` = 1) ^[[0m

Member 具有到此地址的關係,並由宏 has_one :address, :as => :addressable, :dependent => :destroy 定義。注意當 ActiveRecord 載入了 Member 時,您並不會看到地址欄位。但如果在控制台中鍵入 member.address,就可以在 development.log 中看到清單 4 中的內容:
清單 4. 訪問關係會強制資料庫訪問

 ^[[36;2m./vendor/plugins/paginating_find/lib/paginating_find.rb:98:in `find'^[[0m^[[4;35;1mAddress Load (0.252084)^[[0m  ^[[0mSELECT * FROM addresses WHERE (addresses.addressable_id = 1 AND addresses.addressable_type = 'Member') LIMIT 1^[[0m ^[[35;2m./vendor/plugins/paginating_find/lib/paginating_find.rb:98:in `find'^[[0m

所以 ActiveRecord 並不會為地址關係執行查詢,直到您實際訪問 member.address。通常,這種懶散設計會工作得很好,因為持久性架構無需移動如此多的資料來載入成員。但如果您想要訪問很多成員以及所有成員的地址,如清單 5 所示:
清單 5. 用地址檢索多個成員

Member.find([1,2,3]).each {|member| puts member.address.city}

由於您應該看到針對每個地址的查詢,所以就效能而言,結果並不盡如人意。清單 6 給出了問題的全部:
清單 6. N+1 問題的查詢

^[[4;36;1mMember Load (0.004063)^[[0m  ^[[0;1mSELECT * FROM members WHERE (members.`id` IN (1,2,3)) ^[[0m ^[[36;2m./vendor/plugins/paginating_find/lib/paginating_find.rb:98:in `find'^[[0m^[[4;35;1mAddress Load (0.000989)^[[0m  ^[[0mSELECT * FROM addresses WHERE (addresses.addressable_id = 1 AND addresses.addressable_type = 'Member') LIMIT 1^[[0m ^[[35;2m./vendor/plugins/paginating_find/lib/paginating_find.rb:98:in `find'^[[0m^[[4;36;1mAddress Columns (0.073840)^[[0m  ^[[0;1mSHOW FIELDS FROM addresses^[[0m^[[4;35;1mAddress Load (0.002012)^[[0m  ^[[0mSELECT * FROM addresses WHERE (addresses.addressable_id = 2 AND addresses.addressable_type = 'Member') LIMIT 1^[[0m ^[[35;2m./vendor/plugins/paginating_find/lib/paginating_find.rb:98:in `find'^[[0m^[[4;36;1mAddress Load (0.000792)^[[0m  ^[[0;1mSELECT * FROM addresses WHERE (addresses.addressable_id = 3 AND addresses.addressable_type = 'Member') LIMIT 1^[[0m ^[[36;2m./vendor/plugins/paginating_find/lib/paginating_find.rb:98:in `find'^[[0m

結果正如我所預見的那樣糟糕。所有成員共用一個查詢,而每個地址各用一個查詢。我們檢索了三個成員,所以一共用了四個查詢。如果是 N 個成員,就會有 N+1 個查詢。這就是可怕的 N+1 問題。大多數持久性架構都採用熱關聯(eager association)來解決該問題。Rails 也不例外。如果需要訪問關係,就可以選擇將其包括到初始查詢中。ActiveRecord 使用 :include 選項來實現此目的。如果將查詢更改為 Member.find([1,2,3], :include => :address).each {|member| puts member.address.city},結果就會稍好一些:
清單 7. 解決 N+1 問題

^[[4;35;1mMember Load Including Associations (0.004458)^[[0m  ^[  [0mSELECT members.`id` AS t0_r0, members.`type` AS t0_r1,  members.`about_me` AS t0_r2, members.`about_philanthropy`  ...  addresses.`id` AS t1_r0, addresses.`address1` AS t1_r1,  addresses.`address2` AS t1_r2, addresses.`city` AS t1_r3,  ...  addresses.`addressable_id` AS t1_r8 FROM members  LEFT OUTER JOIN addresses ON addresses.addressable_id  = members.id AND addresses.addressable_type =  'Member' WHERE (members.`id` IN (1,2,3)) ^[  [0m ^[[35;2m./vendor/plugins/paginating_find/lib/paginating_find.rb: 98:in `find'^[[0m

該查詢的速度也會更快。一個查詢會檢索所有成員和地址。這就是熱關聯的工作原理。

通過 ActiveRecord,還可以嵌套 :include 選項,但嵌套深度只有一級。例如,有多個 contacts 的 Member 以及有一個 address 的 Contact 就屬於這種情況。如果想要為某個成員的連絡人顯示所有城市,就可以使用清單 8 中所示的代碼:
清單 8: 為某個成員的連絡人擷取城市

member = Member.find(1)member.contacts.each {|contact| puts contact.address.city}

該代碼應該能夠工作,但必須要針對此成員、每個連絡人以及每個連絡人的地址進行查詢。通過用 :include => :contacts 包括 :contacts,可以稍許提高效能。也可以通過將二者都包括進來進一步地改進,如清單 9 所示:
清單 9: 為某個成員的連絡人擷取城市

member = Member.find(1)member.contacts.each {|contact| puts contact.address.city}

通過使用嵌套包含選項還能獲得更好的改進:

member = Member.find(1, :include => {:contacts => :address})member.contacts.each {|contact| puts contact.address.city}

該嵌套包含可讓 Rails 熱包含 contacts 和 address 關係。一旦要在給定的查詢中使用關係,就可以採用熱載入技術。此技術是我們在 ChangingThePresent.org 中使用得最為頻繁的一種效能最佳化技術,但它還是有一些限制的。當必須要串連兩個以上的表時,最好還是採用 SQL。如果需要進行報告,最好是簡單地採取資料庫連接,跨過 ActiveRecord 以及 ActiveRecord::Base.execute("SELECT * FROM...")。通常來講,熱關聯足夠解決問題。現在,我將轉變話題,探討 Rails 開發人員所關心的另一個麻煩問題:繼承。
繼承和 Rails

當大多數 Rails 開發人員第一次接觸到 Rails 時,他們就會立刻被迷住。它太簡單了。您只需在資料庫表上建立一個 type 類,然後再從父類中繼承子類即可。Rails 會負責其餘的事情。比如,有一個名為 Customer 表,它可以從名為 Person 類繼承。一個客戶可以有 Person 的所有列,外加信譽度和訂購曆史。清單 10 顯示了該種解決方案的簡潔之美。主表具有父類和子類的所有列。
清單 10. 實現繼承

create_table "people" do |t| t.column "type", :string t.column "first_name", :string t.column "last_name", :string t.column "loyalty_number", :stringendclass Person < ActiveRecord::Baseendclass Customer < Person has_many :ordersend

在很多方面,這種解決方案都可以很好地工作。代碼簡單且無重複性。這些查詢簡單且效能很好,因為您無需進行任何串連來訪問多個子類,ActiveRecord 可以使用 type 列決定哪個記錄能夠返回。

在某些方面,ActiveRecord 繼承十分有限。如果已有的繼承等級非常寬,繼承就會失效。例如,在 ChangingThePresent,內容有很多類型,每種類型都有自己的名稱、或短或長的描述、某些常見的表示屬性以及幾個定製屬性。我們很希望 cause、nonprofit、gift、member、drive、registry 以及其他一些類型的對象都能夠從通用的基類中繼承,以便我們能以同樣的方式處理所有類型的內容。但我們卻不能如此,因為 Rails 模型將會在單一表中擁有我們所有物件模型的實質內容,這不是一個可行的解決方案。
探索其他可選方案

我們針對此問題實驗了三種解決方案。第一,我們在類自身的表中放置每個類,使用視圖為內容構建通用表。我們很快拋棄了此種解決方案,因為 Rails 不能很好地處理資料庫檢視。

我們的第二個解決方案是使用簡單的多態。通過這種策略,每個子類都會擁有其自身的表。我們將通用列推入每個表。例如,比方說我需要一個名為 Content 的子類,它只包含 name 屬性,以及 Gift、Cause 和 Nonprofit 子類。Gift、Nonprofit 和 Cause 都可有 name 屬性。由於 Ruby 是動態類型的,所以這些子類無需從通用基類中繼承。它們只需對相同的一組方法進行響應。ChangingThePresent 在幾個地方使用了多態以提供通用的行為,尤其是在處理映像的時候。

第三種方法是提供一種通用的功能,但採用的是關聯而非繼承。ActiveRecord 具有一種稱為多態關聯的特性,非常適合將通用行為附加給類,完全無需繼承。在之前的 Address,您已經看到了多態關聯的樣本。我可以使用相同的技術(而非繼承)附加通用屬性用於內容管理。考慮名為 ContentBase 的類。通常,為了將該類關聯到另一個類,可以使用 has_one 關係和一個簡單的外鍵。但您可能更想讓 ContentBase 能與多個類共同工作。這時,您需要一個外鍵,還需要一個能定義目標類的類型的列。而這恰好是 ActiveRecord 多態關聯所擅長的方面。請參看清單 11。
清單 11. 網站內容關係的兩個方面

class Cause < ActiveRecord::Base has_one :content_base, :as => :displayable, :dependent => :destroy ...endclass Nonprofit < ActiveRecord::Base has_one :content_base, :as => :displayable, :dependent => :destroy ...endclass ContentBase < ActiveRecord::Base belongs_to :displayable, :polymorphic => trueend

通常,belongs_to 關係只有一個類,但 ContentBase 中的關係卻是多態的。外鍵不僅具有標識記錄的標識符,而且還具有標識表的一個類型。使用這種技術,我獲得了繼承的諸多益處。常見的功能在單一類中就都包括了。但這也帶來了幾個副作用。我無需將 Cause 和 Nonprofit 中的所有列都放在單一表中。

一些資料庫管理員不太看好多態關聯,原因是他們不怎麼使用真正意義上的外鍵,但對於 ChangingThePresent,我們自由地使用了多態關聯。實際上,資料模型並不像理論上那樣美好。不能使用諸如參考完整性這樣的資料庫特性,也不能依賴於工具來基於列的名稱發現這些關係。簡潔的物件模型的好處對我們來說要比此方式所存在的問題更為重要。

create_table "content_bases", :force => true do |t| t.column "short_description",     :string ... t.column "displayable_type", :string t.column "displayable_id",  :integerend

結束語

ActiveRecord 是一種功能完善的持久性架構。用它可以構建可伸縮的可靠系統,但與其他資料庫架構一樣,您必須要格外注意架構所產生的 SQL。當偶爾遇到問題時,您必須調整自己的方式和策略。保留索引、藉助 include 使用熱載入和在某些地方使用多態關聯代替繼承是三種可用來改進程式碼程式庫的方法。在下月,我將帶您親曆另一個樣本去領略如何編寫真實世界中的 Rails。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.