Micropost refinements
這節我們要來改善一下 user 和 micropost 之間的關聯:
- 按照特定的順序取得 user 的 microposts
- 讓 microposts 依存(dependent) users,一旦 user 銷毀,就自動刪除 user 的所有 microposts
Default scope
預設中,user.microposts 方法無法確保 microposts 的順序,但按照一般 blogs 以及 Twitter 的習慣,我們希望 microposts 按照發布的時間倒序排列,也就是最新發佈的 micropost 會在最前面。(之前要列出所有使用者列表時也有相同問題)
因此,我們要使用 default scope。
這樣的功能很容易導致測試意外通過(例如就算 application code 不對,測試也會通過),所以我們要使用 TDD(test-driven development)來確保實現的方式是正確的。首先,要先編寫一個測試,檢查資料庫中的第一篇 micropost 和 fixture 裡名為 most_recent 的 micropost 一樣:
test/models/micropost_test.rb
require 'test_helper'
class MicropostTest < ActiveSupport::TestCase
.
.
.
test "order should be most recent first" do
assert_equal microposts(:most_recent), Micropost.first
end
end
上述程式碼需要一些 mcroposts fixtures,所以我們要來增加一下:
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 %>
注意,我們使用 ERb 設定 created_at 欄位的值,因為這個欄位是由 Rails 自動更新,通常不可能手動設定,但在 fixtures 可以做到。實際上可能不用自己設定這些屬性,因為在某些系統中 fixtures 會按照定義的順序建立。
在我們的例子中,最後一個 fixture 是最後被建立的(所以會是最新的 micropost),但不要依賴這種行為,非常不可靠,而且不同系統間會有差異。
現在執行測試會是失敗的(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 參數,按照 crested_at 欄位的值排序:
order(:created_at)
不過,這樣的順序是升冪排序(ascending order),也就是由小到大,表示最舊的 microposts 會被排在前面。為了要降冪排列,我們使用 SQL 語法:
order('created_at DESC')
DESC 在 SQL 中表示降冪(descending),例如從最新排到最舊。
SQL 不分大小寫,但習慣使用大寫來表示 SQL 的關鍵字,像是
DESC
舊版 Rails 中,使用純 SQL 語法才能實現這個需求,不過在 Rails 4.0,我們可以使用純 Ruby 語法實現:
order(created_at: :desc)
幫 Micropost model 加上 default scope:
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
上述程式碼使用了「箭頭」語法(stabby lambda syntax),指向一個物件,這個物件稱為 Proc(procedure)或是 lambda,這是一個匿名函式(anonymous function),也就是沒有名字的函式。
stabby lambda -> 接受一個 block,然後回傳一個 Proc,這個 Proc 可以使用 call 方法來調用,執行其中的程式碼,我們可以透過 console 來看它是怎麼運作的:
>> -> { puts "foo" }
=> #<Proc:0x007fab938d0108@(irb):1 (lambda)>
>> -> { puts "foo" }.call
foo
=> nil
然後執行測試,會通過(Green):
$ bundle exec rake test
Dependent:destroy
除了正確的順序,我們還要改善一個東西,之前在設定管理員的時候,管理員有權刪除使用者,如果使用者被刪除了,那麼該名使用者的所有 microposts 也應該被刪除。
我們可以透過傳遞一個參數給 has_many 關聯方法來實現這個行為:
app/models/user.rb
class User < ActiveRecord::Base
has_many :microposts, dependent: :destroy
.
.
.
end
參數 dependent: :destroy 的作用就是當使用者自己被刪除的時候,依附在使用者上的 microposts 也會一併被刪除。這可以防止當管理員從系統刪除使用者時,資料庫不會出現沒有主人的 microposts。
我們可以在 User model 寫個測試來檢查這個行為,只要先儲存使用者(如此就會得到 id),然後建立與之關聯的 micropost,接著檢查刪除使用者之後,microposts 的數量有沒有少一個:
test/models/user_test.rb
require 'test_helper'
class UserTest < ActiveSupport::TestCase
def setup
@user = User.new(name: "Example User", email: "[email protected]",
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
執行測試應該會通過(Green):
$ bundle exec rake test