`
fireflyman
  • 浏览: 112252 次
  • 性别: Icon_minigender_1
  • 来自: 火星
社区版块
存档分类
最新评论

Rails之道 ---><The Rails Way> 摘录(6)運用ActiveRecord

    博客分类:
  • ROR
阅读更多
1.當你在開發模式下運行Rails服務器時,通過Web瀏覽器對數據表結構的改動會立即在ActiveRecord對象中反映出來.然而,如果你運行著Rails的控制臺,那么對數據表結構的改動并不會自動反映到ActiveRecord對象.當然,你可以通過手動的方式將這些改動反映出來,在控制臺輸入Dispatcher.reset_application!即可.

2.遷移是Rails提供的一種幫助你更新應用程序的數據表結構(也就是熟悉的數據描述語言========DDL)的方式,通過它,在每次改變應用程序代碼的時候,你不需要刪除或者重新創建數據表,這就意味著你不會丟失開發數據.在執行遷移的時候,唯一改變的事將數據表結構從一個版本改為另一個版本,不管這個改動是版本更新還是回退.

3.Rails針對創建遷移提供了一個生成器.我們可以通過命令行查看幫助文本,如下所示:
$ script/generate migration
Usage: script/generate migration MigrationName [options]
Rails Info:
-v, --version Show the Rails version and quit.
-h, --help Show this help message and quit.
General Options:
-p, --pretend Run but do not make any changes.
-f, --force Overwrite files that already
exist.
-s, --skip Skip files that already exist.
-q, --quiet Suppress normal output.
-t, --backtrace Debugging: show backtrace on
errors.
-c, --svn Modify files with subversion.
(Note: svn must be in path)

Description:
The migration generator creates a stub for a new database
migration.
The generator takes a migration name as its argument. The
migration
name may be given in CamelCase or under_score.
The generator creates a migration class in db/migrate prefixed by
its number in the queue.
Example:
./script/generate migration AddSslFlag
With 4 existing migrations, this will create an AddSslFlag
migration
in the file db/migrate/005_add_ssl_flag.rb

從上可知,你只需要為遷移提供一個駝峰式的描述性名字,生成器就能完成余下的工作.在需要改變現有表的屬性時,我們只要直接調用生成器即可.(備注:諸如數據庫模型生成器等其他的生成器同樣為你創建遷移腳本,除非你特別指定了----skip-migration選項)

4.所有列選項都接受下面三個選項,
引用
(1):default => value
設置默認值.在創建數據行的時候,該默認值將作為該列的初始值.如果你希望值為null,那么你并不需要進行顯式的設置,只要不設置該項即可.

(2)limit => size
為字符串、文本、二進制或者整數列增加一個范圍大小的參數.根據列類型不同,這個參數的含義也會相應不同.一般來說,對于字符串類型的限制是指字符的個數,而對于其他類型,大小規則是指在數據庫中存儲相應值所需的字節數

(3):null => true
通過增加not null的約束到數據庫的級別上,將某個列設為必須的.

5.小數精度
聲明為:decimal的列接受以下兩個選項:
(1):precision = > number
精度(precision)是指數字的總位數
(2):scale => number
而數值范圍(scale)則是小數點后面數字的總位數.
eg: 123.45的精度為5,而數值范圍是2(自從加入火星后,現在對這個數字很敏感.....)


6.別用浮點數來存儲貨幣值,或者說,別用浮點數來存儲需要固定精度的所有類型的數據.

7.ActiveRecord有一個attributes方法,它返回一個散列,包括所有屬性和相應的值(通過read_attribute方法返回的值).如果使用自定義的屬性讀寫器,那么attributes方法被調用的時候,它并不會使用自定義的屬性讀取器來訪問屬性值,但是attributes= (在其中進行大量賦值操作)確實會調用自定義的屬性寫入器.(備注,通過attributes方法返回的散列并不是ActiveRecord對象內部結構的引用,而是一個副本,這意味著改變它的值并不會對原對象造成影響.)

8.對于許多程序而言,最常見的操作就是基于一個或兩個列進行簡單的查詢.因此Rails提供了一種簡單而有效的方式來進行這些查詢,并且不需要通過find方法的條件參數.這種方式能夠實現,有賴于Ruby那充滿魔力的method_missing回調,每當你調用了還沒有定義的方法的時候,這個回調就會被調用.

9.find_or_create_by_:如果對象存在,那么查找器就會返回它,否則會創建一個并且返回創建的對象.(備注:如果你不想一開始就保存創建的對象,可以使用find_or_initialize_by_查找器)

10.find_by_sql這個類方法可以直接使用SQL的Select查詢,并且給予查詢結果,返回ActiveRecord對象的數組.
eg:
 >>Client.find_by_sql("select * from clients")

備注:你應該慎用此方法,除非是在不得不用的時候.
(1)這個方法減少了數據庫的可移植性,如果使用了ActiveRecord正規的find方法,那么Rails會在底層為你處理好不同數據庫之間的差異.
(2)ActiveRecord已經擁有了一堆內建的函數,用于抽象select語句,因此,重新發明新的方式來實現同樣的功能是不明智的.
>>Client.find_by_sql("select * from clients where code like 'A%'")
==>
>> param = "A"
 >>Clinent.find(:all, :coditions => ["code like?", "#{param}%"])


11.update_all,它與使用update..where這樣的SQL語句進行更新是緊密對應的.update_all方法可以接受兩個參數,一個對應于SQL語句的SET部分,而另一個則對應于WHERE子句,也就是條件.這個方法返回了更新記錄的數目.
eg:
Order.update_all("name = 'Yang'", "pay_type= 'cc'")


12.在ActiveRecord中的屬性,在你需要防止一時疏忽以及大量賦值操作的時候,你可以使用下面兩個類方法來控制對屬性的訪問.
(1)attr_accessible方法以屬性列表為參數,這個列表指明了能夠用于大量賦值的可訪問屬性.(這是用于大量賦值的一種更保守的做法)

(2)如果在默認的情況下,大部分屬性都是開放的,而只有在需要的時候才會被限制某些屬性訪問時,你應該使用attr_protected方法.傳遞給這個方法的屬性將會受到保護,不能用于大量賦值.在大量賦值過程中,這些屬性的賦值會簡單地被忽略,你只能通過直接復制到方法來為這些屬性賦值,如下eg所示:

class Customer < ActiveRecord::Base
   attr_protected :credit_rating
end

customer = Customer.new(:name => “Abe”, :credit_rating => “Excellent”)
customer.credit_rating # => nil

customer.attributes = { “credit_rating” => “Excellent” }
customer.credit_rating # => nil

# and now, the allowed way to set a credit_rating
customer.credit_rating = “Average”
customer.credit_rating # => “Average”


13.刪除和銷毀
    如果你需要從數據庫中移除某個記錄,那么你有兩種選擇:
(1)destroy方法不僅將記錄從數據庫中移除,還會凍結它(將其設為只讀),這樣你就不能再次保存它.
>> bad_timesheet = Timesheet.find(1)
>> bad_timesheet.destroy
=> #<Timesheet:0x2481d70 @attributes={“updated_at”=>”2006-11-21
05:40:27”, “id”=>”1”, “user_id”=>”1”, “submitted”=>nil, “created_at”=>
“2006-11-21 05:40:27”}>

>> bad_timesheet.save
TypeError: can’t modify frozen hash
from activerecord/lib/active_record/base.rb:1965:in `[]=’

