jbuilder vs active_model_serializers vs oj_serializers - Rails JSON API 解決方案選擇
Rails 產生 JSON 有內建的 as_json 可以使用之外,對於 JSON API 處理也有預設安裝的 jbuilder 套件,也有其他不同套件可以選擇。本篇文章將比較下面幾種解決方案:
依據個人的經驗考慮以下需求:
- 容易整理維護。
- 定義好的結構能夠方便重用。
- 要能在程式中產生 JSON 物件使用。
- 效能。
- 方便測試。
情境
假設有兩個 model:Post 和 User。想要分別產生下面的 JSON 結果:
Post index
1 | { |
Post show
1 | { |
User index
1 | { |
這邊是模擬列表頁顯示部分資料,進入內頁後則吐出完整資料,以及巢狀資料結構的情境。
功能
下面分別以四種不同方式來實作上面的例子。
as_json
post.rb
1 | class Post < ApplicationRecord |
posts_controller.rb
1 | class PostsController < ApplicationController |
users_controller.rb
1 | class UsersController < ApplicationController |
作為內建的函式,可以很簡單地將 model 資料轉成 JSON 輸出,不過欄位要處理後才輸出的情況並不好使用。上面例子可以看到需要在 model 實作 timeago。而在重用結構的部分,則需要自行設計一套方式來整理,否則像上面會有很多程式重複。
jbuilder
users/_user.json.jbuilder
1 | json.extract! user, *%i[id name] |
users/index.json.jbuilder
1 | json.users do |
posts/_post.json.jbuilder
1 | json.extract! post, *%i[id title] |
posts/index.json.jbuilder
1 | json.posts do |
posts/show.json.jbuilder
1 | json.post do |
posts_controller.rb
1 | class PostsController < ApplicationController |
users_controller.rb
1 | class UsersController < ApplicationController |
jbuilder 使用 Render View 的方式來輸出 JSON,將如何產生 JSON 的邏輯都移到 view 去。由於是 Render View 的使用方式,在重用的部分使用 partial 來達成,上面範例可以將 User 和 Post 結構重複使用。不過在 Post Show 要輸出詳細資料的情況,需要搭配參數來做不同輸出的判斷,才能重用結構。
由於使用 Render View 來產生 JSON,所以在不是 API 的地方要產生 JSON 就會比較麻煩一點。另外測試需使用 Controller 來測試,在 View 的程式邏輯測試覆蓋率也不會列入計算。
如果想要在程式中利用現有的 View 產生 JSON 的寫法,可以這樣寫
1 | ActionController::Base.new.render_to_string( |
這樣產生的是 JSON 字串,如果需要物件進一步操作,要先 JSON.parse
active_model_serializers
user_serializer.rb
1 | class UserSerializer < ActiveModel::Serializer |
post_serializer.rb
1 | class PostSerializer < ActiveModel::Serializer |
post_detail_serializer.rb
1 | class PostDetailSerializer < PostSerializer |
posts_controller.rb
1 | class PostsController < ApplicationController |
users_controller.rb
1 | class UsersController < ApplicationController |
active_model_serializers 使用定義 Serializer 類別的方式來輸出,重用的部分也可以使用繼承的方式來輸出詳細內容,如上面的 PostDetailSerializer。
如果要在程式中產生 JSON 可以這樣寫:
1 | ActiveModelSerializers::SerializableResource.new(post, { |
不過其實使用上存在蠻多問題:
nil 要額外處理
當取得單筆資料時,如果不是回傳 404 而是期望輸出 null 時,會發生錯誤。例如下面情況,
1 | render json: post, serializer: PostDetailSerializer |
當 post 為 nil 時,原本期望輸出
1 | { |
實際上會發生錯誤
1 | undefined method `read_attribute_for_serialization' for nil |
變成要寫成這樣
1 | if post |
預設單一 root
預設用法會自動產生 posts 或 post 之類的 root,例如:
1 | { |
想要輸出額外的資料,例如
1 | { |
預設做不到,只能使用上面生成 JSON 的方式,或使用 meta 參數
1 | render json: Post.all, meta: { total_pages: 10 } |
但這樣會多一層
1 | { |
oj_serializers
user_serializer.rb
1 | class UserSerializer < Oj::Serializer |
post_serializer.rb
1 | class PostSerializer < Oj::Serializer |
post_detail_serializer.rb
1 | class PostDetailSerializer < PostSerializer |
posts_controller.rb
1 | class PostsController < ApplicationController |
users_controller.rb
1 | class UsersController < ApplicationController |
和 active_model_serializers 一樣式定義類別來產生,但用起來更直覺。透過呼叫定義好的 Serializer 中的函式就可以直接產生 JSON,在 Controller 自行組成 JSON,分頁等其他資訊也可以很容易的加上去。至於單一資料 nil 的問題,如果想要輸出 nil,可透過 one_if 函式達成:
1 | render json: { post: PostDetailSerializer.one_if(post) } |
效能
接著來測試一下效能,這邊我建立了一個測試專案,跑出來的結果大概是這樣
1 | as_json 0.116 (± 0.0%) i/s (8.62 s/i) - 1.000 in 8.623844s |
oj_serializer 的效能最好,其中我又湊巧發現加上 to_json 的效能又更好,以上面的例子來說,就是改成這樣
1 | render json: { posts: PostSerializer.render(Post.all) }.to_json |
上面的測試是已經加上了 oj 的優化結果。
總結
將需求做成表格進行比較
| 需求 | as_json | jbuilder | active_model_serializers | oj_serializer |
|---|---|---|---|---|
| 容易整理維護 | X | O | O | O |
| 方便重用 | X | O | O | O |
| 在程式中產生 JSON | O | △ | O | O |
| 效能 | △ | △ | X | O |
| 方便測試 | O | △ | O | O |
oj_serializer 是個不錯的選擇。
