濮阳杆衣贸易有限公司

主頁 > 知識(shí)庫 > 在Ruby on Rails中優(yōu)化ActiveRecord的方法

在Ruby on Rails中優(yōu)化ActiveRecord的方法

熱門標(biāo)簽:白銀外呼paas系統(tǒng) 滴滴外呼系統(tǒng) 湖州u友防封電銷卡 地圖標(biāo)注賺錢項(xiàng)目注冊 百度地圖標(biāo)注自定義圖片 電銷機(jī)器人廠商代理 徐州網(wǎng)絡(luò)外呼系統(tǒng)哪個(gè)好 常德電銷平臺(tái)外呼系統(tǒng)軟件價(jià)格 高德地圖標(biāo)注客服

 Ruby on Rails 編程常常會(huì)將您寵壞。這一不斷發(fā)展的框架會(huì)讓您從其他框架的沉悶乏味中解脫出來。您可以用習(xí)以為常的幾行代碼片斷表達(dá)自己的意圖。而且還可以使用 ActiveRecord。

對于我這樣的一個(gè)老 Java? 程序員而言,ActiveRecord 多少有點(diǎn)生疏。通過 Java 框架,我通常都會(huì)在獨(dú)立的模型和模式之間構(gòu)建一種映射。像這樣的框架就是映射框架。通過 ActiveRecord,我只定義數(shù)據(jù)庫模式:或者用 SQL 或者用稱為遷移(migration)的 Ruby 類。將對象模型設(shè)計(jì)建立于數(shù)據(jù)庫結(jié)構(gòu)之上的那些框架稱為包裝框架。與大多數(shù)包裝框架不同,Rails 能通過查詢數(shù)據(jù)庫表發(fā)現(xiàn)對象模型的特征。與構(gòu)建復(fù)雜查詢不同,我使用模型在 Ruby(而非 SQL)中遍歷關(guān)系。這樣一來,我既獲得了包裝框架的簡單性,又具備了映射框架的大部分功能。ActiveRecord 易于使用和擴(kuò)展。有時(shí),甚至有些過于簡單。

與任何數(shù)據(jù)庫框架一樣,ActiveRecord 讓我極易做出很多惹麻煩的事。我所能獲取的列太多,又很容易遺漏重要的結(jié)構(gòu)化數(shù)據(jù)庫特性,比如索引或空約束。我并不是說 ActiveRecord 是個(gè)不好的框架。只不過若是需要擴(kuò)展,您需要知道如何堅(jiān)固自己的應(yīng)用程序。在本篇文章中,我將帶您親歷在使用 Rails 這一獨(dú)樹一幟的持久性框架時(shí)可能需要的一些重要優(yōu)化。
基礎(chǔ)管理

生成受模式支持的模型異常容易,只需很少的代碼,即 script/generate model model_name。正如您所知,該命令可生成模型、遷移、單元測試甚至一個(gè)默認(rèn)的 fixture。在該遷移中填上一些數(shù)據(jù)列,并輸入一些測試數(shù)據(jù)、編寫幾個(gè)測試、添加幾個(gè)驗(yàn)證就算大功告成,這樣做真是很有誘惑力。但請您三思而行。您應(yīng)該考慮總體的數(shù)據(jù)庫設(shè)計(jì),要特別注意以下這些事情:

  •     Rails 不會(huì)讓您擺脫基本的數(shù)據(jù)庫性能問題。數(shù)據(jù)庫需要信息,這些信息經(jīng)常以索引的格式才能有不錯(cuò)的性能。
  •     Rails 不會(huì)讓您擺脫數(shù)據(jù)完整性問題。雖然大多數(shù) Rails 開發(fā)人員都不喜歡在數(shù)據(jù)庫中保留限制,但您應(yīng)該考慮像空列這樣的事情。
  •     Rails 為很多元素提供了方便的默認(rèn)屬性。有時(shí),像文本字段的長度這樣的默認(rèn)屬性對于大多數(shù)實(shí)用的應(yīng)用程序而言都會(huì)過大。
  •     Rails 不會(huì)強(qiáng)制您創(chuàng)建有效的數(shù)據(jù)庫設(shè)計(jì)。