(2)另一種可選的方法是可以將destroy和delete方法作為類方法進行調用,只需要傳遞所刪除的id即可.兩個方法都能接受單個id或者id數組作為參數.
Timesheet.delete(1)
Timesheet.destroy([2, 3])

方法的命名看起來有些前后矛盾,事實上并非如此.delete方法直接使用SQL語句,并且不會裝載任何實例(因此會更快).destroy方法會裝載ActiveRecord對象實例,并且通過實例來調用destroy方法.兩者的語義差別是非常細微的,不過,如果你在before_destroy回調中進行了賦值或者擁有依賴關聯的時候,差別就會凸現了.在有依賴關聯的情況下,調用destroy方法會使得子對象和父對象一起被刪除.

14.數據庫鎖定
"鎖定"是一個專業術語,它是指用于防止應用程序的并發用戶復寫各操作結果的技術.從數據庫裝載數據模型數據的時候,ActiveRecord通常都不會使用任何類型的鎖,如果某個特定的Rails應用程序在同一時間只有一個用戶在更新數據,那么你并不需要擔心鎖的問題.
   當多于一個的用戶同時訪問并且更新相同數據的時候,你作為開發人員務必要考慮并發性的問題,這是非常重要的.自我提問一下,如果兩個用戶同時嘗試更新某個特定的數據模型,會發生什么類型的沖突(collision)或者競爭條件(race collision)呢?
    在數據庫支撐的應用程序中,有很多方法可以處理并發性問題,而ActiveRecord支持中的兩種方式:樂觀鎖定(optimistic locking)和悲觀鎖定(pessimistic locking).

