RABLを捨てつつActiveModelSerializersに移行する
FiNCではサーバーサイドにはRuby/Railsを採用し、クライアントからのリクエストに対してJSONを返すAPIを用意しています。 これまではJSONの出力にRABLを使っていましたが、 新たに作成するAPIはActiveModelSerializersを使うように移行しました。
ActiveModelSerializersの良いところ
エラーもrubyの普通のエラーになるのでわかりやすい
従来使用していたRABLは記法が独特なため学習の必要があるのと、ドキュメントが整備されていないため学習コストがもの凄く高く、かつ制約がないため好き勝手できてしまうので、ある程度強制され、基本的にrubyで記述することのできるactive_model_seralizersへと移行しました。
また、これまでFiNCでは、各エンドポイントでいい感じに必要なデータを返すように作っており、場所によって形式が違ったり、共通でアクセスするエンドポイントに色々詰め込んで処理が重くなり、かつ後方互換性のために整理するのが難しくなる…といった問題が起きていました。 そこで現在、クライアントからのアクセスを一手に引き受けるフロントエンドサーバを立て、バックエンドはリソースを返すだけにし、クライアントからの要求に応じてフロントエンドサーバがリソースを取ってきて返す形へ移行をしています。 active_model_serializerはある程度リソースに紐付いたレスポンス形式を強制されるため、導入に踏み切ったという背景もあります。
gem 'active_model_serializers', '~> 0.10.0'
ActiveModelSerializersの基本的な考え方は、ActiveModelを継承したクラスに対して対応するシリアライザークラスを定義し、json render時にシリアライザーに書かれた通りに出力するというものです。
# == Schema Information # # Table name: posts # # id :integer not null, primary key # user_id :integer # title :string # body :string # created_at :datetime not null # updated_at :datetime not null # class Post < ApplicationRecord def first_letter title[0] end end
ActiveModelSerializersはXXXモデルに対してXXXSerializerを探しに行くので、以下のようにPostSerializerを定義します。
class PostSerializer < ActiveModel::Serializer attributes :title, :body, :first_letter end
あとはJSONでレンダリングすると、attributesに指定したものだけが出力されます。
def show render json: Post.first end => {"title":"test","body":"text body","first_letter":"t"}
A. 自動で検索する場合はそうですが、指定する場合は好きな物を使えます。
render json: Post.first, serializer: PostWithUserSerializer
Q. 配列の中の要素に対してシリアライザーを適応できますか
A. できます。配列に要素を積めた場合、配列のシリアライザーを定義する必要は無く、以下のようにeach_serailizerを指定することで、配列内の要素に対してシリアライズ方法を指定できます。
render json: Post.all, each_serializer: PostWithUserSerializer
A. できます。シリアライザーのattributesに指定したものと同名のメソッドをシリアライザークラスやモデルクラスに定義すればそれが使われます。(上記サンプル参照) また、has_manyやbelongs_toをシリアライザーに書くことで、関連先のデータも出力することができます。
class UserSerializer < ActiveModel::Serializer has_many :posts, serializer: PostSerializer end render json: User.first, serializer: UserSerializer, include: '**'
残念ながら、FiNCには大量のエンドポイントが既に存在するため、一度に移行するのは不可能でした。 (クライアントアプリからアクセスされるもので700以上)
さらに、既存のAPIを新たに書き直すには既存の形式を調べてリソースに分割、フロントエンドサーバ・クライアント側での対応などが必要になるため、現実的にはしばらくの間は既存の形式との併用が必要になります。 その場合に問題になるのが、大半を新しく作る一方で部分的に既存形式と揃えたい場合です。 RABLで書かれているのを全てActiveModelSerializersで書き直すにはコストがかかりますが、複雑なRABLだけどそもそもアクセス回数は少ない場合や、将来的にその機能は消すけど当面は使う…といった風に、実装し直すコストに見合わない場合があります。
この場合、以下のようにシリアライザ中でRABLのrenderを呼び出すことで、既存のRABLとの互換性を維持しつつActiveModelSerializersに移行することができます。 これを利用することで、エンドポイント単位ではなく、さらになだらかにRABLを捨てていくことができます。
class UserPostSerializer < ActiveModel::Serializer attributes :id, :old_post_data def old_post_data Rabl.render(object.posts, 'v1/posts/index', view_path: 'app/views/api', format: 'hash', locals: { username: object.name, next_page: true} ) end end
構造が同じだけど少し違うオブジェクトのシリアライザ
{page: 1, data: [ {username: 'aaa'...と{page: 1, data: [ {post_title: 'test'...
FiNCではエンジニアを募集しています! ドキュメントがまともにないDSLを滅ぼしたい人や、それ以外にも興味がある人はお気軽にご連絡ください!
Android → https://www.wantedly.com/projects/54470
iOS → https://www.wantedly.com/projects/59939
Webフロント → https://www.wantedly.com/projects/63233
インフラ/SRE → https://www.wantedly.com/projects/57858
AI/機械学習 → https://www.wantedly.com/projects/57462
分析/データアナリスト → https://www.wantedly.com/projects/57201
QA/テストエンジニア → https://www.wantedly.com/projects/64774
Rubyエンジニア → https://www.wantedly.com/projects/30872