7839

雑草魂エンジニアブログ

【Rails】camelCase⇆snake_caseの変換

今回は久しぶりのRailsアプリケーション開発。
社内の一部業務の自動化のために簡単なアプリケーションを前任者から引継ぎ、一部改良を行った。構成はバックエンドがRuby on Railsで、フロントエンドがNuxt.jsの構成だ。

インターフェースとしてAPIを実装する中で、「jsonデータのkeyに関して、Rails側ではsnake_case、フロント側ではcamelCaseで扱いたい」と思った。フロント側で$axiosのmiddlewareでsnake_case⇆camelCaseの変換を行うことも考えたが、今回Rails側で対応を行った。その方法を備忘録として残しておく。

命名規則に関して

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 では、ブロック変数を用いるブロック渡しより、&:メソッド名渡しの方が高速だと説明されている。)

  1. &はProc coercion演算子とも呼ばれ、SymbolオブジェクトをProcオブジェクトに変換する。そのために、Symbol#to_procが呼び出される。
  2. 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の使い方

  1. gem追加
gem 'active_model_serializers'
  1. モデル生成
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
  1. 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は、RailsAPIを実装する際にとても便利なGemであるように思えた。この便利なGemのお陰で、爆速でAPIが開発できるように感じた。

もっと他にいい方法があれば、是非教えてください。よろしくお願いします。