(1)樂觀鎖定
        是指在沖突發生的時候用于檢測并且解決沖突的策略.在多用戶并且沖突發生較少的情況下,樂觀鎖定是受推崇的.在使用樂觀鎖定的時候,數據記錄并不會真正被鎖定,因此"樂觀鎖定"這個術語多少有點用詞不當.
         樂觀鎖定是個非常常用的策略,因為對于絕大多數應用程序的設計而言,某個特定用戶主要還是更新屬于它自己的數據,而不是別人的,所有兩個用戶搶著更新同一個數據的可能性極少.樂觀鎖定潛在的意圖是既然沖突很少發生,那么只有它們真的發生的時候,我們才去處理它.
       如果你控制數據庫表結構,那么樂觀鎖定是很容易實現的.對于某個特定的表,你只需要增加一個lock_version、默認值為零點整數列即可

class AddLockVersionToTimesheets < ActiveRecord::Migration
  def self.up
   add_column :timesheets, :lock_version, :integer, :default => 0
  end
  def self.down
   remove_column :timesheets, :lock_version
  end
end

增加了lock_version之后,ActiveRecord的功能就會改變.如果兩個不同的數據模型實例被裝載并且被不同用戶保存,那么第一個實例將會成功保存,而第二個實例將會導致ActiveRecord::StaleObjectError異常拋出.
 
我們可以通過一個簡單的單元測試來說明樂觀鎖定的功能:
class TimesheetTest < Test::Unit::TestCase

  fixtures :timesheets, :users
  def test_optimistic_locking_behavior
   first_instance = Timesheet.find(1)
   second_instance = Timesheet.find(1)

   first_instance.approver = users(:approver)
   second_instance.approver = users(:approver2)

   assert first_instance.save, "First instance save succeeded"

   assert_raises ActiveRecord::StaleObjectError do
    second_instance.save
   end
  end
end


測試通過了,因為調用第二個實例的save方法會引發ActiveRecord::StaleObjectError異常,這是我們期待的效果.

(備注:
a.要使用其它數據列代替lock_version,那么你可以使用set_locking_column來改變這個設置.要讓這個改動應用于整個應用程序,則需要在enviroment,rb中增加以下代碼:
ActiveRecord::Base.set_locking_column 'alternate_lock_version'
與其它ActiveRecord設置類似,你可以在數據模型類中增加一個生命,從而改變單個數據模型的設置.

class Timesheet < ActiveRecord::Base
   set_locking_column ‘alternate_lock_version’
end


b.處理StaleObjectError
        在設置了樂觀鎖定之后,我們需要得體的處理StaleObjectError.根據更新數據不同的關鍵性,你可能需要花費很多時間去構建對用戶友好的解決方案,以便保存更新失敗的用戶所提交的數據.即使是在更新數據很容易再創建的情況下,只要要通過一些控制器代碼讓用戶知道為什么更新失敗,如下所示:

  
def update
    begin
     @timesheet = Timesheet.find(params[:id])
     @timesheet.update_attributes(params[:timesheet])
     # redirect somewhere
    rescue ActiveRecord::StaleObjectError
     flash[:error] = "Timesheet was modified while you were editing it."
     redirect_to :action => ‘edit’, :id => @timesheet
   end
end

)

樂觀鎖定有很多優勢,它不需要數據庫提供特殊的支持,而且容易實現,正如你從代碼中看到的,處理StaleObjectError只需要很少的代碼.而樂觀鎖定的缺點主要集中在更新操作會慢一些,因為應用程序會檢查鎖定版本.除此以外,它可能會造成糟糕的用戶體驗,因為在更新提交之前,用戶無法知道更新能否成功.如果更新數據丟失,那么用戶會感到非常憤怒...(這才是他媽的用戶體驗...設身處地的想,我要提交了一次,然后無端端失敗,并且數據也沒了,比如寫了Blog,我肯定是沒興趣再玩下去,并且會對這個網站產生厭煩的感覺,并告知所有我知道的人....或者直接找站長理論去...)

