Active Record

Active Record 是由 Rails 提供的對象關(guān)系映射(ORM)層,也是實現(xiàn) Rails 應用中 model 的一部分。

在本章中,我們將構(gòu)建在 Depot 應用中的數(shù)據(jù)及字符的映射。接著,我們將了解通過 Active Record 管理表關(guān)系的知識,也會學習創(chuàng)建、讀取、更新和刪除操作的過程(也就是通常說的 CRUD 方法)。最后,我們還將深入學習 Active Record 對象的生命周期(包括回調(diào)和事務)。

定義數(shù)據(jù)

在 Depot 中,除了 Order 之外我們還定義了其他 model。訂單 model 包含一些屬性,比如字符串類型的 email 地址。在我們定義的屬性之外,Rails 還提供了 id 屬性,它包含了數(shù)據(jù)的主鍵。當然,除此之外 Rails 還提供了幾個額外屬性,主要用于記錄數(shù)據(jù)被創(chuàng)建和最后更新的時間。Rails 也支持 model 間的關(guān)聯(lián)關(guān)系,比如訂單與購買商品間的關(guān)系。

除了上述提到的特性之外,Rails 還為 model 提供了許多功能,接下來讓我們逐個學習。

規(guī)劃表和字段

每個 ActiveRecord::Base 的子類都表示一個獨立的數(shù)據(jù)庫表,比如 Order 類。Active Record 默認表名為相應類名的復數(shù)形式,如果類名由多個單詞組成,默認情況下表名由被下劃線分隔的多個單詞組成。

Classname Table Name
Order orders
TaxAgency tax_agencies
Batch batches
Diagnosis diagnoses
LineItem line_items
Person people
Datum data
Quantity quantities

這些規(guī)則都表達了 Rails 的哲學,類名應該為單數(shù)形式,而表名應該為相應的復數(shù)形式。

盡管 Rails 可以處理許多不規(guī)則復數(shù),但偶爾還是有意外情況。如果你遭遇了類似情況,可以通過在相應映射文件中添加復數(shù)與單數(shù)單詞的對照解決。

# config/initializers/inflections.rb

# Be sure to restart your server when you modify this file.

# Add new inflection rules using the following format. Inflections
# are locale specific, and you may define rules for as many different
# locales as you wish. All of these examples are active by default:
# ActiveSupport::Inflector.inflections(:en) do |inflect|
#   inflect.plural /^(ox)$/i, '\1en'
#   inflect.singular /^(ox)en/i, '\1'
#   inflect.irregular 'person', 'people'
#   inflect.uncountable %w( fish sheep )
# end

# These inflection rules are supported but not enabled by default:
# ActiveSupport::Inflector.inflections(:en) do |inflect|
#   inflect.acronym 'RESTful'
# end

ActiveSupport::Inflector.inflections do |inflect|
  inflect.irregular 'tax', 'taxes'
end

如果有遺留系統(tǒng)的表需要處理,或者你并不喜歡 Rails 提供的默認方式,可以通過向指定類提供對應的表名自己控制。

class Sheep < ActiveRecord::Base
  self.table_name = "sheep"
end

David 提問:表字段又在哪里呢?

基于數(shù)據(jù)庫管理員(DBA)與開發(fā)人員是兩個分離角色的思想,所以對代碼和 schema 之間也進行了嚴格的隔離。Active Record 模糊了這種區(qū)別,再沒有其他地方比 model 缺少屬性定義更加明顯了。

不過不需要擔心,實踐表明無論是查看數(shù)據(jù)庫或者分離的 XML 映射文件,亦或者 model 中的屬性其實并沒有分別。這種合成視圖與 MVC 模式中的分隔十分相似,只是發(fā)生在不同的尺度上而已。

一旦適應了將 schema 視作 model 定義的一部分時,你將理解遵循 DRY 所帶來的收益。當你需要向 model 添加一個屬性時,只要創(chuàng)建新 migration 然后重新加載應用即可。

將構(gòu)建步驟從 schema 剝離是使余下代碼更加敏捷的演進。這樣可以使創(chuàng)建一個小型 schema,按需要繼承及修改都更加容易。

Active Record 類的實例表示數(shù)據(jù)庫表中的數(shù)據(jù),而對象中的屬性與表的列相對應。你可能已經(jīng)注意到,Order 類并沒有提及 orders 表中的任何列,這都是由于 Active Record 決定在運行時處理相關(guān)內(nèi)容。Active Record 在將 schema 注入數(shù)據(jù)庫時才反射配置表示相應表的類。

在 Depot 中,orders 表由下列 migration 定義:

# db/migrate/20201218093951_create_orders.rb

class CreateOrders < ActiveRecord::Migration
  def change
    create_table :orders do |t|
      t.string :name
      t.text :address
      t.string :email
      t.string :pay_type

      t.timestamps null: false
    end
  end
end

讓我們通過 rails console 命令操作 model。首先,當然是獲取列名列表。

rails console
Order.column_names

接著,再獲取 pay_type 列的詳情。

Order.columns_hash["pay_type"]

注意 Active Record 已經(jīng)收集了相當多的 pay_type 列信息。通過信息我們可以了解到,該字段是最多 255 字符的字符串,并沒有默認值,而且它并不是主鍵,有可能存在空值。當首次使用 Order 類時 Rails 會通過底層的數(shù)據(jù)庫獲得相應信息。

一般情況下,Active Record 實例的屬性與數(shù)據(jù)庫表的數(shù)據(jù)相吻合。例如,orders 表可能存在下列數(shù)據(jù):

sqlite3 -line db/development.sqlite3 "select * from orders limit 1"

如果獲取此數(shù)據(jù)轉(zhuǎn)換為 Active Record 對象,此對象將擁有七個屬性。id 屬性將是數(shù)字 1,name 屬性是字符串「Dave Thomas」等等。

我們通過訪問方法獲取相應屬性的值。當 Rails 通過 schema 反射生成 model 對象時,也會自動為它創(chuàng)建屬性訪問和更新方法。

o = Order.find(1)
puts o.name            #=> "Dave Thomas"
o.name = "Fred Smith"  # set the name

只是設(shè)置屬性值并不會最影響數(shù)據(jù)庫數(shù)據(jù),我們還需要將對象的變動進行存儲才會產(chǎn)生最影響。

屬性讀取方法返回的值是由 Active Record 轉(zhuǎn)換為適當?shù)?Ruby 類型(比如,如果數(shù)據(jù)庫字段為時間戳,對應將返回 Time 對象)。如果希望獲取原生的屬性值,可以在名稱后添加 _before_type_cast 獲取,如下代碼展示:

product.price_before_type_cast      #=> 34.95, a float
product.updated_at_before_type_cast #=> "2013-02-13 10:13:14"

如果是 model 的內(nèi)部代碼,可以通過 read_attribute()write_attribute() 私有方法,此方法需要將屬性名作為參數(shù)。

在下表中,我們將看到 SQL 類型與其 Ruby 類型的對應關(guān)系。Decimal 和 Boolean 字段有一些棘手。

SQL Type Ruby Class
int, integer Fixnum
float, double Float
decimal, numeric BigDecimal
char, varchar, string String
interval, date Date
datetime, time Time
clob, blob, text String
boolean See text

按道理 Decimal 應該與 Fixnum 對象對應,然而它卻對應了 BigDecimal 對象,這是為了確保小數(shù)位不會丟失。

在 Boolean 的示例中,為了方便表示,方法尾部一般都跟隨問號。

user = User.find_by(name: "Dave")
if user.superuser?
  grant_privileges
end

除了這些由我們定義的字段外,Rails 也自動提供了一些其他的屬性,其中有些屬性有著特殊的意義。

