ボクココ

個人開発に関するテックブログ

Rails で複数モデルに関連したパラメータを検証する方法

ども、@kimihom です。

今回は Ruby on Rails における パラメーターの処理について。

よくある Rails の Scaffold では、送られてくるパラメーターとモデルが 1対1 で対応しているため、 Strong Parameter を使って綺麗に CRUD(Create, Read, Update, Delete) 処理ができている。ただ、実際は何かの登録フォームなどで関連するモデルを一括でフォームから登録する、という場合など多くあるかと思う。そんな時は Rails のレールから外れるから仕方なく汚いコード(特に検証のコード)を書いてしまった経験は一度や二度はあるだろう。しかし、実は Rails はそんな時でも綺麗に 書くことのできる方法を提供している。これは、データ保存を必要としないモデルを作成し、そこに ActiveModel をインクルードするのだ。これにより、DB保存はしないけど、Rails の Model と同等の検証機能を利用できる。

ではサンプルを見ていこう。

ActiveModel な Model を作成する

今回のサンプルは、会員登録で一気に複数のモデルを一括作成するようなケースを考えてみる。このような時は User 単体の Create 処理だけじゃ物足りず、その他サービスを利用する上で重要なデータの初期化などを行うことだろう。そんな時のパラメーターと Model に渡すパラメータは複数であることが多い。

通常の Model といえば、ActiveRecord::Base を継承したクラスを作成する。今回は各 Model は既に定義されているとして、パラメータ用のモデルActiveModel::Model をインクルードした RegisterParam を作成する。

今回は例えば契約情報(Contract)クラスがあり、その中に複数のユーザー(User)クラスがあるようなパターンを考えてみる。

class RegisterParam
  include ActiveModel::Model

  attr_accessor  :name, :email, :email_confirm, :tel1, :tel2, :tel3, :password, :password_confirm

  validates :name, presence: true, length: {maximum: 255}
  validates :email, presence: true, length: {maximum: 255}, confirmation: true
  validates :password, presence: true, length: {in: 8..50}, confirmation: true
  validates :tel1, presence: true, numericality: true, format: { with: /\A0\d+\Z/i }
  validates :tel2, presence: true, numericality: true
  validates :tel3, presence: true, numericality: true

  def initialize(attributes={})
    super
  end

  def contract_param
    {
      name: self.name,
      email: self.email,
      tel_number: [self.tel1, self.tel2, self.tel3].join("-")
    }
  end

  def user_param
    {
      email: self.email,
      password: self.password,
      password_confirmation: password_confirmation
    }
  end
end

このようにすることで嬉しい点が大きく2つある。

1つ目は Rails の バリデーション機能を複数のモデルが関連した時でも利用できる、ということ。実際にコントローラ側で検証するには、validate メソッドが利用できる。通常のバリデーションと同様に、エラーが発生した場合はメッセージを自動で生成してくれる。

2つ目は View 側で form_tag ではなく form_for が利用できる点だ。 form_tag は特に何も気にせずに form をかけて手っ取り早いんだけども、データのValue 値など全てinput要素毎に書かないといけない。また、label も同様に自分で書く必要がある。form_tagは UIの変更に非常に弱いのだ。しかし、 form_for を使えばそれらの苦労から解放される。以下のようにコントローラを記述すればform_for が View 側で利用できる。

どちらも、 Rails のレールに乗ることができる というのが最大のメリットである。

  def new
    @register = RegisterParam.new
  end

  def create
    ActiveRecord::Base.transaction do
      @register = RegisterParam.new(register_param)
      raise ActiveRecord::RecordInvalid.new(@register) unless @register.validate
      
      @contract = Contract.create!(@register.contract_param)
      @user = @contract.users.create!(@register.user_param)
    end
    
    rescue => e
       # 例外処理
       @error = e.message
    end
  end

  private
    def register_param
    params.require(:register_param).permit(
      :name,
      :password,
      :password_confirmation,
      :tel1,
      :tel2,
      :tel3,
      :email,
      :email_confirmation
    )
  end
  <%= form_for @register do |f| %>
    <%= f.label :name %>
    <%= f.text_field :name %>

    <!-- ..... -->

    <%= f.submit %>
  <% end %>

通常のモデル処理と同様、validate メソッドの検証に失敗した場合は、@registererror オブジェクトが入ってくるので、エラー箇所などを適切に表現することが可能だ。もちろん 今回のモデルは DB とは連携していないので、save メソッドなどは利用できない。

ちなみにエラーメッセージを出す時のフィールド名も 通常の Model と同様、 config/locales/ja.yml に置いておけば適切に見てくれる。その際、キーはactiverecordではなくactivemodel であることに注意しよう。

  activemodel:
    attributes:
      register_param:
        name: 氏名
        tel1: 電話番号1
        tel2: 電話番号2
        tel3: 電話番号3
        email: メールアドレス
        email_confirmation: メールアドレス確認

終わりに

このように複雑なパラメータ処理でも、美しいコーディングを実現することができた。 Rails のレールに乗っている時の気分の良さはたまらないものがある。しっかりと Rails を理解して書いているという満足感と美しくコードを書くことができたという誇らしさ、そしてそれがメンテンス性、テストの書きやすさにつながってくる。

初心者ほど Rails のレールを踏み外すことが多い。より良いコードを書くために、Rails のレールに乗せることを再度考えてみよう。