【Rails】camelCase⇆snake_caseの変換
今回は久しぶりのRailsアプリケーション開発。
社内の一部業務の自動化のために簡単なアプリケーションを前任者から引継ぎ、一部改良を行った。構成はバックエンドがRuby on Railsで、フロントエンドがNuxt.jsの構成だ。
インターフェースとしてAPIを実装する中で、「jsonデータのkeyに関して、Rails側ではsnake_case、フロント側ではcamelCaseで扱いたい」と思った。フロント側で$axiosのmiddlewareでsnake_case⇆camelCaseの変換を行うことも考えたが、今回Rails側で対応を行った。その方法を備忘録として残しておく。
- Ruby:v3.0.1
- Ruby on Rails : v6.1.3.2
- active_model_serializers:v0.10.12
命名規則に関して
Rubyの命名規則
復習も兼ねて、Rubyスタイルガイドから命名規則について列挙しておく。
- シンボル、メソッド、変数には
snake_case
を用いる- 文字と数字を分離しない(×:var_10, ○:var10)
- 述語(boolean値が返る)メソッドは疑問符で終わる(ex.empty?)
- 危険な可能性(破壊的な変更)のあるメソッドは感嘆符で終わる(ex.update!)
- クラスやモジュールには
CamelCase
を用いる - 定数は
SCREAMING_SNAKE_CASE
を用いる
JavaScriptの命名規則
復習も兼ねて、Google JavaScript Style Guideから命名規則について列挙しておく。
- メソッド、変数には(先頭小文字の)
camelCase
を用いる - クラスには(先頭大文字の)
CamelCase
を用いる - 定数は
SCREAMING_SNAKE_CASE
を用いる
RequestのkeysをcamelCaseからsnake_caseへ変換
Requestに関しては、before_action
でparamsを変換するように、以下の処理を追加した。
class ApplicationController < ActionController::API before_action :snake2camel_params def snake2camel_params params.deep_transform_keys!(&:underscore) end end
deep_transform_keys!
は、ハッシュに対して、ブロック内で変換されたキーを含むハッシュに変換してくれる破壊的メソッドである。ソースコードは以下のようになっている。
class Hash # 〜省略〜 # Destructively converts all keys by using the block operation. # This includes the keys from the root hash and from all # nested hashes and arrays. def deep_transform_keys!(&block) _deep_transform_keys_in_object!(self, &block) end # 〜省略〜 private # Support methods for deep transforming nested hashes and arrays. def _deep_transform_keys_in_object!(object, &block) case object when Hash object.keys.each do |key| value = object.delete(key) object[yield(key)] = _deep_transform_keys_in_object!(value, &block) end object when Array object.map! { |e| _deep_transform_keys_in_object!(e, &block) } else object end end end
underscore
でkeyをcamelCaseからsnake_caseへ変換している。ソースコードは以下のようになっている。
class String # 〜省略〜 def underscore ActiveSupport::Inflector.underscore(self) end end
module ActiveSupport module Inflector # 〜省略〜 def underscore(camel_cased_word) return camel_cased_word unless /[A-Z-]|::/.match?(camel_cased_word) word = camel_cased_word.to_s.gsub("::", "/") word.gsub!(inflections.acronyms_underscore_regex) { "#{$1 && '_' }#{$2.downcase}" } word.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2') word.gsub!(/([a-z\d])([A-Z])/, '\1_\2') word.tr!("-", "_") word.downcase! word end end end
実装方法に関して
&:underscore
はブロック変数ではなく、& + Symbolオブジェクト
を用いている。(Use fast ruby idioms by oniofchaos · Pull Request #32337 では、ブロック変数を用いるブロック渡しより、&:メソッド名渡しの方が高速だと説明されている。)
&
はProc coercion演算子とも呼ばれ、SymbolオブジェクトをProcオブジェクトに変換する。そのために、Symbol#to_procが呼び出される。- Symbol#to_procでは、ブロック引数に対してSymbolオブジェクトのメソッドが呼び出される。
params.deep_transform_keys!(&:underscore) # 同じ実装である params.deep_transform_keys!{|key| key.underscore}
(復習)RubyのSymbolとは何か?
Rubyの内部実装では、メソッド名や変数名、定数名、クラス名などの`名前'を整数で管理しています。これは名前を直接文字列として処理するよりも速度面で有利だからです。そしてその整数をRubyのコード上で表現したものがシンボルです。
Responseのkeysをsnake_caseからcamelCaseへ変換
jsonデータへの整形も含め、Active Model Serializersを用いた。その場合、initializersに以下のファイルを追加するだけでsnake_caseからcamelCaseへ変換することができた。
ActiveModelSerializers.config.key_transform = :camel_lower
公式ドキュメントは以下を参考にして欲しい。
ActiveModelSerializersの使い方
- gem追加
gem 'active_model_serializers'
- モデル生成
rails g serializer <-ModelName-> ex. rails g serializer User
class UserSerializer < ActiveModel::Serializer attributes :id, :name, :email attribute: :password, if: -> {instance_options[:password]} has_many :posts has_many :tasks end
- controllerにシリアライザーを指定
class UsersController < ApplicationController def index users = User.all.includes(:posts) render json: users, each_serializer: UserSerializer, #複数の場合、each_serializer include: ['posts'] #アソシエーションを指定 end def show users = User.find(params[:id]) render json: users, serializer: UserSerializer, #単数の場合、serializer password: true #オプション指定 end end
アソシエーションを指定する場合、N+1問題が発生する場合があるので、includes
メソッドを使うなど対応が必要となる。
また、アソシエーションを指定することでActiveModelSerializers側でクエリを発行することもできるのでパフォーマンスなどはよく確認する必要があるように思えた。
まとめ
ActiveModelSerializersは、RailsでAPIを実装する際にとても便利なGemであるように思えた。この便利なGemのお陰で、爆速でAPIが開発できるように感じた。
もっと他にいい方法があれば、是非教えてください。よろしくお願いします。