由 Active Record 提供的其他字段

有一些列名對 Active Record 格外重要,這里有一些總結(jié):

create_at, create_on, updated_at, updated_on
當數(shù)據(jù)被創(chuàng)建或更新時這些列會自動記錄創(chuàng)建時間或最后更新時間。不過需要確認底層數(shù)據(jù)庫字段支持 date、datetime 或 string 類型。Rails 習慣用 _on 后綴的字段表示日期,而 _at 后綴的字段表示時間。

id
默認情況下這是表主鍵字段名。

xxx_id
默認情況下此字段表示外鍵名稱,xxx 為外鍵關(guān)聯(lián)的表名。

xxx_count
此字段為子表 xxx 維護了一個計數(shù)器緩存。

使用其他插件時也可能會產(chǎn)生額外的字段,比如 acts_as_list。

主鍵與外鍵都是數(shù)據(jù)庫操作中的重要角色,值得為其用新章節(jié)進行講解。

定位及關(guān)聯(lián)數(shù)據(jù)記錄

在 Depot 中,LineItems 與其他三個 model 都有直接的關(guān)聯(lián),分別是 Cart、Order 和 Product,除此之外 model 之間也可以建立間接關(guān)系,Orders 與 Products 就是通過 LineItems 形成的間接聯(lián)系。

這些關(guān)系都可以通過 ID 形成關(guān)聯(lián)。

區(qū)分數(shù)據(jù)記錄

Active Record 類與數(shù)據(jù)庫中的表相對應,類的實例與表中的具體某條數(shù)據(jù)對應。比如,調(diào)用 Order.find(1) 將返回 Order 類的實例,它將包含主鍵為 1 的數(shù)據(jù)。

如果你為 Rails 應用創(chuàng)建了一個新 schema,根據(jù)正常流程 Rails 將向所有的表都添加 id 主鍵。但如果你是處理已經(jīng)存在的 schema 時,Active Record 提供了簡單的方法對表的主鍵名稱進行覆蓋。

例如,有一個遺留系統(tǒng)中 books 表是使用 ISBN 作為主鍵。如下就可以在 Active Record 的 model 中指定主鍵:

class LegacyBook < ActiveRecord::Base
  self.primary_key = "isbn"
end

通常 Active Record 會更加關(guān)注為我們添加至數(shù)據(jù)庫中的數(shù)據(jù)添加主鍵值,一般是增長的整型數(shù)據(jù)(也可能是一些序列)。不過,如果我們覆蓋了主鍵名,也就需要在存儲數(shù)據(jù)前為其主鍵設(shè)置唯一值?;蛟S你會感到驚訝,我們依然通過設(shè)置 id 屬性值來做到這一點。就像 Active Record 關(guān)注的一樣,總是用名為 id 的屬性作為主鍵,而 primary_key= 會聲明用于表中的列名。下面的代碼中,即使數(shù)據(jù)庫的主鍵是 isbn,我們依然使用 id 屬性:

book = LegacyBook.new
book.id = "0-12345-6789"
book.title = "My Great American Novel"
book.save
#...
book = LegacyBook.find("0-12345-6789")
puts book.title   #=> "My Great American Novel"
p book.attributes #=> {"isbn"  => "0-12345-6789", "title" => "My Great American Novel"}

生活總是讓人困惑,model 對象的列只展示了 isbn 和 title,而 id 卻沒有出現(xiàn)。只有在設(shè)置主鍵值時才使用 id,其他時候都得使用真正的主鍵名。

model 對象也重定義了 Ruby 的 id()hash() 方法,用于展示 model 主鍵。這也就意味著擁有有效 ID 的 model 對象將被用于 hash 主鍵。也就是說沒有存儲的 model 對象無法使用 hash 鍵值(因為未存儲的對象不會擁有有效 ID)。

還有最后一點需要注意,如果兩個 model 對象為相同類的實例,并且擁有一樣的主鍵,Rails 將認為它們相等(也就是 == 成立),也就是說尚未存儲的 model 對象即使屬性不相同,但依然相等。如果你發(fā)現(xiàn)自己正在比較未存儲的 model 對象時(這并不是一種常見的操作),應該要重寫相應的 == 方法。

如同我們親眼所見,ID 也在表達關(guān)系時扮演著重要角色。

指定 Model 之間的關(guān)系

Active Record 支持三種表之間的關(guān)聯(lián)關(guān)系,分別是一對一、一對多和多對多。你可能在 model 中添加相應的聲明表述相應關(guān)系,分別是 has_one、has_manybelongs_tohas_and_belongs_to_many。

一對一關(guān)系

一對一關(guān)系(更多情況下被叫做一對零或?qū)σ魂P(guān)系)是通過表中數(shù)據(jù)的存儲外鍵,通過外鍵關(guān)聯(lián)其他表的最多一條數(shù)據(jù)的情況。這種關(guān)系在訂單和發(fā)票間會發(fā)生,通常情況下一個訂單最多有一張發(fā)票。

one to one

如圖所示,在 Rails 應用中我們在 Order model 中添加了 has_one 聲明,在 Invoice model 中添加了 belongs_to 聲明。

圖示中還展示了一個重要的規(guī)則,一般情況下?lián)碛?belongs_to 聲明的 model 對應表為擁有外鍵的一方。

一對多關(guān)系

通過一對多關(guān)系可以處理集合對象。比如,一個訂單可能含有多個購買商品。在數(shù)據(jù)庫中,所有屬于訂單的購買商品數(shù)據(jù)都含有指向訂單的外鍵。

one to many

在 Active Record 中,父對象(也就是邏輯上包含集合對象的對象)通過 has_many 聲明與子表的關(guān)系,而子表通過 belongs_to 指向父表。在上述例子中,LineItem 類 belongs_to :order,而 orders 表 has_many :line_item。

需要再次注意,因為購買商品數(shù)據(jù)包含外鍵,所以它擁有 belongs_to 聲明。

多對多關(guān)系

最后我們需要對商品進行分類。一件商品可以屬于多個分類,一個分類也可以包含多個商品,這就是一個多對多的例子。每一方都可能包含另一方的數(shù)據(jù)集合。

many to many

在 Rails 中我們可以通過向相關(guān)的 model 都添加 has_and_belongs_to_many 聲明表述這種關(guān)系。

多對多是一種對稱關(guān)系,相互連接的表都通過 habtm 聲明了它們之間的關(guān)系。

Rails 是通過中間表實現(xiàn)多對多關(guān)系的,中間表包含了連接兩個目標表的外鍵。Active Record 默認將目標表名字按字母排序連接形成中間表名稱。在上述例子中,我們將 categories 表與 products 表組合,所以 Active Record 將中間表取名為 categories_products。

我們也可以直接定義中間表名。在 Depot 中,Products 與 Carts 或 Orders 表結(jié)合形成 LineItems。自定義中間表的方式給了我們向其添加屬性的能力,比如此例子中 LineItems 的 quantity 屬性。

現(xiàn)在我們已經(jīng)了解了關(guān)系數(shù)據(jù)定義的知識,你自然會希望能夠訪問數(shù)據(jù)庫的數(shù)據(jù),所以接下來我們將介紹相關(guān)知識。

創(chuàng)建、讀取、更新及刪除(CRUD)

無論是 SQLite 還是 MySQL 都是通過 Structure Query Language(SQL)訪問數(shù)據(jù)庫。Rails 會為你處理 SQL,但你也可以決定由自己處理。就如你所理解的一樣,你完全可以編寫 SQL 語句交由數(shù)據(jù)庫執(zhí)行。

