儘管ActiveResource(以下簡稱ARes)在Rails2.0龐大的ChangeLog中只佔了短短的幾行,但我們不應該因此就認爲這只是一個小改動,實際上,ARes可以算作是Rails1.2中引入的Resource概念的一個後續,利用Resource,你可以輕鬆構建基於REST風格的Web Service,而ARes則讓你可以像使用ActiveRecord對象那樣簡單的使用這些Web Service。
理論總是過於抽象,讓我們通過一個例子來看看ARes究竟是如何工作的,假設我們要開發一個類似於豆瓣那樣的書評網站(我們爲它申請了一個 shuping.com的域名),但爲了不在一開始就讓事情變得不可收拾,在第一個迭代週期內,我們只打算讓用戶做兩件事情:提交/編輯書目,提交/編輯 /刪除評論,也就是說我們只需要創建2個Resource:book和comment:
map.resources :books do |book|
book.resources :comments
end
現在,我們已經具有了一組REST風格的URI,用戶可以通過/books/new來創建新書目,或者通過/books/: book_id/comments/new來提交對書目的新評論,這對於使用瀏覽器的普通用戶來說沒什麼問題,只要我們有一位足夠優秀的UI設計師,用戶應該不會在這一過程中感到任何的不快。但我們的目標並不侷限於此,我們希望我們的數據能夠被應用到更廣泛的領域,同時,我們也希望用戶能夠通過更多的方式來向我們提交數據,而不僅僅只是通過我們自己的網站,因此,我們向第三方開發者開放了我們的REST API,他們可以通過:
- GET /books,獲取所有書目
- POST /books,創建新書目
- GET /books/1.xml,獲取編號爲1的書目
- PUT /books/1.xml,修改編號爲1的書目
- DELETE /books/1.xml,刪除編號爲1的書目
但是作爲應用的開發者,他們應該如何來使用我們的API呢,要使用我們的API,他們需要:
- 創建一條到我們服務器的HTTP連接
- 構造一個請求報文
- 發送請求報文到我們的REST URI
- 解析得到的響應,處理可能的錯誤
不過幸運的是,有了ARes,他們不需要真的這麼做,現在讓我們將角色切換到爲我們的書評網站開發應用的第三方開發者,假設我們要使用書評網的數據,那麼,我們首先需要定義一個ARes類:
class Book < ActiveResource::Base
self.site = "http://shuping"
end
接下來,我們就可以像ActiveRecord那樣來使用我們的ActiveResource類了。
獲取書目信息
要從書評網獲取書目信息,可以使用find方法:
books = Book.find(:all)
現在,我們已經得到了書評網最新的書目信息,那麼ARes是如何幫助我們做到這些的呢?實際上它替我們向書評網發送了一條GET請求:
GET http://shuping.com/books.xml
<books type="array">
<book>
<title>Agile Web Development with Rails</title>
</book>
</books>
並將得到的XML報文重新組裝爲本地的Book對象。
但要注意,ARes沒有提供ActiveRecord的find_by函數,不過我們可以通過向find傳遞:params來實現同樣的功能,下面是一些複雜的find操作:
Book.find(:first, :params => {:title => 'TDD'})
# GET http://shuping/books.xml?title=TDD
Book.find(:first, :from => :popular)
# GET http://shuping/books/popular.xml
Book.find(:first, :from => '/yzhang/books.xml')
# GET http://shuping/yzhang/books.xml
同樣的,我們也可以使用save方法創建一條新書目:
book = Book.new(:title => "Programming Ruby")
book.new? # true
book.save # true
book.id # 2
這相當於向書評網發送了一條 POST請求:
POST http://shuping.com/books
POST的內容就是我們新建的書目的XML表示,而返回的XML響應則通過Location字段指示了新資源在遠端服務器上的位置:
Location: http://shuping.com/books/3.xml
ARes會根據返回的響應自動爲我們更新新書目的id字段,因此,我們可以看到,在保存成功後,新書目的id變爲了2,除了find和save,我們也可以更新一個已存在的書目或者刪除錯誤添加的書目:
book = Book.find(2)
book.authors = "Dave Thomas, Andrew Hunter"
book.new? # false
book.save # PUT http://shuping.com/books/1.xml
book.destroy # DELETE http://shuping.com/books/1.xml
book.exist? # false
Book.exist?(2) # false
錯誤處理
上面的代碼,我們假設了所有的數據都是正確的,但在現實世界中,錯誤往往是難以避免的,因此,一個穩固的應用必須考慮到可能的錯誤,首先我們來看看如果我們試圖保存一個沒有名稱的新書目會發生什麼情況:
book = Book.new
book.save # false
book.errors.full_messages # => ["Title can't be blank"]
book.errors.on :title # => Title can't be blank
就像我們所想的,保存失敗了,並且同ActiveRecord一樣,我們可以通過errors來向告訴用戶失敗的原因,但除了Validation失敗,我們還需要面對其它可能出現的錯誤,比如請求的資源不存在,我們將會遭遇一個404響應,因此,要正確的處理這些錯誤,我們必須先了解它們,ARes可能產生的錯誤包括:
- 200 - 399,有效的響應,沒有異常產生
- 404,請求的資源不存在,引發ActiveResource::ResourceNotFound異常
- 409,資源存在衝突,引發ActiveResource::ResourceConflict異常
- 422,無效的資源(如Validation失敗),引發ActiveResource::ResourceInvalid異常
- 401 - 499,其它客戶端錯誤,引發ActiveResource::ClientError異常
- 500 - 599,其它服務端錯誤,引發ActiveResource::ServerError異常
通常情況下,我們只需要考慮404,409,422錯誤即可,因此安全的代碼看起來應該是下面這個樣子:
begin
book = Book.find(1)
rescue ActiveResource::ResourceNotFound
redirect_to :action => 'not_found'
rescue ActiveResource::ResourceConflict, ActiveResource::ResourceInvalid
redirect_to :action => 'new'
end
資源嵌套
大家一定已經注意到了,我們的書評網定義了兩個資源,但是前面的例子都只演示瞭如何操作Book資源,而沒有涉及嵌套在Book中的comment資源,實際上,這相當的簡單,我們只需這樣定義comment.rb:
class Comment < ActiveResource::Base
self.site = "http://shuping/books/:book_id"
end
大家應該注意到,在Site URI中包含了一個:book_id的symbol,ARes會負責將它替換爲真實的user_id,當然我們需要將它傳遞給ARes:
comment = Comment.new(:body => 'test', :book_id => 1)
comment.save #true, POST http://shuping/books/1/comments
comment.id # 1
# GET http://shuping/books/1/comments/1
comment = Comment.find(1, :params => {:book_id => 1})
comment.body # test
自定義方法
現在,讓我們再次回到我們的書評網站,假設我們的網站大受歡迎(雖然這不太可能),那麼,相信我們很快就需要面臨這樣一個問題:垃圾評論。爲了應對垃圾評論,我們決定採用最爲穩妥的方式,人工審覈,因此我們需要爲comment controller擴展一個moderate方法:
book.resources :comments :member => {:moderate => :put}
現在,我們可以通過PUT /books/1/comments/1/moderate來人工批准一條評論,但問題隨之也就來了,畢竟我們自己的人手有限,因此我們計劃讓我們的聯盟網站來和我們一起審覈評論,但是它們應該如何通過ARes來調用moderate方法呢,其實很簡單:
comment = Comment.find(1, :params => {:book_id => 1})
comment.put(:moderate) # PUT http://shuping/books/1/comments/1/moderate
# true
最後,我們的書評網站必然是需要認證的,ARes提供了簡單的方式來支持HTTP認證:
class Book < ActiveResource::Base
self.site = "http://yzhang:password@shuping/"
end