inSmartBank

ワンバンクを運営する株式会社スマートバンクのメンバーによるブログです

Albaへの載せ替えと3.7で追加されたTraits機能の紹介

こんにちは!スマートバンクでEMをしているmitaniです!

この記事ではRubyのJSON Serializer gemの一つであるAlbaの3.7で追加されたTraits機能について紹介します!!

私たちのチームではRailsをAPIモードで運用しており、SerializerにはActiveModelSerializers(以下、AMS)を利用しています。AMSは機能面で大きな不満はなかったものの、現在はメンテナンスモードとなっており活発な開発は行われていません。

現状、Rails 8への対応など最新バージョンへの追従は行われていますが、今後のメンテナンスへの不安や、より高性能な他のSerializerが登場してきたことなどを考慮し、Albaへ移行することにしました。

移行にあたり、既存のAMSで実装したSerializerのちょっとイケていない部分をAlbaで改善したいと考えました。具体的には、以下のような実装です。

class UserSerializer < ActiveModel::Serializer
  attribute :id
  # render_profile? が true であれば name キーを含める
  attribute :name, if: :render_profile?

  def render_profile?
    instance_options[:render_profile] || false
  end
end

class UserController < ApplicationController
  def index
    render json: User.all,
      serializer: UserSerializer,
      render_profile: current_administrator.admin?,
      status: :ok
     #=> {"id":1, "name":"スマートバンク太郎"}
  end
end

管理画面などでログインしている管理者の権限に応じて表示項目を切り替える際、これまではAMSのinstance_optionsを利用してフラグを渡し、表示内容を制御していました。

しかしこの方法では、instance_optionsに渡された引数がSerializer内部でどのように利用されるのかが追いづらく、また、キー名や値に対するバリデーションも効かないため、可読性や保守性の面で課題を感じていました。

今回、AMSから載せ替えするgemを比較検討している際、Blueprinterの Views という機能があることを知りました。

class UserBlueprint < Blueprinter::Base
  identifier :uuid
  field :email, name: :login

  view :normal do
    fields :first_name, :last_name
  end

  view :extended do
    include_view :normal
    field :address
    association :projects
  end
end

puts UserBlueprint.render(user, view: :extended)

こういう機能がAlbaにも欲しい!と思い、Albaの作者である大倉さん(okuramasafumi)に相談したところ、快くご対応いただけることになりました!

AlbaのTraits機能とは

Traitsとは、Serializerに定義されたattributesをグループ化し、呼び出し側でそのグループを利用するかどうかを指定できる機能です。この機能はAlba 3.7から利用可能です。

class User
  attr_accessor :id, :name, :email

  def initialize(id, name, email)
    @id = id
    @name = name
    @email = email
  end
end

class UserResource
  include Alba::Resource

  attributes :id

  trait :additional do
    attributes :name, :email
  end
end

user = User.new(1, 'Foo', 'foo@example.com')
UserResource.new(user).serialize # => '{"id":1}'
UserResource.new(user, with_traits: :additional).serialize # => '{"id":1,"name":"Foo","email":"foo@example.com"}'

詳細は公式ドキュメントも参照してください。

ユースケース

私たちの実際のコードベースでは、主に以下のようなケースでTraits機能を活用できます。

権限管理

冒頭で触れた、instance_optionsを使っていた権限ベースの表示切り替えは、Traitsを使うことで以下のようにシンプルに実現できます。

class UserSerializer
  include Alba::Serializer

  attributes :id

  trait :with_profile do
    attributes :name
  end
end

class UserController < ApplicationController
  def index
    render json: UserSerializer.new(User.all, with_traits: user_traits),
      status: :ok
  end
  
  private
  
  # 権限を持っている人のみプロフィールを返却するTraitを指定する
  def user_traits
    current_administrator.admin? ? :with_profile : nil
  end
end

render_profile?のような条件分岐メソッドが不要になり、コードが非常にスッキリしましたね!👍 Controllerから見たときに、どのような出力がされるのか予期しやすいのが良いです。さらに、不正なtrait名を指定すると実行時エラーが発生するため、typoなどの単純なミスにも気付きやすくなるというメリットもあります。

Serializerの部分的な共通化と切り替え

他にも、例えば関連する複数のリソースで共通のattribute群を定義しつつ、リソース特有のattributeを追加したい場合など、Serializerの部分的な共通化と動的な切り替えにTraitsを活用できます。

class DepositSerializer
  include Alba::Serializer

  attributes :id

  trait :with_convenience_store do
    one :convenience_store, serializer: ConvenienceStoreSerializer
  end

  trait :with_credit_card do
    one :credit_card, serializer: CreditCardSerializer
  end
end

class ConvenienceStoreController < ApplicationController
  def index
    # コンビニの入金情報を返したいのでwith_convenience_storeを指定
    render json: DepositSerializer.new(ConvenienceStore.all, with_traits: :with_convenience_store),
      status: :ok
  end
end

class CreditCardController < ApplicationController
  def index
    # クレカの入金情報を返したいのでwith_credit_cardを指定
    render json: DepositSerializer.new(CreditCard.all, with_traits: :with_credit_card),
      status: :ok
  end
end

APIエンドポイント(index / show)ごとのデータ切り替え

indexアクションとshowアクションのように、同じリソースでもAPIエンドポイントごとに返却するデータ構造を変えたい場合にもTraitsは有効です。

class UserSerializer
  include Alba::Serializer

  attributes :id, :name

  trait :show do
    one :profile, serializer: ProfileSerializer
    many :access_logs, serializer: AccessLogSerializer
  end
end

class UserController < ApplicationController
  def index
    render json: UserSerializer.new(User.all),
      status: :ok
  end

  def show
    # showのエンドポイントだけTraitを指定
    render json: UserSerializer.new(User.first, with_traits: :show),
      status: :ok
  end
end

まとめ

ご覧いただいたように、AlbaのTraits機能は非常に便利で、Serializerの実装をより柔軟かつ直感的にしてくれますね!

私たちのチームでは現在、AMSからAlbaへの移行を鋭意進行中です。移行の過程で見られた工夫や直面した課題、そして気になる移行後のパフォーマンス改善結果などについても、続編としてブログでお届けできればと考えていますので、ぜひご期待ください!

We create the new normal of easy budgeting, easy banking, and easy living.
In this blog, engineers, product managers, designers, business development, legal, CS, and other members will share their insights.