在您繼續(xù)跋涉,深入學(xué)習(xí) ActiveRecord 之前,應(yīng)該首先確保您已經(jīng)打好了足夠的基礎(chǔ)。請確保索引結(jié)構(gòu)可以為您所用。如果給定的表很大,如果將在列上而不是 id 上搜索,如果索引能對您有所幫助(更多細(xì)節(jié),請參見數(shù)據(jù)庫管理器文檔 —— 不同的數(shù)據(jù)庫以不同方式使用索引),那么就需要?jiǎng)?chuàng)建索引。無需采用 SQL 創(chuàng)建索引 —— 可以簡單地使用遷移創(chuàng)建??梢暂p松地使用 create_table 遷移創(chuàng)建索引,也可以創(chuàng)建一個(gè)額外的遷移來創(chuàng)建索引。以下是一個(gè)遷移示例,可用來為 ChangingThePresent.org (請參見 參考資料)創(chuàng)建索引:
清單 1. 在遷移中創(chuàng)建索引

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
 end
end

ActiveRecord 會(huì)負(fù)責(zé) id 上的索引,我顯式地添加了可在各種搜索中使用的索引,原因是此表很大、不經(jīng)常更新卻經(jīng)常被搜索。通常,我們會(huì)等到對給定的查詢中的問題有一定的把握后才會(huì)采取相應(yīng)動(dòng)作。這種策略可以讓我們不必二次猜測數(shù)據(jù)庫引擎。但從用戶這方面來看,我們知道該表將會(huì)很快具有數(shù)百萬的用戶,如果在經(jīng)常搜索的列上沒有索引,該表的效率會(huì)很低。

另外兩個(gè)常見問題也與遷移有關(guān)。如果字符串和列都不應(yīng)該為空,那么就請確保正確編寫了遷移。大多數(shù) DBA(數(shù)據(jù)庫管理員)都會(huì)認(rèn)為 Rails 為空列提供了錯(cuò)誤的默認(rèn)屬性:默認(rèn)情況下列可以為空。如果希望創(chuàng)建一個(gè)不能為空的列,您必須顯式地添加參數(shù) :null => false。如果具有字符串列,請務(wù)必確保編寫應(yīng)用程序的限值。默認(rèn)地,Rails 遷移會(huì)將 string 列按 varchar(255) 編碼。通常,這個(gè)值過于龐大。應(yīng)該盡量保持能如實(shí)反應(yīng)應(yīng)用程序的數(shù)據(jù)庫結(jié)構(gòu)。與提供無任何限制的 login 相反,如果應(yīng)用程序限制 login 只能為 10 個(gè)字符,那么就應(yīng)該相應(yīng)地編寫數(shù)據(jù)庫,如清單 2 所示:
清單 2. 用限值和非空列編寫遷移

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

此外,還應(yīng)該考慮默認(rèn)值以及其他任何能安全提供的信息。通過一點(diǎn)預(yù)備工作,就可以節(jié)省日后跟蹤數(shù)據(jù)完整性問題的大量時(shí)間。在考慮數(shù)據(jù)庫基礎(chǔ)的同時(shí),還應(yīng)該注意哪些頁是靜態(tài)且容易緩存的。在優(yōu)化查詢和緩存頁面這兩個(gè)選項(xiàng)當(dāng)中,如果您能 “消受” 復(fù)雜性,緩存頁面將會(huì)帶來更大的回報(bào)。有時(shí),頁面或片段都是純靜態(tài)的,比如一列狀態(tài)或一組經(jīng)常問到的問題。在這種情況下,緩存更勝一籌。而在其他的一些時(shí)候,您可能會(huì)決定犧牲數(shù)據(jù)庫性能,以減少復(fù)雜性。對于 ChangingThePresent,根據(jù)問題和環(huán)境的具體情況,我們二者都嘗試了。如果您也決定要犧牲查詢性能,就請繼續(xù)閱讀吧。
N+1 問題