如果你對 SQL 已經(jīng)熟悉,閱讀本節(jié)時你只需要注意 Rails 提供了哪些方式替換 SQL 語法,比如 selectfrom,where,group by 等等。如果你對 SQL 尚不熟悉,Rails 的優(yōu)勢可以讓你暫緩對相關(guān)知識的了解,當你真正需要通過 SQL 訪問數(shù)據(jù)庫時再學習。

在本節(jié)中,我們將以 Depot 中的 Order model 作為例子講解,然后通過 Active Record 方法完成創(chuàng)建、讀取、更新和刪除這四種基本的數(shù)據(jù)庫操作。

創(chuàng)建新數(shù)據(jù)

Rails 通過類表現(xiàn)數(shù)據(jù)庫的表,而使用對象表示一條數(shù)據(jù),按照這種類比方式,想在表中創(chuàng)建一條數(shù)據(jù)時就需要按相應的類創(chuàng)建對象。只要調(diào)用 Order.new() 方法即可創(chuàng)建表示 orders 表的新數(shù)據(jù),然后再將數(shù)據(jù)填充至對象的屬性中(對應的就是數(shù)據(jù)庫中的列)。最后再通過對象的 save() 方法將訂單存儲至數(shù)據(jù)庫中,如果沒有調(diào)用 save() 方法,訂單數(shù)據(jù)只會存在于本地內(nèi)存中。

an_order = Order.new
an_order.name = "Dave Thomas"
an_order.email = "dave@example.com"
an_order.address = "123 Main St"
an_order.pay_type = "check"
an_order.save

Active Record 構(gòu)造器可以接收 block。如果傳遞 block 作為參數(shù),block 中可以填寫創(chuàng)建訂單時的數(shù)據(jù)。這種方式在你想創(chuàng)建和存儲訂單而不想創(chuàng)建相應的局部變量時十分有用。

Order.new do |o|
  o.name = "Dave Thomas"
  #...
  o.save
end

最后 Active Record 構(gòu)造器也接收 hash 屬性值作為參數(shù)。每一對 hash 數(shù)據(jù)都表示將數(shù)據(jù)填充至 key 表示的屬性,在將 HTML 表單存儲至數(shù)據(jù)庫時這種方式格外有效。

an_order = Order.new(
  name: "Dave Thomas",
  email: "dave@example.com",
  address: "123 Main St",
  pay_type: "check")
an_order.save

需要注意的是,在上述所有例子中我們都沒有為新建數(shù)據(jù)設(shè)置 id 屬性的值。因為 Active Record 默認情況下會為主鍵設(shè)置唯一整型數(shù)值。隨后我們可以通過屬性值查找數(shù)據(jù)。

an_order = Order.new
an_order.name = "Dave Thomas"
#...
an_order.save
puts "The ID of this order is #{an_order.id}"

new() 構(gòu)造方法在內(nèi)存中創(chuàng)建了一個新的 Order 對象,所以必須記住要將它存儲至數(shù)據(jù)庫。不過 Active Record 提供了一個更加方便的方法 create(),它會將 model 對象實例化并存儲至數(shù)據(jù)庫中。

an_order = Order.create(
  name: "Dave Thomas",
  email: "dave@example.com",
  address: "123 Main St",
  pay_type: "check")

你也可以向 create() 方法傳遞成數(shù)組的 hash 值,此方法將會在數(shù)據(jù)庫中創(chuàng)建多條數(shù)據(jù),并返回相應的 model 對象數(shù)組。

orders = Order.create([
  { name: "Dave Thomas",
    email: "dave@example.com",
    address: "123 Main St",
    pay_type: "check"
  },
  { name: "Andy Hunt",
    email: "andy@example.com",
    address: "456 Gentle Drive",
    pay_type: "po"
  }
])

new()create() 方法接收 hash 數(shù)據(jù)的原因都是方便可以直接從表單數(shù)據(jù)構(gòu)造 model 對象。

@order = Order.new(order_params)

如果你覺得上面這行代碼看起來十分熟悉,那是因為你之前就見過它。在 Depot 的 orders_controller.rb 中它就曾出現(xiàn)過。

讀取已存在的數(shù)據(jù)

如果想從數(shù)據(jù)庫中讀取關(guān)注的數(shù)據(jù)就需要向 Active Record 提供相應的條件,接著它就會返回符合相應條件的數(shù)據(jù)對象。

查找數(shù)據(jù)最簡單的方法就是通過主鍵。每個 model 類都支持 find() 方法,它接收一個或多個主鍵參數(shù)。如果提供一個主鍵參數(shù),方法會返回相應的一條數(shù)據(jù)對象(或者拋出 ActiveRecord::RecordNotFound 異常)。如果提供多個主鍵參數(shù),find() 方法將返回符合條件的一組對象。要注意,在本示例中 RecordNotFound 異常會在提供的 ID 不存在時被拋出(所以如果方法沒有拋出異常表示返回的數(shù)組長度與傳遞的主鍵數(shù)量一致)。

an_order = Order.find(27)  # find the order with id == 27
# Get a list of product ids from a form, then
# find the associated Products
product_list = Product.find(params[:product_ids])

通常情況下除了基于主鍵獲取數(shù)據(jù)的方式之外,你還會有通過其他條件查詢的需求,而 Active Record 提供了其他方法供你通過更加復雜的條件查詢。

SQL 和 Active Record

舉例說明一下 Active Record 如何轉(zhuǎn)換 SQL,當我們向 where() 方法傳遞字符串作為條件時就相當于調(diào)用了 SQL 的 where 語法。比如,查找 Dave 的所有支付類型為 po 的訂單,我們可以使用下列方式:

pos = Order.where("name = 'Dave' and pay_type = 'po'")

David 提問:拋出或不拋出異常?

當你使用一個通過主鍵驅(qū)動的查詢器時,你便可以查找到指定的數(shù)據(jù),也就是說你期望它是存在的。Person.find(5) 就是關(guān)于 people 表的這種表達,也就是我們需要 ID 為 5 的數(shù)據(jù)。如果方法調(diào)用失敗,也許 ID 為 5 的記錄被刪除,這就是一種異常情況。這種情況會交由異常處理,所以 Rails 將拋出 RecordNotFound 異常。

另一方面,查詢器通過條件查找匹配的數(shù)據(jù),所以 Person.where(name: 'Dave').first 等同于告訴數(shù)據(jù)庫(可以將其看作黑盒)「給我第一個叫做 Dave 的人的數(shù)據(jù)」。這里展示的是另一種檢索方式,我們不需要預先確定自己會獲得一個結(jié)果,當前的結(jié)果集完全可能是空的。因此,當用于單個數(shù)據(jù)的查詢器返回 nil,用于多條數(shù)據(jù)的查詢器返回空數(shù)組都是十分正常的,它們并不會進行異常形式的回應。

返回結(jié)果將是包含所有條件的 ActiveRecord::Relation 對象數(shù)據(jù),每一個對象都包含了一個 Order 對象。

如果查詢條件已經(jīng)被預定義的話更好,不過在客戶名稱被明確設(shè)置的情況下我們要如何處理它(或許數(shù)據(jù)來自 web 表單)?一種方式是將變量值替換至條件字符串中。

# get the name from the form
name = params[:name]
# DON't DO THIS!!!
pos = Order.where("name= '#{name}' and pay_type = 'po'")

就如同注釋建議的一樣,這種方式并不好。為什么?因為它為 SQL 侵入攻擊創(chuàng)造了條件,在 265 頁生成的 Rails Guides 中有更加詳細的描述。同理,我們可以認為將外部數(shù)據(jù)直接替換 SQL 語句條件相當于向外界直接暴露整個數(shù)據(jù)庫。