(2)悲觀鎖定
        它需要特所的數據庫支持(大型數據類都內嵌了這樣的支持),并且在更新操作發生的時候鎖定特定的數據行.這樣防止了其他用戶讀取將要更新的數據,從而避免用戶操作已經失效的數據.
        對于Rails而言,悲觀鎖定是非常新的附加特性.在下面的例子中,悲觀鎖定與事務一起配合工作.
eg:
Timesheet.transaction do
  t = Timesheet.find(1, :lock=> true)
  t.approved = true
  t.save!
end

    對于一個存在的數據模型實例調用lock!方法也是可行的,這個方法在底層只是簡單地調用"reload(:lock => true)".對于一個底層包含屬性更改的實例,你則不應該這樣,因為重新載入的操作會丟失屬性的更改.
    悲觀鎖定在數據庫的層面上起作用.油ActiveRecord生成的SELECT語句會包含FOR UPDATE(或者類似的內容)子句,這樣就使得其他所有鏈接都無法訪問由該SELETE語句返回的數據行.只有在事務提交之后,鎖才會被釋放.從理論上說,假如Rails進程在數據事務操作完成之前結束,那么在連接被終止或者超時之前,鎖都不會被釋放


(3)需要考慮到問題
    使用樂觀鎖定的Web應用程序會獲得最佳的伸縮性,因為它不使用特定的數據庫所提供的鎖定功能,這在前文已經討論過了.不過,你不得不為應用程序增加相應的邏輯,以便處理更新失敗的情況.悲觀鎖定會實現得更容易一些,但是又會引發以下的問題:在一個Rails進程等待另外一個Rails進程釋放數據鎖定的時候,它只是在等待,也就無法為其它進入的需求提供服務了.(請記住:Rails是單線程的)
    作者說:既然在Rails中,數據庫事務的持久化并不會跨越多個HTTP請求,而悲觀鎖定又在另一個平臺上,所以它并不危險.事實上,可以確定,在這樣一個無分享(shared-nothing)架構中,跨越多個HTTP請求的數據庫事務的持久化是不可能實現的.
    另外一個需要留意到情況是許多用戶爭相訪問某個特定的記錄,而更新這個特定記錄又需要很長的時間.為了實現最佳的效果,要讓使用悲觀鎖定的事務保持足夠小,并且確定它們能夠很快的執行.

15.高級查找
     我們通常都需要通過某些條件(對應WHERE子句)對查找操作(底層就是一個SELECT的SQL語句)獲得的結果集進行篩選.ActiveRecord為你提供許多方式來實現這個功能,也就是將散列傳遞給find方法.
   
引用
(1):conditions 參數指定了查詢條件,而它可以被指定為字符串、數組或者散列,代表著SQL語句的WHERE部分.如果輸入數據源自外部,那么數組形式的參數就會被使用.
    如果你指定的條件包含布爾值,那么你就需要特別留意.不同的數據庫有多種不同的方式表達布爾值.有的是"1"和"0",而有的是"T"和"F".
    如果你使用數組或者散列作為條件并且使用Ruby布爾值作為參數,那么Rails會為你處理數據轉換的問題,這對用戶來說是透明的.
   
 
Timesheet.find(:all, :conditions => ['submitted=?', true])


(2):order選項能通過SQL子句來指定數據列的排序.
 
Timesheet.find(:all, :order => 'created_at desc')

(備注:SQL規范制定:在省略ascending/descending)選項的情況下,默認排序是升序的.

隨機排序
Rails并不會對:order選項的值進行校驗,這意味著你可以傳遞任何值,而不只是排序的列或升降序,只要底層數據庫能夠理解該值即可.在需要獲取一個隨機值的時候,該特性十分有用,以下例子說明了這一點:
# MySQL
Timesheet.find(:first, :order => ‘RAND()’)

# Postgres
Timesheet.find(:first, :order => ‘RANDOM()’)

# Microsoft SQL Server
Timesheet.find(:first, :order => ‘NEWID()’)

# Oracle
Timesheet.find(:first, :order => ‘dbms_random.value’)

(ff:請記住,對大型的數據集進行隨機排序會造成嚴重的性能問題,大多數數據庫都是如此,MySQL的問題尤為明顯.)

(3):limit能夠接受整形的參數,從而對查詢返回的數據行的數量進行限制.:offset參數則指定了偏移量,也就是從結果集的哪一行開始獲取數據行,它的取值范圍為1至最大的索引.這兩個選項合在一起,都用于對結果進行分頁.
eg:針對10個記錄一頁的時間表列表,以下代碼就用于查找第二頁的記錄.