默認(rèn)情況下,ActiveRecord 關(guān)系十分懶散。這意味著框架會(huì)一直等待訪問關(guān)系直到您實(shí)際訪問了該關(guān)系。比方說,每個(gè)成員都會(huì)有一個(gè)地址??梢源蜷_一個(gè)控制臺(tái)并輸入如下命令:member = Member.find 1??梢钥吹阶芳拥饺罩镜娜缦聝?nèi)容,如清單 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 具有到此地址的關(guān)系,并由宏 has_one :address, :as => :addressable, :dependent => :destroy 定義。注意當(dāng) ActiveRecord 加載了 Member 時(shí),您并不會(huì)看到地址字段。但如果在控制臺(tái)中鍵入 member.address,就可以在 development.log 中看到清單 4 中的內(nèi)容:
清單 4. 訪問關(guān)系會(huì)強(qiáng)制數(shù)據(jù)庫訪問

 ^[[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 并不會(huì)為地址關(guān)系執(zhí)行查詢,直到您實(shí)際訪問 member.address。通常,這種懶散設(shè)計(jì)會(huì)工作得很好,因?yàn)槌志眯钥蚣軣o需移動(dòng)如此多的數(shù)據(jù)來加載成員。但如果您想要訪問很多成員以及所有成員的地址,如清單 5 所示:
清單 5. 用地址檢索多個(gè)成員

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

由于您應(yīng)該看到針對每個(gè)地址的查詢,所以就性能而言,結(jié)果并不盡如人意。清單 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

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

該查詢的速度也會(huì)更快。一個(gè)查詢會(huì)檢索所有成員和地址。這就是熱關(guān)聯(lián)的工作原理。

通過 ActiveRecord,還可以嵌套 :include 選項(xiàng),但嵌套深度只有一級。例如,有多個(gè) contacts 的 Member 以及有一個(gè) address 的 Contact 就屬于這種情況。如果想要為某個(gè)成員的聯(lián)系人顯示所有城市,就可以使用清單 8 中所示的代碼:
清單 8: 為某個(gè)成員的聯(lián)系人獲取城市

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

該代碼應(yīng)該能夠工作,但必須要針對此成員、每個(gè)聯(lián)系人以及每個(gè)聯(lián)系人的地址進(jìn)行查詢。通過用 :include => :contacts 包括 :contacts,可以稍許提高性能。也可以通過將二者都包括進(jìn)來進(jìn)一步地改進(jìn),如清單 9 所示:
清單 9: 為某個(gè)成員的聯(lián)系人獲取城市

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

通過使用嵌套包含選項(xiàng)還能獲得更好的改進(jìn):

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

該嵌套包含可讓 Rails 熱包含 contacts 和 address 關(guān)系。一旦要在給定的查詢中使用關(guān)系,就可以采用熱加載技術(shù)。此技術(shù)是我們在 ChangingThePresent.org 中使用得最為頻繁的一種性能優(yōu)化技術(shù),但它還是有一些限制的。當(dāng)必須要連接兩個(gè)以上的表時(shí),最好還是采用 SQL。如果需要進(jìn)行報(bào)告,最好是簡單地采取數(shù)據(jù)庫連接,跨過 ActiveRecord 以及 ActiveRecord::Base.execute("SELECT * FROM...")。通常來講,熱關(guān)聯(lián)足夠解決問題?,F(xiàn)在,我將轉(zhuǎn)變話題,探討 Rails 開發(fā)人員所關(guān)心的另一個(gè)麻煩問題:繼承。
繼承和 Rails

當(dāng)大多數(shù) Rails 開發(fā)人員第一次接觸到 Rails 時(shí),他們就會(huì)立刻被迷住。它太簡單了。您只需在數(shù)據(jù)庫表上創(chuàng)建一個(gè) type 類,然后再從父類中繼承子類即可。Rails 會(huì)負(fù)責(zé)其余的事情。比如,有一個(gè)名為 Customer 表,它可以從名為 Person 類繼承。一個(gè)客戶可以有 Person 的所有列,外加信譽(yù)度和訂購歷史。清單 10 顯示了該種解決方案的簡潔之美。主表具有父類和子類的所有列。
清單 10. 實(shí)現(xiàn)繼承

create_table "people" do |t|
 t.column "type", :string
 t.column "first_name", :string
 t.column "last_name", :string
 t.column "loyalty_number", :string
end

class Person  ActiveRecord::Base
end

class Customer  Person
 has_many :orders
end

在很多方面,這種解決方案都可以很好地工作。代碼簡單且無重復(fù)性。這些查詢簡單且性能很好,因?yàn)槟鸁o需進(jìn)行任何連接來訪問多個(gè)子類,ActiveRecord 可以使用 type 列決定哪個(gè)記錄能夠返回。

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

我們針對此問題試驗(yàn)了三種解決方案。第一,我們在類自身的表中放置每個(gè)類,使用視圖為內(nèi)容構(gòu)建通用表。我們很快拋棄了此種解決方案,因?yàn)?Rails 不能很好地處理數(shù)據(jù)庫視圖。

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