相比之下,較安全的方式是通過動態(tài) SQL 的方式讓 Active Record 處理它。這樣做是讓 Active Record 創(chuàng)建適當?shù)?SQL,對 SQL 侵入攻擊免疫。讓我們看看要如何處理。

如果我們向 where() 傳遞多個參數(shù),Rails 會將第一個參數(shù)作為生成 SQL 的模板。在 SQL 中,我們可以嵌入占位符,它將在運行時被數(shù)據(jù)余下的數(shù)據(jù)替換。

指定占位符的一種方式是在 SQL 中插入問號。第一個問號由數(shù)組的第二個元素替換,下一個問號則由第三個元素替換等等。比如,我們按如下方式重寫上述查詢語句:

name = params[:name]
pos = Order.where(["name= ? and pay_type= 'po'", name])

占位符也可以使用命名式的,通過將占位符替換為 :name 這種形式實現(xiàn),不過這種方式需要提供相應的 hash 鍵值對實現(xiàn)。

name = params[:name]
pay_type = params[:pay_type]
pos = Order.where("name = :name and pay_type = :pay_type",
                  pay_type: pay_type, name: name)

我們還可以更進一步。因為 params 本身就是一種高效的 hash,所以我們也可以將它完全作為參數(shù)傳入。如果有一個表單是用于輸入查詢條件的,便可以直接使用來自表單的 hash 值。

pos = Order.where("name = :name and pay_type = :pay_type", params[:order])

當然還可以更加簡化。如果我們只是將 hash 作為參數(shù),Rails 會將 hash 的鍵作為列名,而 hash 值作為查詢匹配的值。所以將之前的代碼簡化之后如下:

pos = Order.where(params[:order])

使用上一種條件查詢方式需要小心,因為它將所有的鍵值對都作為匹配條件傳入了。而另一種方法可以使查詢參數(shù)更加明確。

pos = Order.where(name: params[:name],
                  pay_type: params[:pay_type])

不論你使用的是哪種占位符,Active Record 都可以處理妥善,并將查詢條件值替換至 SQL 中。使用這種方式的動態(tài) SQL,Active Record 將使你免于 SQL 侵入攻擊。

使用 Like 語法

我們常常會按下面代碼一樣在查詢條件中使用 like 語法。

# Doesn't work
User.where("name like '?%'", params[:name])

Rails 并不會將此 SQL 轉(zhuǎn)換為條件,也不會懂得將姓名值替換至字符串中。最終,它可能會在 name 參數(shù)值的周圍添加引號。正確的做法應該是構(gòu)造完整的 like 語法,并將參數(shù)傳入條件中。

# Works
User.where("name like ?", params[:name]+"%")

當然,如果這樣處理便需要考慮百分符號,它們將出現(xiàn)在 name 參數(shù)值的周圍,被視作通配符處理。

構(gòu)建返回記錄

如今,我們已經(jīng)知道了如何指定查詢條件,接著讓我們轉(zhuǎn)換視線,來看看 ActiveRecord::Relation 支持的方法,首先從 first()all() 開始。

如同你猜想的一樣,first() 將返回 relation 中的第一條數(shù)據(jù),如果 relation 為空將返回 nil。to_a() 也是類似的,它將把所有數(shù)據(jù)轉(zhuǎn)換為數(shù)組返回,ActiveRecord::Relation 也支持許多 Array 對象的方法,比如 each()map(),在調(diào)用這兩個方法時一般會在后臺先調(diào)用 all()。

要清晰地認識到這并不等同于查詢。這些方法只是允許我們通過一些方式改變查詢的結(jié)果,也就是在調(diào)用這些方法前調(diào)用其他方法?,F(xiàn)在讓我們看看這些方法。

order

SQL 并不會指定數(shù)據(jù)以某種特定排序返回,除非我們在查詢時明確添加了 order by 語法。order() 方法允許我們在其中添加用于 order by 的條件。比如,下面的查詢會返回所有 Dave 的訂單,然后根據(jù)支付類型和購買日期排序(購買日期是使用降序排序)。

orders = Order.where(name: 'Dave').
  order("pay_type, shipped_at DESC")

limit

通過 limit() 方法我們可以限制返回的數(shù)據(jù)數(shù)量。一般情況下,當使用限制方法時也會使用排序方法,以保證查詢結(jié)果的一致性。比如,下列將返回前十個匹配的訂單:

orders = Order.where(name: 'Dave').
  order("pay_type, shipped_at DESC").
  limit(10)

offset

offset() 方法與 limit() 方法是緊密相關(guān)的,它可以指定返回數(shù)據(jù)從第一行開始的偏移量。

# The view wants to display orders grouped into pages,
# where each page shows page_size orders at a time.
# This method returns the orders on page page_num (starting
# at zero)
def Order.find_on_page(page_num, page_size)
  order(:id).limit(page_size).offset(page_num * page_size)
end

通過 offset 和 limit 結(jié)合可以在結(jié)果中查詢 n 行數(shù)據(jù)。

select

默認情況下 ActiveRecord::Relation 將獲取數(shù)據(jù)庫表中所有列的數(shù)據(jù),相當于 select * from ...。通過 select() 方法可以用字符串替換 select 語法中的 *

此方法讓我們可以獲取表中數(shù)據(jù)的子集。比如,podcasts 表包含了 title、speaker 和 date 信息,也含有占大量存儲的 BLOB 類型的 MP3 數(shù)據(jù)。如果你只是想創(chuàng)建聊天列表,將每條數(shù)據(jù)的聲音數(shù)據(jù)加載出來將降低效率,而 select() 能讓我們選擇自己需要加載的數(shù)據(jù)列。

list = Talk.select("title, speaker, recorded_on")

joins

joins() 方法通過指定其他表而讓基加入默認表中。插入 SQL 的參數(shù)處于 model 表名之后,第一個參數(shù)的指定條件之前,而且 join 語法是數(shù)據(jù)庫特有的。下列代碼將返回名為「Programming Ruby」的購買商品列表。

LineItem.select('li.quantity').
  where("pr.title = 'Programming Ruby 1.9'").
  joins("as li inner join products as pr on li.product_id = pr.id")

readonly

readonly() 方法將使 ActiveRecord::Resource 返回的 Active Record 對象無法被存儲回數(shù)據(jù)庫中。

如果我們是通過 joins()select() 方法獲得的結(jié)果,這些結(jié)果將自動被標記為只讀。

group

group() 方法會向 SQL 添加 group by 語法。

summary = LineItem.select("sku, sum(amount) as amount").
                  group("sku")

lock

lock() 方法可以接收一個字符串參數(shù)。如果我們向其傳遞字符串,它將形成數(shù)據(jù)庫語法中的一個 SQL 片段,用于指定某個類型的鎖。比如,在 MySQL 中,共享鎖只向我們提供一行中最后一個版本的數(shù)據(jù),用于防止在我們獲取數(shù)據(jù)時有人進行修改。我們將編寫一段代碼在賬戶余額充足的情況下進行取款,如下所示:

Account.transaction do
  ac = Account.where(id: id).lock("LOCK IN SHARE MODE").first
  ac.balance -= amount if ac.balance > amount
  ac.save
end

如果我們沒有向 lock() 方法傳遞字符串參數(shù),而是傳遞 true,數(shù)據(jù)庫默認會使用排他鎖(通常情況下用于更新操作)。我們通常使用事務消除這種關(guān)于鎖的需求。

數(shù)據(jù)庫還可以做許多基礎(chǔ)查詢及可依賴的數(shù)據(jù)檢索,同時也可以做一些數(shù)據(jù)簡化分析。Rails 也提供了用于此功能的方法。

