# 6.1 用戶模型
接下來的三章要實現網站的“注冊”頁面(構思圖如[圖 6.1](#fig-signup-mockup-preview) 所示),在此之前我們先要解決存儲問題,因為現在還沒地方存儲用戶信息。所以,實現用戶注冊功能的第一步是,創建一個數據結構,獲取并存儲用戶的信息。
圖 6.1:用戶注冊頁面的構思圖
在 Rails 中,數據模型的默認數據結構叫“模型”(model,MVC 中的 M,參見 [1.3.3 節](chapter1.html#model-view-controller))。Rails 為解決數據持久化提供的默認解決方案是,使用數據庫存儲需要長期使用的數據。和數據庫交互默認使用的是 Active Record。[[1](#fn-1)]Active Record 提供了一系列方法,無需使用[關系數據庫](http://en.wikipedia.org/wiki/Relational_database)所用的“結構化查詢語言”(Structured Query Language,簡稱 SQL),[[2](#fn-2)]就能創建、保存和查詢數據對象。Rails 還支持“遷移”(migration)功能,允許我們使用純 Ruby 代碼定義數據結構,而不用學習 SQL “數據定義語言”(Data Definition Language,簡稱 DDL)。最終的結果是,Active Record 把你和數據存儲層完全隔開了。本書開發的應用在本地使用 SQLite,部署后使用 PostgreSQL(由 Heroku 提供,參見 [1.5 節](chapter1.html#deploying))。這就引出了一個更深層的話題——在不同的環境中,即便使用不同類型的數據庫,我們也無需關心 Rails 是如何存儲數據的。
和之前一樣,如果使用 Git 做版本控制,現在應該新建一個主題分支:
```
$ git checkout master
$ git checkout -b modeling-users
```
## 6.1.1 數據庫遷移
回顧一下 [4.4.5 節](chapter4.html#a-user-class)的內容,在我們自己創建的 `User` 類中為用戶對象定義了 `name` 和 `email` 兩個屬性。那是個很有用的例子,但沒有實現持久性最關鍵的要求:在 Rails 控制臺中創建的用戶對象,退出控制臺后就會消失。本節的目的是為用戶創建一個模型,讓用戶數據不會這么輕易消失。
和 [4.4.5 節](chapter4.html#a-user-class)中定義的 `User` 類一樣,我們先為用戶模型創建兩個屬性,分別是 `name` 和 `email`。我們會把 `email` 屬性用作唯一的用戶名。[[3](#fn-3)]([6.3 節](#adding-a-secure-password)會添加一個屬性,存儲密碼)在[代碼清單 4.13](chapter4.html#listing-example-user) 中,我們使用 Ruby 的 `attr_accessor` 方法創建了這兩個屬性:
```
class User
attr_accessor :name, :email
.
.
.
end
```
不過,在 Rails 中不用這樣定義屬性。前面提到過,Rails 默認使用關系數據庫存儲數據,數據庫中的表由數據行組成,每一行都有相應的列,對應數據屬性。例如,為了存儲用戶的名字和電子郵件地址,我們要創建 `users` 表,表中有兩個列,`name` 和 `email`,這樣每一行就表示一個用戶,如[圖 6.2](#fig-users-table) 所示,對應的數據模型如[圖 6.3](#fig-user-model-sketch) 所示。(圖 6.3 只是梗概,完整的數據模型如[圖 6.4](#fig-user-model-initial) 所示。)把列命名為 `name` 和 `email` 后,Active Record 會自動把它們識別為用戶對象的屬性。
圖 6.2:`users` 表中的示例數據圖 6.3:用戶數據模型梗概
你可能還記得,在[代碼清單 5.28](chapter5.html#listing-generate-users-controller) 中,我們使用下面的命令生成了用戶控制器和 `new` 動作:
```
$ rails generate controller Users new
```
創建模型有個類似的命令——`generate model`。我們可以使用這個命令生成用戶模型,以及 `name` 和 `email` 屬性,如[代碼清單 6.1](#listing-generate-user-model) 所示。
##### 代碼清單 6.1:生成用戶模型
```
$ rails generate model User name:string email:string
invoke active_record
create db/migrate/20140724010738_create_users.rb
create app/models/user.rb
invoke test_unit
create test/models/user_test.rb
create test/fixtures/users.yml
```
(注意,控制器名是復數,模型名是單數:控制器是 `Users`,而模型是 `User`。)我們指定了可選的參數 `name:string` 和 `email:string`,告訴 Rails 我們需要的兩個屬性是什么,以及各自的類型(兩個都是字符串)。你可以把這兩個參數與[代碼清單 3.4](chapter3.html#listing-generating-pages) 和[代碼清單 5.28](chapter5.html#listing-generate-users-controller) 中的動作名對比一下,看看有什么不同。
執行上述 `generate` 命令之后,會生成一個遷移文件。遷移是一種遞進修改數據庫結構的方式,可以根據需求修改數據模型。執行 `generate` 命令后會自動為用戶模型創建遷移,這個遷移的作用是創建一個 `users` 表以及 `name` 和 `email` 兩個列,如[代碼清單 6.2](#listing-users-migration) 所示。(我們會在 [6.2.5 節](#uniqueness-validation)介紹如何手動創建遷移文件。)
##### 代碼清單 6.2:用戶模型的遷移文件(創建 `users` 表)
db/migrate/[timestamp]_create_users.rb
```
class CreateUsers < ActiveRecord::Migration
def change
create_table :users do |t|
t.string :name
t.string :email
t.timestamps null: false
end
end
end
```
注意,遷移文件名前面有個時間戳,指明創建的時間。早期,遷移文件名的前綴是遞增的數字,在團隊協作中,如果多個程序員生成了序號相同的遷移文件就可能會發生沖突。除非兩個遷移文件在同一秒鐘生成這種小概率事件發生了,否則使用時間戳基本可以避免沖突的發生。
遷移文件中有一個名為 `change` 的方法,定義要對數據庫做什么操作。在[代碼清單 6.2](#listing-users-migration) 中,`change` 方法使用 Rails 提供的 `create_table` 方法在數據庫中新建一個表,用來存儲用戶。`create_table` 方法可以接受一個塊,塊中有一個塊變量 `t`(“table”)。在塊中,`create_table` 方法通過 `t` 對象創建 `name` 和 `email` 兩個列,均為 `string` 類型。[[4](#fn-4)]表名是復數形式(`users`),不過模型名是單數形式(`User`),這是 Rails 在用詞上的一個約定:模型表示單個用戶,而數據庫表中存儲了很多用戶。塊中最后一行 `t.timestamps null: false` 是個特殊的方法,它會自動創建兩個列,`created_at` 和 `updated_at`,這兩個列分別記錄創建用戶的時間戳和更新用戶數據的時間戳。([6.1.3 節](#creating-user-objects)有使用這兩個列的例子。)這個遷移文件表示的完整數據模型如[圖 6.4](#fig-user-model-initial) 所示。(注意,[圖 6.3](#fig-user-model-sketch) 中沒有列出自動添加的兩個時間戳列。)
圖 6.4:[代碼清單 6.2](#listing-users-migration) 生成的用戶數據模型
我們可以使用如下的 `rake` 命令([旁注 2.1](chapter2.html#aside-rake))執行這個遷移(叫“向上遷移”):
```
$ bundle exec rake db:migrate
```
(你可能還記得,我們在 [2.2 節](chapter2.html#the-users-resource)用過這個命令。)第一次運行 `db:migrate` 命令時會創建 `db/development.sqlite3`,這是 SQLite [[5](#fn-5)]數據庫文件。若要查看數據庫結構,可以使用 [SQLite 數據庫瀏覽器](http://sqlitebrowser.org/)打開 `db/development.sqlite3` 文件,如[圖 6.5](#fig-sqlite-database-browser) 所示。(如果想從云端 IDE 把這個文件下載到本地電腦,可以在 `db/development.sqlite3` 上按右鍵,然后選擇“Download”。)和[圖 6.4](#fig-user-model-initial) 中的模型對比之后,你可能會發現有一個列在遷移中沒有出現——`id` 列。[2.2 節](chapter2.html#the-users-resource)提到過,這個列是自動生成的,Rails 用這個列作為行的唯一標識符。
圖 6.5:在 SQLite 數據庫瀏覽器中查看剛創建的 `users` 表
大多數遷移,包括本書中的所有遷移,都是可逆的,也就是說可以使用一個簡單的 Rake 命令“向下遷移”,撤銷之前的操作,這個命令是 `db:rollback`:
```
$ bundle exec rake db:rollback
```
(還有一個撤銷遷移的方法,參見[旁注 3.1](chapter3.html#aside-undoing-things)。)這個命令會調用 `drop_table` 方法,把 `users` 表從數據庫中刪除。之所以可以這么做,是因為 `change` 方法知道 `create_table` 的逆操作是 `drop_table`,所以回滾時會直接調用 `drop_table` 方法。對于一些無法自動逆轉的操作,例如刪除列,就不能依賴 `change` 方法了,我們要分別定義 `up` 和 `down` 方法。關于遷移的更多信息請查看 [Rails 指南](http://guides.rubyonrails.org/migrations.html)。
如果你執行了上面的回滾操作,在繼續閱讀之前請再遷移回來:
```
$ bundle exec rake db:migrate
```
## 6.1.2 模型文件
我們看到,執行[代碼清單 6.1](#listing-generate-user-model) 中的命令后會生成一個遷移文件([代碼清單 6.2](#listing-users-migration)),也看到了執行遷移后得到的結果([圖 6.5](#fig-sqlite-database-browser)):修改 `db/development.sqlite3` 文件,新建 `users` 表,并創建 `id`、`name`、`email`、`created_at` 和 `updated_at` 這幾個列。[代碼清單 6.1](#listing-generate-user-model) 同時還生成了一個模型文件,本節剩下的內容專門解說這個文件。
我們先看用戶模型的代碼,在 `app/models/` 文件夾中的 `user.rb` 文件里。這個文件的內容非常簡單,如[代碼清單 6.3](#listing-raw-user-model) 所示。
##### 代碼清單 6.3:剛創建的用戶模型
app/models/user.rb
```
class User < ActiveRecord::Base
end
```
[4.4.2 節](chapter4.html#class-inheritance)介紹過,`class User < ActiveRecord::Base` 的意思是 `User` 類繼承自 `ActiveRecord::Base` 類,所以用戶模型自動獲得了 `ActiveRecord::Base` 的所有功能。當然了,只知道這種繼承關系沒什么用,我們并不知道 `ActiveRecord::Base` 做了什么。下面看幾個實例。
## 6.1.3 創建用戶對象
和[第 4 章](chapter4.html#rails-flavored-ruby)一樣,探索數據模型使用的工具是 Rails 控制臺。因為我們還不想修改數據庫中的數據,所以要在沙盒模式中啟動控制臺:
```
$ rails console --sandbox
Loading development environment in sandbox
Any modifications you make will be rolled back on exit
>>
```
如提示消息所說,“Any modifications you make will be rolled back on exit”,在沙盒模式下使用控制臺,退出當前會話后,對數據庫做的所有改動都會回歸到原來的狀態。
在 [4.4.5 節](chapter4.html#a-user-class)的控制臺會話中,我們要引入[代碼清單 4.13](chapter4.html#listing-example-user) 中的代碼才能使用 `User.new` 創建用戶對象。對模型來說,情況有所不同。你可能還記得 [4.4.4 節](chapter4.html#a-controller-class)說過,Rails 控制臺會自動加載 Rails 環境,這其中就包括模型。也就是說,現在無需加載任何代碼就可以直接創建用戶對象:
```
>> User.new
=> #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil>
```
上述代碼顯示了一個用戶對象的默認值。
如果不為 `User.new` 指定參數,對象的所有屬性值都是 `nil`。在 [4.4.5 節](chapter4.html#a-user-class),自己編寫的 `User` 類可以接受一個哈希參數初始化對象的屬性。這種方式是受 Active Record 啟發的,在 Active Record 中也可以使用相同的方式指定初始值:
```
>> user = User.new(name: "Michael Hartl", email: "mhartl@example.com")
=> #<User id: nil, name: "Michael Hartl", email: "mhartl@example.com", created_at: nil, updated_at: nil>
```
我們看到 `name` 和 `email` 屬性的值都已經設定了。
數據的有效性對理解 Active Record 模型對象很重要,我們會在 [6.2 節](#user-validations)深入介紹。不過注意,現在這個 `user` 對象是有效的,我們可以在這個對象上調用 `valid?` 方法確認:
```
>> user.valid?
true
```
到目前為止,我們都沒有修改數據庫:`User.new` 只在內存中創建一個對象,`user.valid?` 只是檢查對象是否有效。如果想把用戶對象保存到數據庫中,我們要在 `user` 變量上調用 `save` 方法:
```
>> user.save
(0.2ms) begin transaction
User Exists (0.2ms) SELECT 1 AS one FROM "users" WHERE LOWER("users".
"email") = LOWER('mhartl@example.com') LIMIT 1
SQL (0.5ms) INSERT INTO "users" ("created_at", "email", "name", "updated_at)
VALUES (?, ?, ?, ?) [["created_at", "2014-09-11 14:32:14.199519"],
["email", "mhartl@example.com"], ["name", "Michael Hartl"], ["updated_at",
"2014-09-11 14:32:14.199519"]]
(0.9ms) commit transaction
=> true
```
如果保存成功,`save` 方法返回 `true`,否則返回 `false`。(現在所有保存操作都會成功,因為還沒有數據驗證功能,[6.2 節](#user-validations)會看到一些失敗的例子。)Rails 還會在控制臺中顯示 `user.save` 對應的 SQL 語句(`INSERT INTO "users"…`),以供參考。本書幾乎不會使用原始的 SQL,[[6](#fn-6)]所以此后會省略 SQL。不過,從 Active Record 各種操作生成的 SQL 中可以學到很多知識。
你可能注意到了,剛創建時用戶對象的 `id`、`created_at` 和 `updated_at` 屬性值都是 `nil`,下面看一下保存之后有沒有變化:
```
>> user
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com",
created_at: "2014-07-24 00:57:46", updated_at: "2014-07-24 00:57:46">
```
我們看到,`id` 的值變成了 `1`,那兩個自動創建的時間戳屬性也變成了當前時間。[[7](#fn-7)]現在這兩個時間戳是一樣的,[6.1.5 節](#updating-user-objects)會看到二者不同的情況。
和 [4.4.5 節](chapter4.html#a-user-class)的 `User` 類一樣,用戶模型的實例也可以使用點號獲取屬性:
```
>> user.name
=> "Michael Hartl"
>> user.email
=> "mhartl@example.com"
>> user.updated_at
=> Thu, 24 Jul 2014 00:57:46 UTC +00:00
```
[第 7 章](chapter7.html#sign-up)會介紹,雖然一般習慣把創建和保存分成如上所示的兩步完成,不過 Active Record 也允許我們使用 `User.create` 方法把這兩步合成一步:
```
>> User.create(name: "A Nother", email: "another@example.org")
#<User id: 2, name: "A Nother", email: "another@example.org", created_at:
"2014-07-24 01:05:24", updated_at: "2014-07-24 01:05:24">
>> foo = User.create(name: "Foo", email: "foo@bar.com")
#<User id: 3, name: "Foo", email: "foo@bar.com", created_at: "2014-07-24
01:05:42", updated_at: "2014-07-24 01:05:42">
```
注意,`User.create` 的返回值不是 `true` 或 `false`,而是創建的用戶對象,可直接賦值給變量(例如上面第二個命令中的 `foo` 變量).
`create` 的逆操作是 `destroy`:
```
>> foo.destroy
=> #<User id: 3, name: "Foo", email: "foo@bar.com", created_at: "2014-07-24
01:05:42", updated_at: "2014-07-24 01:05:42">
```
奇怪的是,`destroy` 和 `create` 一樣,返回值是對象。我不覺得什么地方會用到 `destroy` 的返回值。更奇怪的是,銷毀的對象還在內存中:
```
>> foo
=> #<User id: 3, name: "Foo", email: "foo@bar.com", created_at: "2014-07-24
01:05:42", updated_at: "2014-07-24 01:05:42">
```
那么我們怎么知道對象是否真被銷毀了呢?對于已經保存而沒有銷毀的對象,怎樣從數據庫中讀取呢?要回答這些問題,我們要先學習如何使用 Active Record 查找用戶對象。
## 6.1.4 查找用戶對象
Active Record 提供了好幾種查找對象的方法。下面我們使用這些方法查找創建的第一個用戶,同時也驗證一下第三個用戶(`foo`)是否被銷毀了。先看一下還存在的用戶:
```
>> User.find(1)
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com",
created_at: "2014-07-24 00:57:46", updated_at: "2014-07-24 00:57:46">
```
我們把用戶的 ID 傳給 `User.find` 方法,Active Record 會返回 ID 為 1 的用戶對象。
下面來看一下 ID 為 3 的用戶是否還在數據庫中:
```
>> User.find(3)
ActiveRecord::RecordNotFound: Couldn't find User with ID=3
```
因為我們在 [6.1.3 節](#creating-user-objects)銷毀了第三個用戶,所以 Active Record 無法在數據庫中找到這個用戶,拋出了一個異常,這說明在查找過程中出現了問題。因為 ID 不存在,所以 `find` 方法拋出 `ActiveRecord::RecordNotFound` 異常。[[8](#fn-8)]
除了這種查找方法之外,Active Record 還支持通過屬性查找用戶:
```
>> User.find_by(email: "mhartl@example.com")
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com",
created_at: "2014-07-24 00:57:46", updated_at: "2014-07-24 00:57:46">
```
我們會使用電子郵件地址做用戶名,所以在學習如何讓用戶登錄網站時會用到這種 `find` 方法([第 7 章](chapter7.html#sign-up))。你可能會擔心如果用戶數量過多,使用 `find_by` 的效率不高。事實的確如此,我們會在 [6.2.5 節](#uniqueness-validation)說明這個問題,以及如何使用數據庫索引解決。
最后,再介紹幾個常用的查找方法。首先是 `first` 方法:
```
>> User.first
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com",
created_at: "2014-07-24 00:57:46", updated_at: "2014-07-24 00:57:46">
```
很明顯,`first` 會返回數據庫中的第一個用戶。還有 `all` 方法:
```
>> User.all
=> #<ActiveRecord::Relation [#<User id: 1, name: "Michael Hartl",
email: "mhartl@example.com", created_at: "2014-07-24 00:57:46",
updated_at: "2014-07-24 00:57:46">, #<User id: 2, name: "A Nother",
email: "another@example.org", created_at: "2014-07-24 01:05:24",
updated_at: "2014-07-24 01:05:24">]>
```
從控制臺的輸出可以看出,`User.all` 方法返回一個 `ActiveRecord::Relation` 實例,其實這是一個數組([4.3.1 節](chapter4.html#arrays-and-ranges)), 包含數據庫中的所有用戶。
## 6.1.5 更新用戶對象
創建對象后,一般都會進行更新操作。更新有兩種基本方式,其一,可以分別為各屬性賦值,在 [4.4.5 節](chapter4.html#a-user-class)就是這么做的:
```
>> user # 只是為了查看 user 對象的屬性是什么
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com",
created_at: "2014-07-24 00:57:46", updated_at: "2014-07-24 00:57:46">
>> user.email = "mhartl@example.net"
=> "mhartl@example.net"
>> user.save
=> true
```
注意,如果想把改動寫入數據庫,必須執行最后一個方法。我們可以執行 `reload` 命令來看一下沒保存的話是什么情況。`reload` 命令會使用數據庫中的數據重新加載對象:
```
>> user.email
=> "mhartl@example.net"
>> user.email = "foo@bar.com"
=> "foo@bar.com"
>> user.reload.email
=> "mhartl@example.net"
```
現在我們已經更新了用戶數據,如在 [6.1.3 節](#creating-user-objects)中所說,自動創建的那兩個時間戳屬性不一樣了:
```
>> user.created_at
=> "2014-07-24 00:57:46"
>> user.updated_at
=> "2014-07-24 01:37:32"
```
更新數據的第二種常用方式是使用 `update_attributes` 方法:[[9](#fn-9)]
```
>> user.update_attributes(name: "The Dude", email: "dude@abides.org")
=> true
>> user.name
=> "The Dude"
>> user.email
=> "dude@abides.org"
```
`update_attributes` 方法接受一個指定對象屬性的哈希作為參數,如果操作成功,會執行更新和保存兩個操作(保存成功時返回值為 `true`)。注意,如果任何一個數據驗證失敗了,例如存儲記錄時需要密碼([6.3 節](#adding-a-secure-password)實現),`update_attributes` 操作就會失敗。如果只需要更新單個屬性,可以使用 `update_attribute`,跳過驗證:
```
>> user.update_attribute(:name, "The Dude")
=> true
>> user.name
=> "The Dude"
```
- Ruby on Rails 教程
- 致中國讀者
- 序
- 致謝
- 作者譯者簡介
- 版權和代碼授權協議
- 第 1 章 從零開始,完成一次部署
- 1.1 簡介
- 1.2 搭建環境
- 1.3 第一個應用
- 1.4 使用 Git 做版本控制
- 1.5 部署
- 1.6 小結
- 1.7 練習
- 第 2 章 玩具應用
- 2.1 規劃應用
- 2.2 用戶資源
- 2.3 微博資源
- 2.4 小結
- 2.5 練習
- 第 3 章 基本靜態的頁面
- 3.1 創建演示應用
- 3.2 靜態頁面
- 3.3 開始測試
- 3.4 有點動態內容的頁面
- 3.5 小結
- 3.6 練習
- 3.7 高級測試技術
- 第 4 章 Rails 背后的 Ruby
- 4.1 導言
- 4.2 字符串和方法
- 4.3 其他數據類型
- 4.4 Ruby 類
- 4.5 小結
- 4.6 練習
- 第 5 章 完善布局
- 5.1 添加一些結構
- 5.2 Sass 和 Asset Pipeline
- 5.3 布局中的鏈接
- 5.4 用戶注冊:第一步
- 5.5 小結
- 5.6 練習
- 第 6 章 用戶模型
- 6.1 用戶模型
- 6.2 用戶數據驗證
- 6.3 添加安全密碼
- 6.4 小結
- 6.5 練習
- 第 7 章 注冊
- 7.1 顯示用戶的信息
- 7.2 注冊表單
- 7.3 注冊失敗
- 7.4 注冊成功
- 7.5 專業部署方案
- 7.6 小結
- 7.7 練習
- 第 8 章 登錄和退出
- 8.1 會話
- 8.2 登錄
- 8.3 退出
- 8.4 記住我
- 8.5 小結
- 8.6 練習
- 第 9 章 更新,顯示和刪除用戶
- 9.1 更新用戶
- 9.2 權限系統
- 9.3 列出所有用戶
- 9.4 刪除用戶
- 9.5 小結
- 9.6 練習
- 第 10 章 賬戶激活和密碼重設
- 10.1 賬戶激活
- 10.2 密碼重設
- 10.3 在生產環境中發送郵件
- 10.4 小結
- 10.5 練習
- 10.6 證明超時失效的比較算式
- 第 11 章 用戶的微博
- 11.1 微博模型
- 11.2 顯示微博
- 11.3 微博相關的操作
- 11.4 微博中的圖片
- 11.5 小結
- 11.6 練習
- 第 12 章 關注用戶
- 12.1 “關系”模型
- 12.2 關注用戶的網頁界面
- 12.3 動態流
- 12.4 小結
- 12.5 練習