# 11.1 微博模型
實現微博資源的第一步是創建微博數據模型,在模型中設定微博的基本特征。和 [2.3 節](chapter2.html#the-microposts-resource)創建的模型類似,我們要實現的微博模型要包含數據驗證,以及和用戶模型之間的關聯。除此之外,我們還會做充分的測試,指定默認的排序方式,以及自動刪除已注銷用戶的微博。
如果使用 Git 做版本控制的話,和之前一樣,建議你新建一個主題分支:
```
$ git checkout master
$ git checkout -b user-microposts
```
## 11.1.1 基本模型
微博模型只需要兩個屬性:一個是 `content`,用來保存微博的內容;另一個是 `user_id`,把微博和用戶關聯起來。微博模型的結構如[圖 11.1](#fig-micropost-model) 所示。
圖 11.1:微博數據模型
注意,在這個模型中,`content` 屬性的類型為 `text`,而不是 `string`,目的是存儲任意長度的文本。雖然我們會限制微博內容的長度不超過 140 個字符([11.1.2 節](#micropost-validations)),也就是說在 `string` 類型的 255 個字符長度的限制內,但使用 `text` 能更好地表達微博的特性,即把微博看成一段文本更符合常理。在 [11.3.2 節](#creating-microposts),會把文本字段換成多行文本字段,用于提交微博。而且,如果以后想讓微博的內容更長一些(例如包含多國文字),使用 `text` 類型處理起來更靈活。何況,在生產環境中使用 `text` 類型并[沒有什么性能差異](http://www.postgresql.org/docs/9.1/static/datatype-character.html),所以不會有什么額外消耗。
和用戶模型一樣([代碼清單 6.1](chapter6.html#listing-generate-user-model)),我們要使用 `generate model` 命令生成微博模型:
```
$ rails generate model Micropost content:text user:references
```
這個命令會生成一個遷移文件,用于在數據庫中生成一個名為 `microposts` 的表,如[代碼清單 11.1](#listing-micropost-migration) 所示。可以和生成 `users` 表的遷移對照一下,參見[代碼清單 6.2](chapter6.html#listing-users-migration)。二者之間最大的區別是,前者使用了 `references` 類型。`references` 會自動添加 `user_id` 列(以及索引),把用戶和微博關聯起來。和用戶模型一樣,微博模型的遷移中也自動生成了 `t.timestamps`。[6.1.1 節](chapter6.html#database-migrations)說過,這行代碼的作用是添加 `created_at` 和 `updated_at` 兩列。([11.1.4 節](#micropost-refinements)和 [11.2.1 節](#rendering-microposts)會使用 `created_at` 列。)
##### 代碼清單 11.1:微博模型的遷移文件,還創建了索引
db/migrate/[timestamp]_create_microposts.rb
```
class CreateMicroposts < ActiveRecord::Migration
def change
create_table :microposts do |t|
t.text :content
t.references :user, index: true, foreign_key: true
t.timestamps null: false
end
add_index :microposts, [:user_id, :created_at] end
end
```
因為我們會按照發布時間的倒序查詢某個用戶發布的所有微博,所以在上述代碼中為 `user_id` 和 `created_at` 列創建了索引(參見[旁注 6.2](chapter6.html#aside-database-indices)):
```
add_index :microposts, [:user_id, :created_at]
```
我們把 `user_id` 和 `created_at` 放在一個數組中,告訴 Rails 我們要創建的是“多鍵索引”(multiple key index),因此 Active Record 會同時使用這兩個鍵。
然后像之前一樣,執行下面的命令更新數據庫:
```
$ bundle exec rake db:migrate
```
## 11.1.2 微博模型的數據驗證
我們已經創建了基本的數據模型,下面要添加一些驗證,實現符合需求的約束。微博模型必須要有一個屬性表示用戶的 ID,這樣才能知道某篇微博是由哪個用戶發布的。實現這樣的屬性,最好的方法是使用 Active Record 關聯。[11.1.3 節](#user-micropost-associations)會實現關聯,現在我們直接處理微博模型。
我們可以參照用戶模型的測試([代碼清單 6.7](chapter6.html#listing-name-presence-test)),在 `setup` 方法中新建一個微博對象,并把它和固件中的一個有效用戶關聯起來,然后在測試中檢查這個微博對象是否有效。因為每篇微博都要和用戶關聯起來,所以我們還要為 `user_id` 屬性的存在性驗證編寫一個測試。綜上所述,測試如[代碼清單 11.2](#listing-micropost-validity-test) 所示。
##### 代碼清單 11.2:測試微博是否有效 RED
test/models/micropost_test.rb
```
require 'test_helper'
class MicropostTest < ActiveSupport::TestCase
def setup
@user = users(:michael)
# 這行代碼不符合常見做法 @micropost = Micropost.new(content: "Lorem ipsum", user_id: @user.id) end
test "should be valid" do
assert @micropost.valid?
end
test "user id should be present" do
@micropost.user_id = nil
assert_not @micropost.valid?
end
end
```
如 `setup` 方法中的注釋所說,創建微博使用的方法不符合常見做法,我們會在 [11.1.3 節](#user-micropost-associations)修正。
微博是否有效的測試能通過,但用戶 ID 存在性驗證的測試無法通過,因為微博模型目前還沒有任何驗證規則:
##### 代碼清單 11.3:**RED**
```
$ bundle exec rake test:models
```
為了讓測試通過,我們要添加用戶 ID 存在性驗證,如[代碼清單 11.4](#listing-micropost-user-id-validation) 所示。(注意,這段代碼中 `belongs_to` 那行由[代碼清單 11.1](#listing-micropost-migration) 中的遷移自動生成。[11.1.3 節](#user-micropost-associations)會深入介紹這行代碼的作用。)
##### 代碼清單 11.4:微博模型 `user_id` 屬性的驗證 GREEN
app/models/micropost.rb
```
class Micropost < ActiveRecord::Base
belongs_to :user
validates :user_id, presence: true end
```
現在,整個測試組件應該都能通過:
##### 代碼清單 11.5:**GREEN**
```
$ bundle exec rake test
```
接下來,我們要為 `content` 屬性加上數據驗證(參照 [2.3.2 節](chapter2.html#putting-the-micro-in-microposts)的做法)。和 `user_id` 一樣,`content` 屬性必須存在,而且還要限制內容的長度不能超過 140 個字符,這才是真正的“微”博。首先,我們要參照 [6.2 節](chapter6.html#user-validations)用戶模型的驗證測試,編寫一些簡單的測試,如[代碼清單 11.6](#listing-micropost-validations-tests) 所示。
##### 代碼清單 11.6:測試微博模型的驗證 RED
test/models/micropost_test.rb
```
require 'test_helper'
class MicropostTest < ActiveSupport::TestCase
def setup
@user = users(:michael)
@micropost = Micropost.new(content: "Lorem ipsum", user_id: @user.id)
end
test "should be valid" do
assert @micropost.valid?
end
test "user id should be present" do
@micropost.user_id = nil
assert_not @micropost.valid?
end
test "content should be present" do @micropost.content = " " assert_not @micropost.valid? end
test "content should be at most 140 characters" do @micropost.content = "a" * 141 assert_not @micropost.valid? end end
```
和 [6.2 節](chapter6.html#user-validations)一樣,[代碼清單 11.6](#listing-micropost-validations-tests)也用到了字符串連乘來測試微博內容長度的驗證:
```
$ rails console
>> "a" * 10
=> "aaaaaaaaaa"
>> "a" * 141
=> "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
```
在模型中添加的代碼基本上和用戶模型 `name` 屬性的驗證一樣([代碼清單 6.16](chapter6.html#listing-length-validation)),如[代碼清單 11.7](#listing-micropost-validations) 所示。
##### 代碼清單 11.7:微博模型的驗證 GREEN
app/models/micropost.rb
```
class Micropost < ActiveRecord::Base
belongs_to :user
validates :user_id, presence: true
validates :content, presence: true, length: { maximum: 140 } end
```
現在,測試組件應該能通過了:
##### 代碼清單 11.8:**GREEN**
```
$ bundle exec rake test
```
## 11.1.3 用戶和微博之間的關聯
為 Web 應用構建數據模型時,最基本的要求是要能夠在不同的模型之間建立關聯。在這個應用中,每篇微博都屬于某個用戶,而每個用戶一般都有多篇微博。用戶和微博之間的關系在 [2.3.3 節](chapter2.html#a-user-has-many-microposts)簡單介紹過,如[圖 11.2](#fig-micropost-belongs-to-user) 和[圖 11.3](#fig-user-has-many-microposts) 所示。在實現這種關聯的過程中,我們會為微博模型和用戶模型編寫一些測試。
圖 11.2:微博和所屬用戶之間的 `belongs_to`(屬于)關系圖 11.3:用戶和微博之間的 `has_many`(擁有多個)關系
使用本節實現的 `belongs_to`/`has_many` 關聯之后,Rails 會自動創建一些方法,如[表 11.1](#table-association-methods) 所示。注意,從表中可知,相較于下面的方法
```
Micropost.create
Micropost.create!
Micropost.new
```
我們得到了
```
user.microposts.create
user.microposts.create!
user.microposts.build
```
后者才是創建微博的正確方式,即通過相關聯的用戶對象創建。通過這種方式創建的微博,其 `user_id` 屬性會自動設為正確的值。所以,我們可以把[代碼清單 11.2](#listing-micropost-validity-test) 中的下述代碼
```
@user = users(:michael)
# 這行代碼不符合常見做法
@micropost = Micropost.new(content: "Lorem ipsum", user_id: @user.id)
```
改為
```
@user = users(:michael)
@micropost = @user.microposts.build(content: "Lorem ipsum")
```
(和 `new` 方法一樣,`build` 方法返回一個存儲在內存中的對象,不會修改數據庫。)只要關聯定義的正確,`@micropost` 變量的 `user_id` 屬性就會自動設為所關聯用戶的 ID。
表 11.1:用戶和微博之間建立關聯后得到的方法簡介
| 方法 | 作用 |
| --- | --- |
| `micropost.user` | 返回和微博關聯的用戶對象 |
| `user.microposts` | 返回用戶發布的所有微博 |
| `user.microposts.create(arg)` | 創建一篇 `user` 發布的微博 |
| `user.microposts.create!(arg)` | 創建一篇 `user` 發布的微博(失敗時拋出異常) |
| `user.microposts.build(arg)` | 返回一個 `user` 發布的新微博對象 |
| `user.microposts.find_by(id: 1)` | 查找 `user` 發布的一篇微博,而且微博的 ID 為 1 |
為了讓 `@user.microposts.build` 這樣的代碼能使用,我們要修改用戶模型和微博模型,添加一些代碼,把這兩個模型關聯起來。[代碼清單 11.1](#listing-micropost-migration) 中的遷移已經自動添加了 `belongs_to :user`,如[代碼清單 11.9](#listing-micropost-belongs-to-user) 所示。關聯的另一頭,`has_many :microposts`,我們要自己動手添加,如[代碼清單 11.10](#listing-user-has-many-microposts) 所示。
##### 代碼清單 11.9:一篇微博屬于(`belongs_to`)一個用戶 GREEN
app/models/micropost.rb
```
class Micropost < ActiveRecord::Base
belongs_to :user validates :user_id, presence: true
validates :content, presence: true, length: { maximum: 140 }
end
```
##### 代碼清單 11.10:一個用戶有多篇(`has_many`)微博 GREEN
app/models/user.rb
```
class User < ActiveRecord::Base
has_many :microposts .
.
.
end
```
定義好關聯后,我們可以修改[代碼清單 11.2](#listing-micropost-validity-test) 中的 `setup` 方法了,使用正確的方式創建一個微博對象,如[代碼清單 11.11](#listing-micropost-validity-test-idiomatic) 所示。
##### 代碼清單 11.11:使用正確的方式創建微博對象 GREEN
test/models/micropost_test.rb
```
require 'test_helper'
class MicropostTest < ActiveSupport::TestCase
def setup
@user = users(:michael)
@micropost = @user.microposts.build(content: "Lorem ipsum") end
test "should be valid" do
assert @micropost.valid?
end
test "user id should be present" do
@micropost.user_id = nil
assert_not @micropost.valid?
end
.
.
.
end
```
當然,經過這次簡單的重構后測試組件應該還能通過:
##### 代碼清單 11.12:**GREEN**
```
$ bundle exec rake test
```
## 11.1.4 改進微博模型
本節,我們要改進一下用戶和微博之間的關聯:按照特定的順序取回用戶的微博,并且讓微博依屬于用戶,如果用戶注銷了,就自動刪除這個用戶發布的所有微博。
### 默認作用域
默認情況下,`user.microposts` 不能確保微博的順序,但是按照博客和 Twitter 的習慣,我們希望微博按照創建時間倒序排列,也就是最新發布的微博在前面。[[1](#fn-1)]為此,我們要使用“默認作用域”(default scope)。
這樣的功能很容易讓測試意外通過(就算應用代碼不對,測試也能通過),所以我們要使用測試驅動開發技術,確保實現的方式是正確的。首先,我們編寫一個測試,檢查數據庫中的第一篇微博和微博固件中名為 `most_recent` 的微博相同,如[代碼清單 11.13](#listing-micropost-order-test) 所示。
##### 代碼清單 11.13:測試微博的排序 RED
test/models/micropost_test.rb
```
require 'test_helper'
class MicropostTest < ActiveSupport::TestCase
.
.
.
test "order should be most recent first" do
assert_equal Micropost.first, microposts(:most_recent)
end
end
```
這段代碼要使用微博固件,所以我們要定義固件,如[代碼清單 11.14](#listing-micropost-fixtures) 所示。
##### 代碼清單 11.14:微博固件
test/fixtures/microposts.yml
```
orange:
content: "I just ate an orange!"
created_at: <%= 10.minutes.ago %>
tau_manifesto:
content: "Check out the @tauday site by @mhartl: http://tauday.com"
created_at: <%= 3.years.ago %>
cat_video:
content: "Sad cats are sad: http://youtu.be/PKffm2uI4dk"
created_at: <%= 2.hours.ago %>
most_recent:
content: "Writing a short test"
created_at: <%= Time.zone.now %>
```
注意,我們使用嵌入式 Ruby 明確設置了 `created_at` 屬性的值。因為這個屬性由 Rails 自動更新,一般無法手動設置,但在固件中可以這么做。實際上可能不用自己設置這些屬性,因為在某些系統中固件會按照定義的順序創建。在這個文件中,最后一個固件最后創建(因此是最新的一篇微博)。但是絕不要依賴這種行為,因為并不可靠,而且在不同的系統中有差異。
現在,測試應該無法通過:
##### 代碼清單 11.15:**RED**
```
$ bundle exec rake test TEST=test/models/micropost_test.rb \
> TESTOPTS="--name test_order_should_be_most_recent_first"
```
我們要使用 Rails 提供的 `default_scope` 方法讓測試通過。這個方法的作用很多,這里我們要用它設定從數據庫中讀取數據的默認順序。為了得到特定的順序,我們要在 `default_scope` 方法中指定 `order` 參數,按 `created_at` 列的值排序,如下所示:
```
order(:created_at)
```
可是,這實現的是“升序”,從小到大排列,即最早發布的微博排在最前面。為了讓微博降序排列,我們要向下走一層,使用純 SQL 語句:
```
order('created_at DESC')
```
在 SQL 中,`DESC` 表示“降序”,即新發布的微博在前面。在以前的 Rails 版本中,必須使用純 SQL 語句才能實現這個需求,但從 Rails 4.0 起,可以使用純 Ruby 句法實現:
```
order(created_at: :desc)
```
把默認作用域加入微博模型,如[代碼清單 11.16](#listing-micropost-ordering) 所示。
##### 代碼清單 11.16:使用 `default_scope` 排序微博 GREEN
app/models/micropost.rb
```
class Micropost < ActiveRecord::Base
belongs_to :user
default_scope -> { order(created_at: :desc) } validates :user_id, presence: true
validates :content, presence: true, length: { maximum: 140 }
end
```
[代碼清單 11.16](#listing-micropost-ordering) 中使用了“箭頭”句法,表示一種對象,叫 Proc(procedure)或 lambda,即“匿名函數”(沒有名字的函數)。`->` 接受一個代碼塊([4.3.2 節](chapter4.html#blocks)),返回一個 Proc。然后在這個 Proc 上調用 `call` 方法執行其中的代碼。我們可以在控制臺中看一下怎么使用 Proc:
```
>> -> { puts "foo" }
=> #<Proc:0x007fab938d0108@(irb):1 (lambda)>
>> -> { puts "foo" }.call
foo
=> nil
```
(Proc 是高級 Ruby 知識,如果現在不理解也不用擔心。)
按照[代碼清單 11.16](#listing-micropost-ordering) 修改后,測試應該可以通過了:
##### 代碼清單 11.17:**GREEN**
```
$ bundle exec rake test
```
### 依屬關系:destroy
除了設定恰當的順序外,我們還要對微博模型做一項改進。我們在 [9.4 節](chapter9.html#deleting-users)介紹過,管理員有刪除用戶的權限。那么,在刪除用戶的同時,有必要把該用戶發布的微博也刪除。
為此,我們可以把一個參數傳給 `has_many` 關聯方法,如[代碼清單 11.18](#listing-micropost-dependency) 所示。
##### 代碼清單 11.18:確保用戶的微博在刪除用戶的同時也被刪除
app/models/user.rb
```
class User < ActiveRecord::Base
has_many :microposts, dependent: :destroy .
.
.
end
```
`dependent: :destroy` 的作用是在用戶被刪除的時候,把這個用戶發布的微博也刪除。這么一來,如果管理員刪除了用戶,數據庫中就不會出現無主的微博了。
我們可以為用戶模型編寫一個測試,證明[代碼清單 11.18](#listing-micropost-dependency) 中的代碼是正確的。我們要保存一個用戶(因此得到了用戶的 ID),再創建一個屬于這個用戶的微博,然后檢查刪除用戶后微博的數量有沒有減少一個,如[代碼清單 11.19](#listing-dependent-destroy-test) 所示。(和[代碼清單 9.57](chapter9.html#listing-delete-link-integration-test) 中“刪除”鏈接的集成測試對比一下。)
##### 代碼清單 11.19:測試 `dependent: :destroy` GREEN
test/models/user_test.rb
```
require 'test_helper'
class UserTest < ActiveSupport::TestCase
def setup
@user = User.new(name: "Example User", email: "user@example.com",
password: "foobar", password_confirmation: "foobar")
end
.
.
.
test "associated microposts should be destroyed" do
@user.save
@user.microposts.create!(content: "Lorem ipsum")
assert_difference 'Micropost.count', -1 do
@user.destroy
end
end
end
```
如果[代碼清單 11.18](#listing-micropost-dependency) 正確,測試組件就應該能通過:
##### 代碼清單 11.20:**GREEN**
```
$ bundle exec rake test
```
- 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 練習