字段統(tǒng)計及計算

Rails 也能夠?qū)ψ侄芜M行統(tǒng)計計算。比如,對于訂單表我們可以進行下列計算:

average = Order.average(:amount)
max = Order.maximum(:amount)
min = Order.minimum(:amount)
total = Order.sum(:amount)
number = Order.count

盡管這些都是數(shù)據(jù)庫聚合函數(shù),但它們都在獨立于數(shù)據(jù)庫的環(huán)境中運行。

這些方法也可以與之前講解的知識點結(jié)合運用。

Order.where("amount > 20").minimum(:amount)

上述函數(shù)對數(shù)據(jù)進行了聚合處理,一般情況下,聚合函數(shù)會返回一個結(jié)果。例如,生成符合符合某些條件的訂單的最小數(shù)值。不過,如果你使用了 group 方法就將得到一組結(jié)果,每個結(jié)果都是按分組條件聚合的數(shù)值。比如,下面計算計算每種狀態(tài)的銷售金額:

result = Order.group(:state).maximum(:amount)
puts result #=> {"TX"=>12345, "NC"=>3456, ...}

在上述代碼返回的 hash 結(jié)果中,你可以通過分組元素定位相應數(shù)值(在此例中是 "TX","NC",....)。你也可以通過 each() 方法遍歷每條數(shù)據(jù),每條數(shù)據(jù)的值都是聚合函數(shù)的結(jié)果。

進行分組操作時 order 方法和 limit 方法也可以結(jié)合使用。

result = Order.group(:state)
               order("max(amount) desc")
               limit(3)

這段代碼并不是獨立于數(shù)據(jù)庫的,為了對聚合列進行排序必須在聚合函數(shù)中使用 SQLite 語法(本例中是使用 max)。

Scopes

由于方法的鏈式調(diào)用可能導致方法鏈過長,所以鏈式調(diào)用的重用成為一個值得關(guān)注的問題,而這種情況 Rails 也考慮到了。一個 Active Record scope 可以與一個 Proc 關(guān)聯(lián),所以有如下討論:

class Order < ActiveRecord::Base
  scope :last_n_days, lambda { |days| where('updated < ?', days) }
end

比如這個 scope 就可以輕松用于查找最后一周的訂單。

orders = Order.last_n_days(7)

更簡化的 scope 還可以省略參數(shù)。

class Order < ActiveRecord::Base
  scope :checks, -> { where(pay_type: :check) }
end

scope 當然可以結(jié)合使用,查找最后一周通過 check 方式支付的訂單就可以如下編寫:

orders = Order.checks.last_n_days(7)

這種方式可以提高代碼的可讀性,書寫也方便,而且也可以讓代碼更加高效。例如前面的示例中就可以將其作為一個單獨的 SQL 查詢進行處理。

ActiveRecord::Relation 就相當于一個匿名 scope。

in_house = Order.where('email LIKE "%@pragprog.com"')

按上述理論 relation 也可以與 scope 結(jié)合使用。

in_house.checks.last_n_days(7)

scope 并沒有限制任何 where 條件,我們依然可以對 limit、orderjoin 進行調(diào)用。不過要注意 Rails 并不了解如何處理多個 order 或 limit 語句,所以要確保在每個調(diào)用鏈中只使用一次。

在最近的示例中,我們對相應的方法都做了充足的描述。但 Rails 并不滿足于這些示例的功能,有些情況需要親自編制查詢語句,Rails 也提供了相關(guān)的 API。

自己編寫 SQL

我們看到的每個方法都提供了完全使用 SQL 語句的相關(guān) API。find_by_sql() 可以讓我們的應用完全進行控制,此方法只接收一個含有 select SQL 語法的參數(shù)(或者一組包含 SQL 和占位符數(shù)據(jù)的參數(shù),就像 find() 一樣),并且返回一組 model 對象(也可能是空數(shù)組)。model 結(jié)果中的屬性由查詢結(jié)果的相應字段值填充。通常我們會使用 select * 查詢所有列,但這里不需要。

orders = LineItem.find_by_sql("select line_items.* from line_items, orders where order_id = orders.id and orders.name = 'Dave Thomas'")

只有查詢返回的屬性對于 model 對象才是可用的。通過 attributes()、attribute_names()attribute_present?() 方法可以對 model 對象中的屬性是否是否可用進行判斷。第一個方法返回的是一對屬性名稱和數(shù)值的 hash 鍵值對,第二個方法返回的是一組名稱,如果在 model 對象中指定的屬性名稱可用的話最后一個方法將返回 true。

orders = Order.find_by_sql("select name, pay_type from orders")
first = orders[0]
p first.attributes
p first.attribute_names
p first.attribute_present?("address")

上述代碼將輸出下列結(jié)果:

{"name"=>"Dave Thomas", "pay_type"=>"check"}
["name", "pay_type"]
false

find_by_sql() 也可以根據(jù)原生字段創(chuàng)建 model 對象。如果利用 as xxx SQL 語法向原生字段提供一個結(jié)果集名稱,那這個名字也將是屬性名稱。

items = LineItem.find_by_sql("select *," +
                             " products.price as unit_price," +
                             " quantity*products.price as total_price, "+
                             " products.title as title " +
                             "from line_items, products " +
                             "where line_items.product_id = products.id")
li = items[0]
puts "#{li.title}: #{li.quantity}x#{li.unit_price} => #{li.total_price}"

我們也可以通過數(shù)組的方式向 find_by_sql() 傳遞參數(shù),不過第一個參數(shù)是帶有占位符的字符串,數(shù)組的其他元素可以可以是 hash 數(shù)據(jù)也可以是數(shù)組集合,不過都是用于替換占位符。

Order.find_by_sql(["select * from orders where amount > ?", params[:amount]])

在舊版 Rails 中,大家通常使用 find_by_sql()。不過時至今日,所有的功能都已經(jīng)附加至 find() 方法,我們不必再使用這些低級別的方法了。

重載數(shù)據(jù)

應用中的數(shù)據(jù)庫存在被多個進程訪問的可能(或者被多個應用訪問),所以有可能獲得的 model 對象并不是最新數(shù)據(jù),很有可能其他進程已經(jīng)向數(shù)據(jù)庫寫入了新數(shù)據(jù)。

David 提問:但難道 SQL 不會污染代碼嗎?

通常開發(fā)人員都會使用面向?qū)ο蟮姆绞接成潢P(guān)系型數(shù)據(jù)庫,其中被討論最多的問題是如何更高度地進行抽象。一些對象關(guān)系映射想完全去除使用 SQL,希望通過面向?qū)ο髮泳徒鉀Q所有的請求。

但 Active Record 并不是這樣想的,它基于 SQL 并不會污染代碼或者 SQL 是不好的這種概念,只是在某些情況下會導致代碼的冗長。所以我們應該關(guān)注的是去除這些導致冗長的語法的需求(親手編寫 insert 的十個參數(shù)將使程序員十分疲憊),但同時也要為復雜的查詢保留相應的表達方式,SQL 就是用于完美解決這些復雜的查詢。

所以當你使用 find_by_sql() 處理性能瓶頸或復雜的查詢時不要內(nèi)疚。為了高效且愉快的編寫代碼請開始使用面向?qū)ο蟮慕涌?,而在你真正需要手?SQL 時再親自學習和了解。

一般情況下這種問題都是通常事務處理(如同在 304 頁描述的一樣)。不過到時需要手動刷新 model 對象,Active Record 此方式進行了簡化,只要調(diào)用 reload() 方法對象的屬性將會從數(shù)據(jù)庫中刷新。

