objects_from_params 他2件: 小林研 Rails Tips (19)

はじめに

Rails Tips の 19 回目です。今回はコントローラでモデルを取得するときの記述を減らすためのヘルパメソッドです。パラメータの渡し方で 3 種類のメソッドがあるので順に説明します。

params[:id] から取得

showedit などでは、params[:id] で指定されたモデルを読み込みます。例えば、Gnumber モデルのオブジェクトを取得するには通常以下のように書くでしょう。

@gnumber = Gnumber.find params[:id]

これを以下のように記述できるようにしました。

@gnumber = object_from_params_id Gnumber

このメソッドは applicatoin_controller.rb に以下のように記述してあります。params[:id] が存在した場合だけ、model からオブジェクトを取得します。try なので存在しなかった時にもエラーは出ません。

  # @param [Class] model モデルクラス
  # @return [Object] オブジェクト
  def object_from_params_id(model)
    params[:id].try { |id| model.find id }
  end

params[:model_id] から取得 (複数個も可)

クラスの中の学生を参照するようなときにはkurasus/:kurasu_id/gnumbers/:id のようなネストされたパスが指定されることがあります。このような場合、クラスは以下のように取得するでしょう。

@kurasu = Kurasu.find params[:kurasu_id]

これを以下のように記述できるようにしました。

@kurasu = objects_from_params Kurasu

クラスだけでなく、キャンパスも同時に取得したいとします。これらをたくさん書くのは莫迦らしいので、以下のように書けるようにしています。メソッド名が objects で始まるのはそういう理由です。

@kurasu, @campus = objects_from_params Kurasu, Campus

最後の部分でこれを踏まえた実装は以下のようになります。params のキーはクラス名から underscore を使って作成しています。このメソッドは少し工夫があって、モデルが一つの時には最初に示したように単体のオブジェクトが返るようにしています。いちいち配列の最初の値だけ取り出すのは面倒だからです。

  # @params [Array<Class>] models
  # @return [Object, Array<Object>] 複数の場合、オブジェクトの配列、一つの場合、オブジェクト
  # @note モデルクラスの配列を渡し、params に入っているデータを取得する
  def objects_from_params(*models)
    ans = models.map do |m|
      params["#{m.to_s.underscore}_id"].try { |id| m.find id }
    end
    ans.count == 1 ? ans.first : ans
  end

params[:another_id] から取得

ほとんどが上の二つだけで済むのですが、たまに model 名とは異なる id でオブジェクトを取得しなければならないことがありました。例えば、表示年度を取得するために、Year モデルからview_year_id のようなキーで取得する場合には、以下のようにしていました。

@view_year = Year.find params[:view_year_id]

これも書くのが面倒なので、以下のように取得できるようしています。

@view_year = object_from_params_another_id Year, :view_year_id

これは object_from_params_id とほぼ同様ですが、標準とは違うキー名 (another_id) を追加で渡さなければいけない部分だけが違います。

  # @param [Class] model
  # @param [Symbol] another_id
  # @return [Object] オブジェクト
  def object_from_params_another_id(model, another_id)
    params[another_id].try { |id| model.find id }
  end

おわりに

実際のコントローラでは、以下のように take_base や take_one を使って共通部分を括り出してしまうので、それほどの記述短縮にはならないです。それでも雛形にこれらを用意して置けるのは便利かなと思って使い続けています。

  before_action :take_base
  before_action :take_one, only: %i[show edit update destroy]

  (中略)
  private
  def take_base
    @kurasu = objects_from_params Kurasu
  end

  def take_one
    @gnumber = object_from_params_id Gnumber
  end