# 4.1 模型的基礎操作
## 概要:
本課時講解模型的基礎操作,數據遷移,常用的 CRUD 方法,在數據查詢時,如何避免 N+1問題,如何使用 scope 包裝查詢條件,編寫模型 Rspec 測試。
## 知識點:
1. Active Record
1. Migration
1. CRUD
## 正文
### 4.1.1 Active Record 簡介
Active Record 模式,是由 Martin Fowler 的《企業應用架構模式》一書中提出的,在該模式中,一個 Active Record(簡稱 AR)對象包含了持久數據(保存在數據庫中的數據)和數據操作(對數據庫里的數據進行操作)。
對象關系映射(Object-Relational Mapping,簡稱 ORM),是將程序中的對象(Object)和關系型數據庫(Relational Database)的表之間進行關聯。使用 ORM 可以方便的將對象的 `屬性` 和 `關聯關系` 保存入數據庫,這樣可以不必編寫復雜的 SQL 語句,而且不必擔心使用的是哪種數據庫,一次編寫的代碼可以應用在 Sqlite,Mysql,PostgreSQL 等各種數據庫上。
Active Record 就是個 ORM 框架。
所以,我們可以用 Actice Record 來做這幾件事:
- 表示模型(Model)和模型數據
- 表示模型間的關系(比如一對多,多對多關系)
- 通過模型間關聯表示繼承層次
- 在保存如數據庫前,校驗模型(比如屬性校驗)
- 用 `面向對象` 的方式處理數據庫
### 4.1.2 Active Record 中的約定
Rails 中使用了 ActiveRecord 這個 Gem,使用它可以不必去做任何配置(大多數情況是這樣的),還記得 Rails 的兩個哲學理念之一么:`約定優于配置`。(另一個是 `不要重復自己`,這是 Dave Thomas 在《程序員修煉之道》一書里提出的。)
那么,我們講兩個 Active Record 中的約定:
#### 4.1.2.1 命名約定
- 數據表名:復數,下劃線分隔單詞(例如 book_clubs)
- 模型類名:單數,每個單詞的首字母大寫(例如 BookClub)
比如:
| 模型(Class) | 數據表(Schema) |
|-----|-----|
| Post | posts |
| LineItem | line_items |
| Deer | deers |
| Mouse | mice |
| Person | people |
單詞在單復數轉換時,是按照英文語法約定的。
#### 4.1.2.2 Schema 約定
注:數據庫中的 Schema,指數據庫對象集合,可以被用戶直接使用。Schema 包含數據的邏輯結構,用戶可以通過命名調用數據庫對象,并且安全的管理數據庫。
- 外鍵 - 使用 singularized_table_name_id 形式命名,例如 item_id,order_id。創建模型關聯后,Active Record 會查找這個字段;
- 主鍵 - 默認情況下,Active Record 使用整數字段 id 作為表的主鍵。使用 Active Record 遷移創建數據表時,會自動創建這個字段;
在數據庫字段命名的時候,有幾個特殊意義的名字,盡量回避:
- created_at - 創建記錄時,自動設為當前的時間戳
- updated_at - 更新記錄時,自動設為當前的時間戳
- lock_version - 在模型中添加樂觀鎖定功能
- type - 讓模型使用單表繼承,給字段命名的時候,盡量避開這個詞
- (association_name)_type - 多態關聯的類型
- (table_name)_count - 保存關聯對象的數量。例如,posts 表中的 comments_count 字段,Rails 會自動更新該文章的評論數
### 4.1.3 數據庫遷移(Migration)
在我們使用 scaffold 創建資源的時候,或者使用 generate 創建 model 的時候,Rails 會給我們自動創建一個數據庫遷移文件,它在 `db/migrate` 中,它的前綴是時間戳,他們按照時間的先后順序排列,當運行數據庫遷移時,他們按照時間順序先后被執行。
新創建的遷移文件,我們使用 `rake db:migrate` 命令執行它(們),這里會判斷,哪個遷移文件是還沒有被執行的。
如果我們對執行過的遷移操作不滿意,我們可以回滾這個遷移:
~~~
rake db:rollback [1]
rake db:rollback STEP=3 [2]
~~~
[1] 回滾最近的一個遷移
[2] 回滾指定的遷移個數
回滾之后,遷移停留在回滾到的那個位置的,schema 也會更新到那個位置時的狀態。比如,我們上一次遷移執行了5個文件,我們回滾的時候,是一個個文件回滾的,所以我們指定 STEP=5,才能把剛才遷移的5個文件回滾。
在我們開發代碼的過程中,有是會因為失誤少寫了一個字段,我們回滾之后,在遷移文件中把它加上,然后,我們 `rake db:migrate` 再次運行。不過,`rake db:migrate:redo [STEP=3]` 直接回滾然后再次運行遷移,這樣會方便些。
這種回滾操作適合開發過程中,出現了新的想法,而回滾最近連續的幾個遷移。
如果我們想回滾很久以前的某個操作,而且在那個遷移之后,我們已經執行了多個遷移。這時該如何處理呢?
如果在開發階段,我們干脆 `rake db:drop`,`rake db:create`,`rake db:migrate`。但是在生產環境,我們決不能這么做,這時我們要針對需求,編寫一個遷移文件:
~~~
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`用于回滾。
我們創建一個 model 的時候,會自動創建它的 migration 文件,我們還可以使用 `rails g migration XXX`的方法,添加自定義的遷移文件。如果我們的命名是 "AddXXXToYYY" 或者 "RemoveXXXFromYYY" 時,會自動為我們添加字符類型的字段,比如我為 variant 添加一個color 字段:
~~~
rails g migration AddColorToVariants color:string
~~~
它的內容是:
~~~
class AddColorToVariants < ActiveRecord::Migration
def change
add_column :variants, :color, :string
end
end
~~~
### 4.1.4 CRUD
CRUD并不是一個 Rails 的概念,它表示系統(業務層)和數據庫(持久層)之間的基本操作,簡單的講叫“增(C)刪(D)改(U)查(R)”。
我們已經使用 scaffold 命令創建了資源:商品(product),我們現在使用 `app/models/product.rb` 來演示這些操作。
首先,我們需要讓 Product 類繼承 ActiveRecord:
~~~
class Product < ActiveRecord::Base
end
~~~
這樣,Product 類就可以操作數據庫了,是不是很簡單。
### 4.1.5 創建記錄
我們使用 Product 類,向數據添加一條記錄,我們先進入 Rails 控制臺:
~~~
% 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,創建了一條記錄。我們還有其他的方法保存記錄。
[2],begin 和 commit ,將我們的數據保存入數據庫。如果在保存的時候出現錯誤,比如屬性校驗失敗,拋出異常等,不會將記錄保存到數據庫。
[3],我們拿到了一個 Product 類的實例。
除了類方法,我們還可以使用實例的 `save` 方法,來保存記錄到數據,比如:
~~~
> 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,來創建一個實例,注意,[2] 告訴我們,這是一個沒有保存到數據庫的實例,因為它的 id 還是 nil。
[3] 我們使用實例方法 save,把這個實例,保存到數據庫。
[4] 調用 save 后,會返回執行結果,true 或者 false。這種判斷很有用,而且也很常見,如果你現在打開 `app/controllers/products_controller.rb` 的話,可以看到這樣的判斷:
~~~
if @product.save
...
else
...
end
~~~
那么,你可能會有個疑問,使用類方法 create 保存的時候,如果失敗,會返回我們什么呢?是一個實例,還是 false?
我們使用下一章里要介紹的屬性校驗,來讓保存失敗,比如,我們讓商品的名稱必須填寫:
~~~
class Product < ActiveRecord::Base
validates :name, presence: true [1]
end
~~~
[1] validates 是校驗命令,要求 name 屬性必須填寫。
好了,我們來測試下類方法 create 會返回給我們什么:
~~~
> 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 >
~~~
答案揭曉,它返回給我們一個未保存的實例,它有一個實用的方法,可以查看哪里出了錯誤:
~~~
> product.errors.full_messages
=> ["名稱不能為空字符"]
~~~
當然,判斷一個實例是否保存成功,不必去檢查它的 errors 是否為空,有兩個方法會根據 errors 是否添加,而返回實例的狀態:
~~~
person = Person.new
person.invalid?
person.valid?
~~~
要留意的是,invalid? 和 valid? 都會調用實例的校驗。
我使用類方法和實例方法的稱呼,希望沒有給你造成理解的障礙,如果有些難理解,建議你先看一看 Ruby 中關于類和實例的介紹。
### 4.1.6 查詢記錄
#### 4.1.6.1 Find 查詢
數據查詢,是 Rails 項目經常要做的操作,如何拿到準確的數據,優化查詢,是我們要重點關注的。
查詢時,會得到兩種結果,一個實例,或者實例的集合(Array)。如果找不到結果,也會給有兩種情況,返回 nil或空數組,或者拋出 ActiveRecord::RecordNotFound 異常。
Rails 給我們提供了這些常用的查詢方法:
| 方法名稱 | 含義 | 參數 | 例子 | 找不到時 |
|-----|-----|-----|-----|-----|
| find | 獲取指定主鍵對應的對象 | 主鍵值 | Product.find(10) | 異常 |
| take | 獲取一個記錄,不考慮任何順序 | 無 | Product.take | nil |
| first | 獲取按主鍵排序得到的第一個記錄 | 無 | Product.first | nil |
| last | 獲取按主鍵排序得到的最后一個記錄 | 無 | Product.last | nil |
| find_by | 獲取滿足條件的第一個記錄 | hash | Product.find_by(name: "T恤") | nil |
表中的四個方法不會拋出異常,如果需要拋出異常,可以在他們名字后面加上 `!`,比如 Product.take!。
如果將上面幾個方法的參數改動,我們就會得到集合:
| 方法名稱 | 含義 | 參數 | 例子 | 找不到時 |
|-----|-----|-----|-----|-----|
| find | 獲取指定主鍵對應的對象 | 主鍵值集合 | Product.find([1,2,3]) | 異常 |
| take | 獲取一個記錄,不考慮任何順序 | 個數 | Product.take(2) | [] |
| first | 獲取按主鍵排序得到的第N個記錄 | 個數 | Product.first(3) | [] |
| last | 獲取按主鍵排序得到的最后N個記錄 | 個數 | Product.last(4) | [] |
| all | 獲取按主鍵排序得到的全部記錄 | 無 | Product.all | [] |
Rails 還提供了一個 find_by 的查詢方法,它可以接收多個查詢參數,返回符合條件的第一個記錄。比如:
~~~
Product.find_by(name: 'T-Shirt', price: 59.99)
~~~
`find_by` 有一個常用的變形,比如:
~~~
Product.find_by_name("Hat")
Product.find_by_name_and_price("Hat", 9.99)
~~~
如果需要查詢不到結果拋出異常,可以使用 `find_by!`。通常,以`!`結尾的方法都會拋出異常,這也是一種約定。不過,直接使用 find,會查詢主索引,查詢不到直接拋出異常,所以是沒有 `find!` 方法的。
使用 find_by 的時候,還可以使用 sql 語句,比如:
~~~
Product.find_by("name = ?", "T")
~~~
這是一個有用的查詢,當我們搜索多個條件,并且是 OR 關系時,可以這樣做:
~~~
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])
~~~
#### 4.1.6.2 Where 查詢
集合的查找,最常用的方法是 `where`,它可以通過多種形式查找記錄:
| 查詢形式 | 實例 |
|-----|-----|
| 數組(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%")
~~~
查詢某個區間:
~~~
Product.where(price: 5..6)
~~~
以及上面提到的,sql 的查詢:
~~~
Product.where("color = ? OR price > ?", "red", 9)
~~~
Active Record 有多種查詢方法,以至于 Rails 手冊中單獨列出一章來講解,而且講解的很細致,如果你想靈活的掌握這些數據查詢方法,建議你經常閱讀 [Active Record Query Interface](http://guides.rubyonrails.org/active_record_querying.html) 一章,這是 [中文版](http://guides.ruby-china.org/active_record_querying.html)。
### 4.1.7 更新記錄(Update)
和創建記錄一樣,更新記錄也可以使用類方法和實力方法。
類方法是 update,比如:
~~~
Product.update(1, name: "T-Shirt", price: 23)
~~~
1 是更新目標的 ID,如果該記錄不存在,update 會拋出 `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 更新記錄的時候,會調用 Model 的 validates(校驗) 和 callbacks(回調),保證我們寫入正確的數據,這個是定義在 Model 中的方法。但是,update_all 會略過校驗和回調,直接將數據寫入到數據庫中。
和 update_all 類似,update_column/update_columns 也是將數據直接寫入到數據庫,它是一個實例方法:
~~~
product = Product.first
product.update_column(:name, "")
product.update_columns(name: "", price: 0)
~~~
雖然為 product 增加了 name 非空的校驗,但是 update_column(s) 還是可以講數據寫入數據庫。
當我們創建遷移文件的時候,Rails 默認會添加兩個時間戳字段,created_at 和 updated_at。
當我們使用 update 更新記錄時,觸發 Model 的校驗和回調時,也會自動更新 updated_at 字段。但是 Model.update_all 和 model.update_column(s) 在跳過回調和校驗的同時,也不會更新 updated_at 字段。
我們也可以用 save 方法,將新的屬性保存到數據庫,這也會觸發調用和回調,以及更新時間戳:
~~~
product = Product.first
product.name = "Shoes"
product.save
~~~
### 4.1.8 刪除記錄(Destroy)
在我們接觸計算機英語里,表示刪除的英文有很多,這里我們用到的是 destroy, delete。
#### 4.1.8.1 Delete 刪除
使用 delete 刪除時,會跳過回調,以及關聯關系中定義的 `:dependent` 選項,直接從數據庫中刪除,它是一個類方法,比如:
~~~
Product.delete(1)
Product.delete([2,3,4])
~~~
當傳入的 id 不存在的時候,它不會拋出任何異常,看下它的源碼:
~~~
# File activerecord/lib/active_record/relation.rb, line 502
def delete(id_or_array)
where(primary_key => id_or_array).delete_all
end
~~~
它使用不拋出異常的 where 方法查找記錄,然后調用 delete_all。
delete 也可以是實例方法,比如:
~~~
product = Product.first
product.delete
~~~
在有具體實例的時候,可以這樣使用,否則會產生 `NoMethodError: undefined method`delete' for nil:NilClass`,這在我們設計邏輯的時候要注意。
delete_all 方法和 delete 是一樣的,直接發送數據刪除的命令,看一下 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
~~~
#### 4.1.8.2 Destroy 刪除
destroy 方法,會觸發 model 中定義的回調(before_remove, after_remove , before_destroy 和 after_destroy),保證我們正確的操作。它也可以是類方法和實例方法,用法和前面的一樣。
需要說明,delete/delete_all 和 destroy/destroy_all 都可以作用在關系查詢結果,也就是(ActiveRecord::Relation)上,刪掉查找到的記錄。
如果你不想真正從數據庫中抹掉數據,而是給它一個刪除標注,可以使用 [https://github.com/radar/paranoia](https://github.com/radar/paranoia) 這個 gem,他會給記錄一個 deleted_at 時間戳,并且使用 `restore` 方法把它從數據庫中恢復過來,或者使用 `really_destroy!` 將它真正的刪除掉。
- 寫在前面
- 第一章 Ruby on Rails 概述
- Ruby on Rails 開發環境介紹
- Rails 文件簡介
- 用戶界面(UI)設計
- 第二章 Rails 中的資源
- 應用 scaffold 命令創建資源
- REST 架構
- 深入路由(routes)
- 第三章 Rails 中的視圖
- 布局和輔助方法
- 表單
- 視圖中的 AJAX 交互
- 模板引擎的使用
- 第四章 Rails 中的模型
- 模型的基礎操作
- 深入模型查詢
- 模型中的關聯關系
- 模型中的校驗
- 模型中的回調
- 第五章 Rails 中的控制器
- 控制器中的方法
- 控制器中的邏輯
- 第六章 Rails 的配置及部署
- Assets 管理
- 緩存及緩存服務
- 異步任務及郵件發送
- I18n
- 生產環境部署
- 常用 Gem
- 寫在后面