stock = Market.find_by(ticker: "RUBY")
loop do
  puts "Price = #{stock.price}"
  sleep 60
  stock.reload
end

實際生產(chǎn)中 reload() 很少在單元測試的環(huán)境之外使用。

更新已存在的數(shù)據(jù)

在大量關(guān)于查詢方法的討論之后,你肯定很樂意學習并不多的關(guān)于數(shù)據(jù)更新的知識。

如果你擁有 Active Record 對象(可以是 orders 表的一條數(shù)據(jù)),通過 save() 方法便可以將其寫入數(shù)據(jù)庫。如果此對象之前已經(jīng)是從數(shù)據(jù)庫中讀取的,保存操作將更新已經(jīng)存在的數(shù)據(jù),否則保存操作會插入新的數(shù)據(jù)。

如果是將已經(jīng)存在的數(shù)據(jù)更新,Active Record 會通過它的主鍵匹配內(nèi)存中的對象。Active Record 對象中的屬性將決定哪些字段會被更新,在數(shù)據(jù)庫中字段被更新意味著它的值被修改。在下面的例子中,數(shù)據(jù)庫中訂單 123 的所有值將被更新:

order = Order.find(123)
order.name = "Fred"
order.save

但是在下面的例子中,Active Record 只包括了 id、name 和 paytype 屬性,當對象被保存時只有這幾個字段會被修改。(如果你通過 find_by_sql() 保存查詢的數(shù)據(jù)時,需要在 SQL 中包含 id)。

orders = Order.find_by_sql("select id, name, pay_type from orders where id=123")
first = orders[0]
first.name = "Wilma"
first.save

除了 save() 方法外,我們也可以通過 update() 方法修改屬性值并保存。

order = Order.find(321)
order.update(name: "Barney", email: "barney@bedrock.com")

update() 方法常常在 controller 中使用,特別是將來自表單的數(shù)據(jù)向已經(jīng)存在數(shù)據(jù)庫的數(shù)據(jù)整合時。

def save_after_edit
  order = Order.find(params[:id])
  if order.update(order_params)
    redirect_to action: :index
  else
    render action: :edit
  end
end

我們可以將查詢數(shù)據(jù)和將其更新的方法 update()、update_all() 結(jié)合使用。update() 方法接收參數(shù) id 和一組屬性。它將獲取相應的數(shù)據(jù),然后設(shè)置傳遞的屬性值,接著將其存儲至數(shù)據(jù)庫,最后返回 model 對象。

order = Order.update(12, name: "Barney", email: "barney@bedrock.com")

update() 傳遞的參數(shù)也可以是一組 ID 和一組屬性的 hash 數(shù)據(jù),接著將更新數(shù)據(jù)庫中相應的數(shù)據(jù),然后返回一組 model 對象。

update_all() 方法允許我們指定 update SQL 語句中的 setwhere 子句。比如下面這個例子,將標題中包含 Java 的商品價格提高百分之十:

result = Product.update_all("price = 1.1*price", "title like '%Java%'")

update_all() 返回的值依賴于數(shù)據(jù)庫適配器,多數(shù)情況下(但不包括 Oracle)會返回數(shù)據(jù)庫中被修改數(shù)據(jù)的行數(shù)。

save、save!、create 及 create!

其實 savecreate 方法有兩個版本的方法。主要的不同是它們報錯的方式有所區(qū)別。

  • 如果數(shù)據(jù)正常保存 save 方法將返回 true,否則就返回 nil

  • 如果保存成功 save! 方法將返回 true,否則會拋出異常。

  • 無論是否成功保存 create 方法都將返回 Active Record 對象。如果你想確認數(shù)據(jù)是否已經(jīng)被更新就需要驗證對象的錯誤檢測信息。

  • 當保存成功時 creat! 將返回 Active Record 對象,否則將拋出異常。

接著看看其他相關(guān)詳情。

如果 model 對象是有效的且被正常存儲常規(guī)版本的 save() 將返回 true。

if order.save
  # all OK
else
  # validation failed
end

檢查 save() 的調(diào)用結(jié)果是否為我們期望的也很需要。有些原因?qū)е?Active Record 是對此的使用如此寬泛,它通常認為 save() 方法會在 controller 的 action 方法上下文中調(diào)用,而 view 將向終端用戶顯示返回的錯誤。在許多應用中,這樣的場景都十分常見。

不過,如果我們是希望在能確認處理所有異常的情況下保存 model 對象就應該使用 save()。如果對象不能正常存儲就會拋出 RecordInvalid 異常。

begin
  order.save!
rescue RecordInvalid => error
  # validation failed
end

刪除數(shù)據(jù)

Active Record 支持兩種風格的數(shù)據(jù)刪除。首先,它擁有兩種類級別的刪除方法,分別是 delete()delete_all(),這些都是數(shù)據(jù)庫級別的刪除。delete() 方法接受一個參數(shù) ID 或一組 ID,然后將刪除數(shù)據(jù)庫表中的相應數(shù)據(jù),delete_all() 將刪除相應條件的數(shù)據(jù)(如果不指定條件將刪除所有數(shù)據(jù))。雖然調(diào)用的結(jié)果依賴于選擇的適配器,但通常是受影響的數(shù)據(jù)行數(shù)。如果數(shù)據(jù)并不存在調(diào)用的先后順序異常將不會被拋出。

Order.delete(123)
User.delete([2, 3, 4, 5])
Product.delete_all(["price > ?", @expensive_price])

由 Active Record 提供的另一種刪除方法是 destroy。這些方法要正常運行需要通過 model 對象調(diào)用。

destroy() 實例方法會從數(shù)據(jù)庫刪除 model 對象相應的數(shù)據(jù)。接著它將凍結(jié)對象的內(nèi)容,避免屬性被修改。

order = Order.find_by(name: "Dave")
order.destroy
# ... order is now frozen

不過還有兩個類級別的刪除方法,destroy()(接收一個 ID 或一組 ID)和 destroy_all()(接收條件)。這兩個方法都是將相應數(shù)據(jù)從數(shù)據(jù)庫讀取至 model 對象中,然后再調(diào)用 model 對象的 destroy() 方法。這兩個方法也不會返回任何有意義的值。

Order.destroy_all(["shipped_at < ?", 30.days.ago])

為什么類級別的 deletedestroy 方法都需要呢?delete 會繞過 Active Record 回調(diào)及驗證方法,而 destroy 會保證這些方法都被執(zhí)行。所以當你想保證數(shù)據(jù)庫中數(shù)據(jù)與 model 類中定義的規(guī)則一致的話最好使用 destroy 方法。

在 77 頁的內(nèi)容我們已經(jīng)講過驗證,后面對回調(diào)會進行講解。

參與流程監(jiān)控

Active Record 負責管理 model 對象的生命周期,Active Record 會創(chuàng)建 model 對象,也在它們被修改、保存或更新時進行管理,在看到它們被刪除時也會感到悲傷。只要通過回調(diào)我們的代碼也可以參與到 model 對象的管理流程中。在對象的生命周期中任何重要事件都可以調(diào)用我們編寫的代碼。通過回調(diào)我們可以執(zhí)行復雜的驗證,在數(shù)據(jù)進入或輸出數(shù)據(jù)庫時匹配字段值,以此保證操作的完整性。

Active Record 定義了 16 個回調(diào)函數(shù),其中的 14 個函數(shù)是對象生命周期事件的前后成對的方法。比如,before_destroy 會在 destroy() 方法前被調(diào)用,而 after_destroy() 會在 destroy() 方法后被調(diào)用。另外兩個特別的方法是 after_findafter_initialize,它們都沒有相應的 before 方法。(這兩個回調(diào)函數(shù)與其他的也不同,稍后我們會繼續(xù)討論)。