Timesheet.find(:all, :limit => 10, :offset => 11)

(備注:根據應用程序數據模型的細節,對于某一特定的查詢,總是限制其獲取的ActiveRecord對象的最大數量是非常有意義的.如果讓用戶能夠觸發一些查詢,并一次性獲取了成千上萬的ActiveRecord對象,就無疑是引火ZF(cao ni ma ge B ---FGW,這也算敏感詞).)

(4):select
在默認情況下,:select選項是"*",與"SELECT * FROM"語句類似.但是,在需要的時候,這個值是可以改變的.
eg:
>> b = BillableWeek.find(:first, :select => "monday_hours +
tuesday_hours + wednesday_hours as three_day_total")
=> #<BillableWeek:0x2345fd8 @attributes={"three_day_total"=>"24"}>


(備注:
warning:>> b.monday_hours
NoMethodError: undefined method `monday_hours’ for
#<BillableWeek:0x2336f74 @attributes={“three_day_total”=>”24”}>
from activerecord/lib/active_record/base.rb:1850:in
`method_missing’
from (irb):38


要獲取對象常規的列值以及計算出來的列值,需要給:select參數增加個*
:select => '*, monday_hours + tuesday_hours + wednesday_hours as
three_day_total'


)

(5):form選項指定了所生成的SQL語句的表名部分.如果你需要包含用于連接的額外表或者對數據視圖的引用,那么你可以提供一個自定義值.
eg:

def find_tagged_with(list)
 find(:all,
      :select =>"#{table_name}.*",
      :from => "#{table_name}, tags, taggings",
      :conditions =>
["#{table_name}.#{primary_key}=taggings.taggable_id
      and taggings.taggable_type = ?
      and taggings.tag_id = tags.id and tags.name IN (?)",
      name, Tag.parse(list)])
end

(備注:如果你在思考為什么使用table_name而不是一個顯式的值,那么可以告訴你,因為這個代碼混入(mixin)到了一個使用Ruby 模組的目標類)

(6)group by 選項
通過某個屬性名,我們能夠將結果集進行分組,就像GROUP BY SQL子句一樣.一般來說,你需要將:group選項結合:select選項一起用,因為合法的SQL要求包含分組操作的SELECT語句所選取的列要么是聚合函數,要么就是數據表的常規列.

>> users = Account.find(:all,
:select => ‘name, SUM(cash) as money’,
:group => ‘name’)
=> [#<User:0x26a744 @attributes={“name”=>”Joe”, “money”=>”3500”}>,
#<User:0xaf33aa @attributes={“name”=>”Jane”, “money”=>”9245”}>]


(備注:額外獲取的列值是字符串類型 -------ActiveRecord并不會對它們進行類型轉換.你需要使用to_i和to_f方法將它們轉換為數據類型.
>> users.first.money > 1_000_000
ArgumentError: comparison of String with Fixnum failed
from (irb):8:in ‘>’

)

(7):lock
在事務作用域內對查找操作指定":lock => true"選項會對選取的數據行設置唯一的鎖.

(8):joins
在你執行“GROUP BY”并且聚合了其他表達數據的時候,:joins選項是非常有用的,但你并不需要裝載關聯的對象.
Buyer.find(:all,
    :select => 'buyers.id, count(carts.id) as cart_count',
    :joins => 'left join carts on carts.buyer_id=buyers.id',
    :group => 'buyers.id')


盡管如此,:joins和:include選項最常見的用法還是讓你能夠在單個SELETE語句中主動獲取(eager-fetch)額外的對象.

(9):readonly
   指定":readonly => true "選項會將返回的對象標記為只讀的.你可以改變它們的屬性,但是不能將這些更改的屬性保存到數據庫.(ff:這個是個很邪惡的用法)
>> c = Comment.find(:first, :readonly => true)
=> #<Comment id: 1, body: “Hey beeyotch!”>
>> c.body = “Keep it clean!”
=> “Keep it clean!”
>> c.save
ActiveRecord::ReadOnlyRecord: ActiveRecord::ReadOnlyRecord
from /vendor/rails/activerecord/lib/active_record/base.rb:1958



1
2
分享到:
评论
1 楼 fireflyman 2010-06-03  

針對rails常用的數據庫映射

相关推荐

Global site tag (gtag.js) - Google Analytics