鍍金池/ 教程/ Ruby/ 第四章 Rails 中的模型
寫在后面
寫在前面
第六章 Rails 的配置及部署
第四章 Rails 中的模型
4.4 模型中的校驗(yàn)(Validates)
1.3 用戶界面(UI)設(shè)計(jì)
6.5 生產(chǎn)環(huán)境部署
3.2 表單
4.3 模型中的關(guān)聯(lián)關(guān)系(Relations)
4.5 模型中的回調(diào)(Callback)
第五章 Rails 中的控制器
4.2 深入模型查詢
5.2 控制器中的方法
6.2 緩存
3.4 模板引擎的使用
6.4 I18n
第一章 Ruby on Rails 概述
6.6 常用 Gem
1.2 Rails 文件簡介
2.2 REST 架構(gòu)
2.3 深入路由(routes)
第三章 Rails 中的視圖
6.3 異步任務(wù)及郵件發(fā)送
第二章 Rails 中的資源
3.3 視圖中的 AJAX 交互

第四章 Rails 中的模型

課程概要:

本課程講解Rails 模型(Model)中基本的 CRUD 操作、模型間的關(guān)聯(lián)關(guān)系、屬性校驗(yàn)、回調(diào)以及編寫 Rspec 測(cè)試的方法,并完成網(wǎng)店的數(shù)據(jù)庫模型設(shè)計(jì)。

知識(shí)點(diǎn):

  1. CRUD
  2. 數(shù)據(jù)庫遷移(Migration)
  3. 表間關(guān)聯(lián)(Relations)
  4. 屬性校驗(yàn)(Validates)
  5. 回調(diào)(Callback)

課程背景

模型(Model)是 MVC 架構(gòu)中的 M,代表數(shù)據(jù)庫,通過對(duì)模型的學(xué)習(xí),可以了解 Rails 是如何實(shí)現(xiàn)數(shù)據(jù)庫操作的。

4.1 模型的基礎(chǔ)操作

概要:

本課時(shí)講解模型的基礎(chǔ)操作,數(shù)據(jù)遷移,常用的 CRUD 方法,在數(shù)據(jù)查詢時(shí),如何避免 N+1問題,如何使用 scope 包裝查詢條件,編寫模型 Rspec 測(cè)試。

知識(shí)點(diǎn):

  1. Active Record
  2. Migration
  3. CRUD

正文

4.1.1 Active Record 簡介

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 來做這幾件事:

  • 表示模型(Model)和模型數(shù)據(jù)
  • 表示模型間的關(guān)系(比如一對(duì)多,多對(duì)多關(guān)系)
  • 通過模型間關(guān)聯(lián)表示繼承層次
  • 在保存如數(shù)據(jù)庫前,校驗(yàn)?zāi)P停ū热鐚傩孕r?yàn))
  • 面向?qū)ο?/code> 的方式處理數(shù)據(jù)庫

4.1.2 Active Record 中的約定

Rails 中使用了 ActiveRecord 這個(gè) Gem,使用它可以不必去做任何配置(大多數(shù)情況是這樣的),還記得 Rails 的兩個(gè)哲學(xué)理念之一么:約定優(yōu)于配置。(另一個(gè)是 不要重復(fù)自己,這是 Dave Thomas 在《程序員修煉之道》一書里提出的。)

那么,我們講兩個(gè) Active Record 中的約定:

4.1.2.1 命名約定

  • 數(shù)據(jù)表名:復(fù)數(shù),下劃線分隔單詞(例如 book_clubs)
  • 模型類名:單數(shù),每個(gè)單詞的首字母大寫(例如 BookClub)

比如:

模型(Class) 數(shù)據(jù)表(Schema)
Post posts
LineItem line_items
Deer deers
Mouse mice
Person people

單詞在單復(fù)數(shù)轉(zhuǎn)換時(shí),是按照英文語法約定的。

4.1.2.2 Schema 約定

注:數(shù)據(jù)庫中的 Schema,指數(shù)據(jù)庫對(duì)象集合,可以被用戶直接使用。Schema 包含數(shù)據(jù)的邏輯結(jié)構(gòu),用戶可以通過命名調(diào)用數(shù)據(jù)庫對(duì)象,并且安全的管理數(shù)據(jù)庫。

  • 外鍵 - 使用 singularized_table_name_id 形式命名,例如 item_id,order_id。創(chuàng)建模型關(guān)聯(lián)后,Active Record 會(huì)查找這個(gè)字段;
  • 主鍵 - 默認(rèn)情況下,Active Record 使用整數(shù)字段 id 作為表的主鍵。使用 Active Record 遷移創(chuàng)建數(shù)據(jù)表時(shí),會(huì)自動(dòng)創(chuàng)建這個(gè)字段;

在數(shù)據(jù)庫字段命名的時(shí)候,有幾個(gè)特殊意義的名字,盡量回避:

  • created_at - 創(chuàng)建記錄時(shí),自動(dòng)設(shè)為當(dāng)前的時(shí)間戳
  • updated_at - 更新記錄時(shí),自動(dòng)設(shè)為當(dāng)前的時(shí)間戳
  • lock_version - 在模型中添加樂觀鎖定功能
  • type - 讓模型使用單表繼承,給字段命名的時(shí)候,盡量避開這個(gè)詞
  • (association_name)_type - 多態(tài)關(guān)聯(lián)的類型
  • (table_name)_count - 保存關(guān)聯(lián)對(duì)象的數(shù)量。例如,posts 表中的 comments_count 字段,Rails 會(huì)自動(dòng)更新該文章的評(píng)論數(shù)

4.1.3 數(shù)據(jù)庫遷移(Migration)

在我們使用 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

4.1.4 CRUD

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ù)庫了,是不是很簡單。

4.1.5 創(chuàng)建記錄

我們使用 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í)例的介紹。

4.1.6 查詢記錄

4.1.6.1 Find 查詢

數(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])

4.1.6.2 Where 查詢

集合的查找,最常用的方法是 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 一章,這是 中文版。

4.1.7 更新記錄(Update)

和創(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

4.1.8 刪除記錄(Destroy)

在我們接觸計(jì)算機(jī)英語里,表示刪除的英文有很多,這里我們用到的是 destroy, delete。

4.1.8.1 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 methoddelete' 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

4.1.8.2 Destroy 刪除

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! 將它真正的刪除掉。

上一篇:6.6 常用 Gem下一篇:寫在前面