第三種方法是提供一種通用的功能,但采用的是關(guān)聯(lián)而非繼承。ActiveRecord 具有一種稱為多態(tài)關(guān)聯(lián)的特性,非常適合將通用行為附加給類,完全無需繼承。在之前的 Address,您已經(jīng)看到了多態(tài)關(guān)聯(lián)的示例。我可以使用相同的技術(shù)(而非繼承)附加通用屬性用于內(nèi)容管理。考慮名為 ContentBase 的類。通常,為了將該類關(guān)聯(lián)到另一個(gè)類,可以使用 has_one 關(guān)系和一個(gè)簡單的外鍵。但您可能更想讓 ContentBase 能與多個(gè)類共同工作。這時(shí),您需要一個(gè)外鍵,還需要一個(gè)能定義目標(biāo)類的類型的列。而這恰好是 ActiveRecord 多態(tài)關(guān)聯(lián)所擅長的方面。請參看清單 11。
清單 11. 站點(diǎn)內(nèi)容關(guān)系的兩個(gè)方面

class Cause  ActiveRecord::Base
 has_one :content_base, :as => :displayable, :dependent => :destroy
 ...
end

class Nonprofit  ActiveRecord::Base
 has_one :content_base, :as => :displayable, :dependent => :destroy
 ...
end


class ContentBase  ActiveRecord::Base
 belongs_to :displayable, :polymorphic => true
end

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

一些數(shù)據(jù)庫管理員不太看好多態(tài)關(guān)聯(lián),原因是他們不怎么使用真正意義上的外鍵,但對于 ChangingThePresent,我們自由地使用了多態(tài)關(guān)聯(lián)。實(shí)際上,數(shù)據(jù)模型并不像理論上那樣美好。不能使用諸如引用完整性這樣的數(shù)據(jù)庫特性,也不能依賴于工具來基于列的名稱發(fā)現(xiàn)這些關(guān)系。簡潔的對象模型的好處對我們來說要比此方式所存在的問題更為重要。

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

 ...

 t.column "displayable_type", :string
 t.column "displayable_id",  :integer
end

結(jié)束語

ActiveRecord 是一種功能完善的持久性框架。用它可以構(gòu)建可伸縮的可靠系統(tǒng),但與其他數(shù)據(jù)庫框架一樣,您必須要格外注意框架所生成的 SQL。當(dāng)偶爾遇到問題時(shí),您必須調(diào)整自己的方式和策略。保留索引、借助 include 使用熱加載和在某些地方使用多態(tài)關(guān)聯(lián)代替繼承是三種可用來改進(jìn)代碼庫的方法。在下月,我將帶您親歷另一個(gè)示例去領(lǐng)略如何編寫真實(shí)世界中的 Rails。

您可能感興趣的文章:
  • 對優(yōu)化Ruby on Rails性能的一些辦法的探究

標(biāo)簽:三沙 張家界 普洱 公主嶺 梧州 永州 荊門 遼寧

巨人網(wǎng)絡(luò)通訊聲明:本文標(biāo)題《在Ruby on Rails中優(yōu)化ActiveRecord的方法》,本文關(guān)鍵詞  在,Ruby,Rails,中,優(yōu)化,ActiveRecord,;如發(fā)現(xiàn)本文內(nèi)容存在版權(quán)問題,煩請?zhí)峁┫嚓P(guān)信息告之我們,我們將及時(shí)溝通與處理。本站內(nèi)容系統(tǒng)采集于網(wǎng)絡(luò),涉及言論、版權(quán)與本站無關(guān)。
  • 相關(guān)文章
  • 下面列出與本文章《在Ruby on Rails中優(yōu)化ActiveRecord的方法》相關(guān)的同類信息!
  • 本頁收集關(guān)于在Ruby on Rails中優(yōu)化ActiveRecord的方法的相關(guān)信息資訊供網(wǎng)民參考!
  • 推薦文章
    大同县| 福鼎市| 连城县| 武宣县| 遵义县| 红原县| 长乐市| 昌乐县| 巴马| 怀柔区| 上蔡县| 黔西| 浦城县| 菏泽市| 安陆市| 洮南市| 绵阳市| 宜川县| 海原县| 汉寿县| 汉源县| 思茅市| 乃东县| 卢龙县| 凤台县| 文登市| 诸暨市| 永泰县| 文水县| 肥乡县| 青河县| 鹤庆县| 灌云县| 青海省| 沙坪坝区| 抚宁县| 筠连县| 桦南县| 洛阳市| 合山市| 尼木县|