本課程講解Rails 模型(Model)中基本的 CRUD 操作、模型間的關(guān)聯(lián)關(guān)系、屬性校驗(yàn)、回調(diào)以及編寫 Rspec 測(cè)試的方法,并完成網(wǎng)店的數(shù)據(jù)庫模型設(shè)計(jì)。
模型(Model)是 MVC 架構(gòu)中的 M,代表數(shù)據(jù)庫,通過對(duì)模型的學(xué)習(xí),可以了解 Rails 是如何實(shí)現(xiàn)數(shù)據(jù)庫操作的。
本課時(shí)講解模型的基礎(chǔ)操作,數(shù)據(jù)遷移,常用的 CRUD 方法,在數(shù)據(jù)查詢時(shí),如何避免 N+1問題,如何使用 scope 包裝查詢條件,編寫模型 Rspec 測(cè)試。
Active Record 模式,是由 Martin Fowler 的《企業(yè)應(yīng)用架構(gòu)模式》一書中提出的,在該模式中,一個(gè) Active Record(簡稱 AR)對(duì)象包含了持久數(shù)據(jù)(保存在數(shù)據(jù)庫中的數(shù)據(jù))和數(shù)據(jù)操作(對(duì)數(shù)據(jù)庫里的數(shù)據(jù)進(jìn)行操作)。
對(duì)象關(guān)系映射(Object-Relational Mapping,簡稱 ORM),是將程序中的對(duì)象(Object)和關(guān)系型數(shù)據(jù)庫(Relational Database)的表之間進(jìn)行關(guān)聯(lián)。使用 ORM 可以方便的將對(duì)象的 屬性
和 關(guān)聯(lián)關(guān)系
保存入數(shù)據(jù)庫,這樣可以不必編寫復(fù)雜的 SQL 語句,而且不必?fù)?dān)心使用的是哪種數(shù)據(jù)庫,一次編寫的代碼可以應(yīng)用在 Sqlite,Mysql,PostgreSQL 等各種數(shù)據(jù)庫上。
Active Record 就是個(gè) ORM 框架。
所以,我們可以用 Actice Record 來做這幾件事:
面向?qū)ο?/code> 的方式處理數(shù)據(jù)庫
Rails 中使用了 ActiveRecord 這個(gè) Gem,使用它可以不必去做任何配置(大多數(shù)情況是這樣的),還記得 Rails 的兩個(gè)哲學(xué)理念之一么:約定優(yōu)于配置
。(另一個(gè)是 不要重復(fù)自己
,這是 Dave Thomas 在《程序員修煉之道》一書里提出的。)
那么,我們講兩個(gè) Active Record 中的約定:
比如:
模型(Class) | 數(shù)據(jù)表(Schema) |
---|---|
Post | posts |
LineItem | line_items |
Deer | deers |
Mouse | mice |
Person | people |
單詞在單復(fù)數(shù)轉(zhuǎn)換時(shí),是按照英文語法約定的。
注:數(shù)據(jù)庫中的 Schema,指數(shù)據(jù)庫對(duì)象集合,可以被用戶直接使用。Schema 包含數(shù)據(jù)的邏輯結(jié)構(gòu),用戶可以通過命名調(diào)用數(shù)據(jù)庫對(duì)象,并且安全的管理數(shù)據(jù)庫。
在數(shù)據(jù)庫字段命名的時(shí)候,有幾個(gè)特殊意義的名字,盡量回避:
在我們使用 scaffold 創(chuàng)建資源的時(shí)候,或者使用 generate 創(chuàng)建 model 的時(shí)候,Rails 會(huì)給我們自動(dòng)創(chuàng)建一個(gè)數(shù)據(jù)庫遷移文件,它在 db/migrate
中,它的前綴是時(shí)間戳,他們按照時(shí)間的先后順序排列,當(dāng)運(yùn)行數(shù)據(jù)庫遷移時(shí),他們按照時(shí)間順序先后被執(zhí)行。
新創(chuàng)建的遷移文件,我們使用 rake db:migrate
命令執(zhí)行它(們),這里會(huì)判斷,哪個(gè)遷移文件是還沒有被執(zhí)行的。
如果我們對(duì)執(zhí)行過的遷移操作不滿意,我們可以回滾這個(gè)遷移:
rake db:rollback [1]
rake db:rollback STEP=3 [2]
[1] 回滾最近的一個(gè)遷移
[2] 回滾指定的遷移個(gè)數(shù)
回滾之后,遷移停留在回滾到的那個(gè)位置的,schema 也會(huì)更新到那個(gè)位置時(shí)的狀態(tài)。比如,我們上一次遷移執(zhí)行了5個(gè)文件,我們回滾的時(shí)候,是一個(gè)個(gè)文件回滾的,所以我們指定 STEP=5,才能把剛才遷移的5個(gè)文件回滾。
在我們開發(fā)代碼的過程中,有是會(huì)因?yàn)槭д`少寫了一個(gè)字段,我們回滾之后,在遷移文件中把它加上,然后,我們 rake db:migrate
再次運(yùn)行。不過,rake db:migrate:redo [STEP=3]
直接回滾然后再次運(yùn)行遷移,這樣會(huì)方便些。
這種回滾操作適合開發(fā)過程中,出現(xiàn)了新的想法,而回滾最近連續(xù)的幾個(gè)遷移。
如果我們想回滾很久以前的某個(gè)操作,而且在那個(gè)遷移之后,我們已經(jīng)執(zhí)行了多個(gè)遷移。這時(shí)該如何處理呢?
如果在開發(fā)階段,我們干脆 rake db:drop
,rake db:create
,rake db:migrate
。但是在生產(chǎn)環(huán)境,我們決不能這么做,這時(shí)我們要針對(duì)需求,編寫一個(gè)遷移文件:
class ChangeProductsPrice < ActiveRecord::Migration
??def change
????reversible do |dir|
??????change_table :products do |t|
????????dir.up?? { t.change :price, :string }
????????dir.down { t.change :price, :integer }
??????end
????end
??end
end
或者:
class ChangeProductsPrice < ActiveRecord::Migration
def up
change_table :products do |t|
t.change :price, :string
end
end
def down
change_table :products do |t|
t.change :price, :integer
end
end
end
up
是向前遷移到最新的,down
用于回滾。
我們創(chuàng)建一個(gè) model 的時(shí)候,會(huì)自動(dòng)創(chuàng)建它的 migration 文件,我們還可以使用 rails g migration XXX
的方法,添加自定義的遷移文件。如果我們的命名是 "AddXXXToYYY" 或者 "RemoveXXXFromYYY" 時(shí),會(huì)自動(dòng)為我們添加字符類型的字段,比如我為 variant 添加一個(gè)color 字段:
rails g migration AddColorToVariants color:string
它的內(nèi)容是:
class AddColorToVariants < ActiveRecord::Migration
def change
add_column :variants, :color, :string
end
end
CRUD并不是一個(gè) Rails 的概念,它表示系統(tǒng)(業(yè)務(wù)層)和數(shù)據(jù)庫(持久層)之間的基本操作,簡單的講叫“增(C)刪(D)改(U)查(R)”。
我們已經(jīng)使用 scaffold 命令創(chuàng)建了資源:商品(product),我們現(xiàn)在使用 app/models/product.rb
來演示這些操作。
首先,我們需要讓 Product 類繼承 ActiveRecord:
class Product < ActiveRecord::Base
end
這樣,Product 類就可以操作數(shù)據(jù)庫了,是不是很簡單。
我們使用 Product 類,向數(shù)據(jù)添加一條記錄,我們先進(jìn)入 Rails 控制臺(tái):
% rails c
Loading development environment (Rails 4.2.0)
> Product.create [1]
(0.2ms) begin transaction [2]
SQL (2.8ms) INSERT INTO "products" ("created_at", "updated_at") VALUES (?, ?) [["created_at", "2015-03-14 16:23:44.640578"], ["updated_at", "2015-03-14 16:23:44.640578"]]
(0.8ms) commit transaction [2]
=> #<Product id: 1, name: nil, description: nil, price: nil, created_at: "2015-03-14 16:23:44", updated_at: "2015-03-14 16:23:44"> [3]
這里,我貼出了完整的代碼。
[1],我們使用了 Product 的類方法 create,創(chuàng)建了一條記錄。我們還有其他的方法保存記錄。
[2],begin 和 commit ,將我們的數(shù)據(jù)保存入數(shù)據(jù)庫。如果在保存的時(shí)候出現(xiàn)錯(cuò)誤,比如屬性校驗(yàn)失敗,拋出異常等,不會(huì)將記錄保存到數(shù)據(jù)庫。
[3],我們拿到了一個(gè) Product 類的實(shí)例。
除了類方法,我們還可以使用實(shí)例的 save
方法,來保存記錄到數(shù)據(jù),比如:
> product = Product.new [1]
=> #<Product id: nil, name: nil, description: nil, price: nil, created_at: nil, updated_at: nil> [2]
> product.save [3]
(0.1ms) begin transaction [4]
SQL (0.9ms) INSERT INTO "products" ("created_at", "updated_at") VALUES (?, ?) [["created_at", "2015-03-14 16:47:26.817663"], ["updated_at", "2015-03-14 16:47:26.817663"]]
(9.3ms) commit transaction [4]
=> true [5]
[1],我們使用類方法 new,來創(chuàng)建一個(gè)實(shí)例,注意,[2] 告訴我們,這是一個(gè)沒有保存到數(shù)據(jù)庫的實(shí)例,因?yàn)樗?id 還是 nil。
[3] 我們使用實(shí)例方法 save,把這個(gè)實(shí)例,保存到數(shù)據(jù)庫。
[4] 調(diào)用 save 后,會(huì)返回執(zhí)行結(jié)果,true 或者 false。這種判斷很有用,而且也很常見,如果你現(xiàn)在打開 app/controllers/products_controller.rb
的話,可以看到這樣的判斷:
if @product.save
...
else
...
end
那么,你可能會(huì)有個(gè)疑問,使用類方法 create 保存的時(shí)候,如果失敗,會(huì)返回我們什么呢?是一個(gè)實(shí)例,還是 false?
我們使用下一章里要介紹的屬性校驗(yàn),來讓保存失敗,比如,我們讓商品的名稱必須填寫:
class Product < ActiveRecord::Base
validates :name, presence: true [1]
end
[1] validates 是校驗(yàn)命令,要求 name 屬性必須填寫。
好了,我們來測(cè)試下類方法 create 會(huì)返回給我們什么:
> product = Product.create
(0.3ms) begin transaction
(0.1ms) rollback transaction
=> #<Product id: nil, name: nil, description: nil, price: nil, created_at: nil, updated_at: nil>
2.2.0 :003 >
答案揭曉,它返回給我們一個(gè)未保存的實(shí)例,它有一個(gè)實(shí)用的方法,可以查看哪里出了錯(cuò)誤:
> product.errors.full_messages
=> ["名稱不能為空字符"]
當(dāng)然,判斷一個(gè)實(shí)例是否保存成功,不必去檢查它的 errors 是否為空,有兩個(gè)方法會(huì)根據(jù) errors 是否添加,而返回實(shí)例的狀態(tài):
person = Person.new
person.invalid?
person.valid?
要留意的是,invalid? 和 valid? 都會(huì)調(diào)用實(shí)例的校驗(yàn)。
我使用類方法和實(shí)例方法的稱呼,希望沒有給你造成理解的障礙,如果有些難理解,建議你先看一看 Ruby 中關(guān)于類和實(shí)例的介紹。
數(shù)據(jù)查詢,是 Rails 項(xiàng)目經(jīng)常要做的操作,如何拿到準(zhǔn)確的數(shù)據(jù),優(yōu)化查詢,是我們要重點(diǎn)關(guān)注的。
查詢時(shí),會(huì)得到兩種結(jié)果,一個(gè)實(shí)例,或者實(shí)例的集合(Array)。如果找不到結(jié)果,也會(huì)給有兩種情況,返回 nil或空數(shù)組,或者拋出 ActiveRecord::RecordNotFound 異常。
Rails 給我們提供了這些常用的查詢方法:
方法名稱 | 含義 | 參數(shù) | 例子 | 找不到時(shí) |
---|---|---|---|---|
find | 獲取指定主鍵對(duì)應(yīng)的對(duì)象 | 主鍵值 | Product.find(10) | 異常 |
take | 獲取一個(gè)記錄,不考慮任何順序 | 無 | Product.take | nil |
first | 獲取按主鍵排序得到的第一個(gè)記錄 | 無 | Product.first | nil |
last | 獲取按主鍵排序得到的最后一個(gè)記錄 | 無 | Product.last | nil |
find_by | 獲取滿足條件的第一個(gè)記錄 | hash | Product.find_by(name: "T恤") | nil |
表中的四個(gè)方法不會(huì)拋出異常,如果需要拋出異常,可以在他們名字后面加上 !
,比如 Product.take!。
如果將上面幾個(gè)方法的參數(shù)改動(dòng),我們就會(huì)得到集合:
方法名稱 | 含義 | 參數(shù) | 例子 | 找不到時(shí) |
---|---|---|---|---|
find | 獲取指定主鍵對(duì)應(yīng)的對(duì)象 | 主鍵值集合 | Product.find([1,2,3]) | 異常 |
take | 獲取一個(gè)記錄,不考慮任何順序 | 個(gè)數(shù) | Product.take(2) | [] |
first | 獲取按主鍵排序得到的第N個(gè)記錄 | 個(gè)數(shù) | Product.first(3) | [] |
last | 獲取按主鍵排序得到的最后N個(gè)記錄 | 個(gè)數(shù) | Product.last(4) | [] |
all | 獲取按主鍵排序得到的全部記錄 | 無 | Product.all | [] |
Rails 還提供了一個(gè) find_by 的查詢方法,它可以接收多個(gè)查詢參數(shù),返回符合條件的第一個(gè)記錄。比如:
Product.find_by(name: 'T-Shirt', price: 59.99)
find_by
有一個(gè)常用的變形,比如:
Product.find_by_name("Hat")
Product.find_by_name_and_price("Hat", 9.99)
如果需要查詢不到結(jié)果拋出異常,可以使用 find_by!
。通常,以!
結(jié)尾的方法都會(huì)拋出異常,這也是一種約定。不過,直接使用 find,會(huì)查詢主索引,查詢不到直接拋出異常,所以是沒有 find!
方法的。
使用 find_by 的時(shí)候,還可以使用 sql 語句,比如:
Product.find_by("name = ?", "T")
這是一個(gè)有用的查詢,當(dāng)我們搜索多個(gè)條件,并且是 OR 關(guān)系時(shí),可以這樣做:
User.find_by("id = ? OR login = ?", params[:id], params[:id])
這句話還可以改寫成:
User.find_by("id = :id OR login = :name", id: params[:id], name: params[:id])
或者更簡潔的:
User.find_by("id = :q OR login = :q", q: params[:id])
集合的查找,最常用的方法是 where
,它可以通過多種形式查找記錄:
查詢形式 | 實(shí)例 |
---|---|
數(shù)組(Array)查詢 | Product.where("name = ? and price = ?", "T恤", 9.99) |
哈希(hash)查詢 | Product.where(name: "T恤", price: 9.99) |
Not查詢 | Product.where.not(price: 9.99) |
空 | Product.none |
使用 where 查詢,常見的還有模糊查詢:
Product.where("name like ?", "%a%")
查詢某個(gè)區(qū)間:
Product.where(price: 5..6)
以及上面提到的,sql 的查詢:
Product.where("color = ? OR price > ?", "red", 9)
Active Record 有多種查詢方法,以至于 Rails 手冊(cè)中單獨(dú)列出一章來講解,而且講解的很細(xì)致,如果你想靈活的掌握這些數(shù)據(jù)查詢方法,建議你經(jīng)常閱讀 Active Record Query Interface 一章,這是 中文版。
和創(chuàng)建記錄一樣,更新記錄也可以使用類方法和實(shí)力方法。
類方法是 update,比如:
Product.update(1, name: "T-Shirt", price: 23)
1 是更新目標(biāo)的 ID,如果該記錄不存在,update 會(huì)拋出 ActiveRecord::RecordNotFound
異常。
update
也可以更新多條記錄,比如:
Product.update([1, 2], [{ name: "Glove", price: 19 }, { name: "Scarf" }])
我們看看它的源代碼:
# File activerecord/lib/active_record/relation.rb, line 363
def update(id, attributes)
if id.is_a?(Array)
id.map.with_index { |one_id, idx| update(one_id, attributes[idx]) }
else
object = find(id)
object.update(attributes)
object
end
end
如果要更新全部記錄,可以使用 update_all :
Product.update_all(price: 20)
在使用 update 更新記錄的時(shí)候,會(huì)調(diào)用 Model 的 validates(校驗(yàn)) 和 callbacks(回調(diào)),保證我們寫入正確的數(shù)據(jù),這個(gè)是定義在 Model 中的方法。但是,update_all 會(huì)略過校驗(yàn)和回調(diào),直接將數(shù)據(jù)寫入到數(shù)據(jù)庫中。
和 update_all 類似,update_column/update_columns 也是將數(shù)據(jù)直接寫入到數(shù)據(jù)庫,它是一個(gè)實(shí)例方法:
product = Product.first
product.update_column(:name, "")
product.update_columns(name: "", price: 0)
雖然為 product 增加了 name 非空的校驗(yàn),但是 update_column(s) 還是可以講數(shù)據(jù)寫入數(shù)據(jù)庫。
當(dāng)我們創(chuàng)建遷移文件的時(shí)候,Rails 默認(rèn)會(huì)添加兩個(gè)時(shí)間戳字段,created_at 和 updated_at。
當(dāng)我們使用 update 更新記錄時(shí),觸發(fā) Model 的校驗(yàn)和回調(diào)時(shí),也會(huì)自動(dòng)更新 updated_at 字段。但是 Model.update_all 和 model.update_column(s) 在跳過回調(diào)和校驗(yàn)的同時(shí),也不會(huì)更新 updated_at 字段。
我們也可以用 save 方法,將新的屬性保存到數(shù)據(jù)庫,這也會(huì)觸發(fā)調(diào)用和回調(diào),以及更新時(shí)間戳:
product = Product.first
product.name = "Shoes"
product.save
在我們接觸計(jì)算機(jī)英語里,表示刪除的英文有很多,這里我們用到的是 destroy, delete。
使用 delete 刪除時(shí),會(huì)跳過回調(diào),以及關(guān)聯(lián)關(guān)系中定義的 :dependent
選項(xiàng),直接從數(shù)據(jù)庫中刪除,它是一個(gè)類方法,比如:
Product.delete(1)
Product.delete([2,3,4])
當(dāng)傳入的 id 不存在的時(shí)候,它不會(huì)拋出任何異常,看下它的源碼:
# File activerecord/lib/active_record/relation.rb, line 502
def delete(id_or_array)
where(primary_key => id_or_array).delete_all
end
它使用不拋出異常的 where 方法查找記錄,然后調(diào)用 delete_all。
delete 也可以是實(shí)例方法,比如:
product = Product.first
product.delete
在有具體實(shí)例的時(shí)候,可以這樣使用,否則會(huì)產(chǎn)生 NoMethodError: undefined method
delete' for nil:NilClass`,這在我們?cè)O(shè)計(jì)邏輯的時(shí)候要注意。
delete_all 方法和 delete 是一樣的,直接發(fā)送數(shù)據(jù)刪除的命令,看一下 api 文檔中的例子:
Post.delete_all("person_id = 5 AND (category = 'Something' OR category = 'Else')")
Post.delete_all(["person_id = ? AND (category = ? OR category = ?)", 5, 'Something', 'Else'])
Post.where(person_id: 5).where(category: ['Something', 'Else']).delete_all
destroy 方法,會(huì)觸發(fā) model 中定義的回調(diào)(before_remove, after_remove , before_destroy 和 after_destroy),保證我們正確的操作。它也可以是類方法和實(shí)例方法,用法和前面的一樣。
需要說明,delete/delete_all 和 destroy/destroy_all 都可以作用在關(guān)系查詢結(jié)果,也就是(ActiveRecord::Relation)上,刪掉查找到的記錄。
如果你不想真正從數(shù)據(jù)庫中抹掉數(shù)據(jù),而是給它一個(gè)刪除標(biāo)注,可以使用 https://github.com/radar/paranoia 這個(gè) gem,他會(huì)給記錄一個(gè) deleted_at 時(shí)間戳,并且使用 restore
方法把它從數(shù)據(jù)庫中恢復(fù)過來,或者使用 really_destroy!
將它真正的刪除掉。