下列圖示中我們可以看到 Rails 如何包裝圍繞在創(chuàng)建、更新和刪除這些基礎(chǔ)操作周圍周圍的 16 個成對的回調(diào)方法。甚至你不會驚訝驚訝地發(fā)現(xiàn)前后驗證的調(diào)用并不是嚴格的內(nèi)嵌方式。

sequence of active record callbacks

before_validationafter_validation 調(diào)用方法也接收 on: :createon: :update 參數(shù),此參數(shù)表示回調(diào)后觸發(fā)選擇的操作。

圖中的 16 個調(diào)用方法中,after_find 將在任意查詢后觸發(fā),after_initialize 將在 model 對象被創(chuàng)建后觸發(fā)。

如果需要回調(diào)方法調(diào)用你的代碼,你必須編寫一個處理器并將它關(guān)聯(lián)至合適的回調(diào)事件。

下面是兩種基本的實現(xiàn)回調(diào)方式。

我們更加推薦定義回調(diào)方法的方式聲明處理器,處理器可以是一個方法也可以是 block。通過在事件之后使用類方法名便可以將處理器與指定的事件綁定。對于綁定的方法需要聲明為私有或受保護的,然后將它的名字作為處理器聲明的標識。如果回調(diào)是一個 block,只需要將它添加在聲明之后即可,而且 block 接收到的參數(shù)是 model 對象。

class Order < ActiveRecord::Base
  before_validation :normalize_credit_card_number
  after_create do |order|
    logger.info "Order #{order.id} created"
  end

  protected
  def normalize_credit_card_number
    self.cc_number.gsub!(/[-\s]/, '')
  end
end

你也可以對同一個回調(diào)事件定義多個處理器。如果沒有處理器返回 false 它們通常會按指定順序執(zhí)行(處理器必須返回實際值是 false),當處理器返回 false 時回調(diào)鏈將提前斷裂。

而且你也可以通過回調(diào)對象定義回調(diào)實例方法、內(nèi)聯(lián)方法(利用 proc 實現(xiàn))或內(nèi)聯(lián) eval 方法(利用 string 實現(xiàn)),可以通過在線文檔了解更多細節(jié)。

將相關(guān)回調(diào)分組

如果你擁有一個關(guān)聯(lián)回調(diào)的分組,就可以十分方便地將它們分隔到處理器內(nèi)中。這些處理器會在多個 model 中共享使用。處理器類只是一個定義了回調(diào)方法的類(比如 before_save()、after_create() 方法等),一般都在 app/models 路徑中創(chuàng)建處理器類的源文件。

在使用處理器的 model 對象中,你創(chuàng)建了處理器類的實例,并將這些實例傳遞向各個回調(diào)聲明中。下面會舉兩個例子說明。

如果我們的應用在多個地方使用信用卡,就會希望在多個 model 中共享 normalize_credit_card_number 方法。為了按這種方式實現(xiàn),需要將這個方法提取至自己的類中,并在希望處理的事件后命名它。此方法會接收一個參數(shù)——由回調(diào)生成的 model 對象。

class CreditCardCallbacks
  # Normalize the credit card number
  def before_validation(mode)
    model.cc_number.gsub!(/[-\s]/, '')
  end
end

現(xiàn)在,在我們的 model 類中便可以共享調(diào)用這些回調(diào)方法。

class Order < ActiveRecord::Base
  before_validation CreditCardCallbacks.new
  #...
end

class Subscription < ActiveRecord::Base
  before_validation CreditCardCallbacks.new
  #...
end

在這個例子中,處理器類假設(shè)信用卡號在 model 中是命名為 cc_number 的屬性,所以 Order 和 Subscription 都應該擁有這個名字的屬性。不過我們還可以更加泛化這種方式,使處理器類減少與關(guān)聯(lián)類實現(xiàn)細節(jié)的依賴。

例如,我們可以創(chuàng)建一個通用的加密和解密處理器。通過這個處理器,我們可以在將數(shù)據(jù)存入數(shù)據(jù)庫前將其加密,取回時將其解密方便閱讀。在任何需要此功能的 model 中都可以使用此回調(diào)處理器。

在 model 的數(shù)據(jù)被存儲至數(shù)據(jù)庫前處理器需要將 model 中指定的一組屬性進行加密。由于應用還需要處理這些屬性的普通文字版本,所以在存儲完成后還需要將數(shù)據(jù)解密,而且當數(shù)據(jù)從數(shù)據(jù)庫讀取轉(zhuǎn)化為 model 對象時也需要解密。這些需求意味著我們需要處理 before_saveafter_saveafter_find 事件。因為在存儲完數(shù)據(jù)后和查找到數(shù)據(jù)后都需要解密數(shù)據(jù),所以我們通過給 after_save() 添加 after_find() 別名實現(xiàn),同一個方法也可以有兩個名字。

class Encrypter
  def initialize(attrs_to_manage)
    @attrs_to_manage = attrs_to_manage
  end

  def before_save(model)
    @attrs_to_manage.each do |field|
      model[field].tr!("a-z", "b-za")
    end
  end

  def after_save(model)
    @attrs_to_manage.each do |field|
      model[field].tr!("b-za", "a-z")
    end
  end

  alias_method :after_find, :after_save
end

上面例子中的加密比較簡單,在真正使用前你應該想將它改造加強。

現(xiàn)在我們就可以在訂單 model 內(nèi)部使用 Encrypter 類。

require "encrypter"
class Order < ActiveRecord::Base
  encrypter = Encrypter.new([:name, :email])
  before_save encrypter
  after_save encrypter
  after_find encrypter
protected
  def after_find
  end
end

在 model 中,我們創(chuàng)建了一個 Encrypter 對象,并將它與 before_saveafter_saveafter_find 方法關(guān)聯(lián)。按照這種方法,在訂單被保存之前 encrypter 中的 before_save() 方法將被調(diào)用,其他的回調(diào)類似。

不過,為什么我們要定義一個空 after_find() 方法?回憶一下之前說過 after_findafter_initialize 需要區(qū)別對待。

其中的一個特別之處就是如果 model 類沒有真實存在 after_find() 方法 Active Record 就不知道要調(diào)用 after_find 處理器。所以我們必須定義一個空占位符方便 after_find 進行替換。

一切都已準備妥當,但每個 model 類想使用我們的加密處理器時都需要添加 8 行代碼,就像我們在 Order 類中做的那樣,不過我們還可以繼續(xù)改進它。我們可以定義一個輔助方法處理這些工作,并且在所有的 Active Record 中共享使用。為了按這種方式實現(xiàn),我們需要將輔助方法添加至 ActiveRecord::Base 類中。

class ActiveRecord::Base
  def self.encrypt(*attr_names)
    encrypter = Encrypter.new(attr_names)

    before_save encrypter
    after_save encrypter
    after_find encrypter

    define_method(:after_find) { }
  end
end

上述提供的方法中,現(xiàn)在我們通過單獨的調(diào)用便可以向任何 model 類的屬性進行加密。

class Order < ActiveRecord::Base
  encrypt(:name, :email)
end

一個簡單的驅(qū)動程序就可以使我們更加簡易地使用它。

o = Order.new
o.name = "Dave Thomas"
o.address = "123 The Street"
o.email = "dave@example.com"
o.save
puts o.name

o = Order.find(o.id)
puts o.name

在控制臺中,我們會看到 model 對象中客戶的名字(按普通文字顯示)。

ruby encrypt.rb
Dave Thomas
Dave Thomas

不過在數(shù)據(jù)庫中,名字和郵箱地址會被我們強有力的加密方式掩藏起來。

sqlite3 -line db/development.sqlite3 "select * from orders"

回調(diào)是一種不錯的技術(shù),但有時這樣會導致 model 承擔與真實的 model 無關(guān)的職責。比如,在 298 頁,我們創(chuàng)建了一個回調(diào),當訂單被創(chuàng)建時會生成日志日志信息。雖然這個函數(shù)并不應該是 Order 類的一部分,只是為了回調(diào)能正常執(zhí)行所以將其放置在 Order 類中。

如果只是適度使用,這種方法也不會導致重大的問題,不過如果你一直使用這種方式便會發(fā)現(xiàn)自己在重復編寫代碼,此時可以考慮使用 Concerns 替換。

事務

數(shù)據(jù)庫事務就是將一系統(tǒng)變化組織起來,使數(shù)據(jù)庫在應用這些變化時要不都成功要不都失敗。關(guān)于事務的需求其中一個經(jīng)典的例子是在兩個銀行賬戶間轉(zhuǎn)錢?;镜倪壿嬋缦拢?/p>

account1.deposit(100)
account2.withdraw(100)

不過我們需要小心。如果存款成功但由于某些原因轉(zhuǎn)款失?。ㄓ锌赡苁强蛻粲囝~余額不足)會發(fā)生什么?參照上述代碼,account1 將添加 100 美元,而 account2 并沒有扣除相應的錢。這將導致我們憑空創(chuàng)造 100 美元。

在 Active Record 中我們通過 transaction() 方法在相應的數(shù)據(jù)庫事務中執(zhí)行 block。在 block 的結(jié)束時事務將被提交并更新數(shù)據(jù)庫,除非在 block 中有異常被拋出,如果出現(xiàn)異常數(shù)據(jù)庫此次事務中的所有變動都將回滾。由于事務存在于數(shù)據(jù)庫連接的上下文中,我們必須在 Active Record 的接收器中使用。

所以,我們可以這樣編寫代碼:

Account.transaction do
  account1.deposit(100)
  account2.withdraw(100)
end

讓我們探索一下事務。首先要創(chuàng)建一個新的數(shù)據(jù)庫表。(先確認你的數(shù)據(jù)庫支持事務,否則代碼并不能正常運行)。

create_table :accounts, force: true do |t|
  t.string :number
  t.decimal :balance, precision: 10, scale: 2, default: 0
end

接下來,我們要定義一個簡單的賬戶類。類中還需要定義存錢和取錢的實例方法。同時也需要提供一些基礎(chǔ)的驗證,對于這些特殊類型的賬戶交易不能是負的。

class Account < ActiveRecord::Base
  validates :balance, numbericality: {greater_than_or_equal_to: 0}
  def withdraw(amount)
    adjust_balance_and_save!(-amount)
  end

  def deposit(amount)
    adjust_balance_and_save!(amount)
  end

  private
  def adjust_balance_and_save!(amount)
    self.balance += amount
    save!
  end
end

所以,現(xiàn)在讓我們編寫代碼在兩個賬戶之間轉(zhuǎn)錢,而代碼也是十分直截了當。

peter = Account.create(balance: 100, number: "12345")
paul = Account.create(balance: 200, number: "54321")

Account.transaction do
  paul.deposit(10)
  peter.withdraw(10)
end

我們檢查一下數(shù)據(jù)庫,并確認有足夠的錢能夠互相轉(zhuǎn)賬。

sqlite3 -line db/development.sqlite3 "select * from accounts"

現(xiàn)在我們轉(zhuǎn)更多的錢,如果這次我們嘗試轉(zhuǎn)賬 350 美元,Peter 將發(fā)生錯誤,而且驗證規(guī)則也無法通過。讓我們試試:

peter = Account.create(balance: 100, number: "12345")
paul = Account.create(balance: 200, number: "12345")

Account.transaction do
  paul.deposit(350)
  peter.withdraw(350)
end

當我們運行這段代碼時,控制臺中會報告一個異常。

.../validations.rb:736:in `save!': Validation failed: Balance is negativefrom transactions.rb:46:in `adjust_balance_and_save!'
:        :        :
from transactions.rb:8

查看數(shù)據(jù)庫我們會發(fā)現(xiàn)數(shù)據(jù)并沒有發(fā)生變化。

sqlite3 -line db/development.sqlite3 "select * from accounts"

不過還有個陷阱在等待你。事務可以防止數(shù)據(jù)庫由于不一致引起的問題,但我們的 model 對象正常嗎?仔細看看 model 對象發(fā)生的事情,我們必須處理異常讓程序能夠繼續(xù)運行。

peter = Account.create(balance: 100, number: "12345")
paul = Account.create(balance: 200, number: "54321")

begin
  Account.transaction do
    paul.deposit(350)
    peter.withdraw(350)
  end
rescue
  puts "Transfer aborted"
end

puts "Paul has #{paul.balance}"
puts "Peter has #{peter.balance}"

不過結(jié)果還是讓人有點驚訝。

Transfer aborted
Paul has 550.0
Peter has -250.0

盡管數(shù)據(jù)庫遠離了危險,但 model 對象依然會被修改。這是因為 Active Record 不能追蹤到多個對象在變化前后的狀態(tài),實際上要了解哪個 model 中包含事務并不是件容易的事。

構(gòu)建事務

當我們在 282 頁討論父表及子表時,我們講過 Active Record 會在保存父表數(shù)據(jù)時將相關(guān)的子表數(shù)據(jù)也同時存儲。這種方式使用了多 SQL 聲明執(zhí)行(一個是父表數(shù)據(jù)的,其他的每一個都是子表數(shù)據(jù)的)。

當然這些修改都是原子的,但直到現(xiàn)在我們還沒有通過事務保存相關(guān)的對象。是我們忽略了嗎?

幸運的是,并沒有,Active Record 會在事務中處理 save() 方法中所有相關(guān)的更新和插入(destroy() 也會處理相關(guān)的刪除),它們要不就全部成功要不就永遠無法將數(shù)據(jù)寫入數(shù)據(jù)庫。當你管理多個 SQL 聲明時必須要有明確的事務。

當我們了解了基礎(chǔ)知識后事務就顯得十分精妙了。它們展現(xiàn)了叫做 ACID 的特性:它們是原子的,并且保證是一致的,而運行是獨立的,以及影響是持久的(當事務提交后修改就是永久的)。如果你打算了解數(shù)據(jù)庫應用的知識便需要找到一本優(yōu)秀的數(shù)據(jù)庫書籍并閱讀事務相關(guān)相關(guān)章節(jié)。

總結(jié)

我們學習了相關(guān)的數(shù)據(jù)結(jié)構(gòu)和表、類、字段、屬性、id、關(guān)聯(lián)關(guān)系的命令規(guī)范。同時也學習了如何創(chuàng)建、讀取、更新和刪除數(shù)據(jù)。最后,還學習了解了用于防止不一致的變動的事務和回調(diào)。

關(guān)于驗證的知識在 77 頁已經(jīng)進行了表述,當時的討論已經(jīng)包含了 Rails 程序員需要了解的關(guān)于 Active Record 的所有要點。如果章節(jié)的知識并沒有滿足你的需求可以查看 265 頁是生成的 Rails 指南,其中包含更多信息。

下章的主要內(nèi)容是關(guān)于 Action Pack,其中覆蓋了 Rails 的 view 和 controller 部分。


本文翻譯自《Agile Web Development with Rails 4》,目的為學習所用,如有轉(zhuǎn)載請注明出處。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容