LoginSignup
7
3

More than 3 years have passed since last update.

【Rails】Formオブジェクトを使ったモデルに依存しないバリデーション機能を実装

Posted at

RailsでAPIを作る際にハマった点について備忘録も兼ねて投稿したいと思います。

Formオブジェクトとは

Hiroki Zenigamiさんのブログでは下のように説明されています。

Formオブジェクトはフォームの責務をカプセル化し、コントローラやビューを疎結合に保つために必要なデザインパターンです。

ユーザの入力の整形や永続化をコントローラだけで行うと、コントローラが肥大化してしまいます。 この原因はコントローラがモデル層の知識をもちすぎるためにあります。 このときビューもフォームを表示するための知識をもつことになるため、コントローラと同じような問題が起こってしまいます。 このことは単一責任の原則に反し、モデル層の変更がコントローラやビューに影響を及ぼすことになります。

逆にActiveRecordモデルにこういった責務をもたせると、今度はActiveRecordモデルがフォームの知識を持ちすぎてしまいます。 フォームという独立した責務があるのであれば、これをひとつのクラスにカプセル化する、というのがFormオブジェクトの役割です。

簡単にざっくりとでいうとユーザの入力した値に対してバリデーションをかけたい時、コントローラーに書くと冗長になり、モデルに書いてもスマートじゃないので、バリデーション用のクラスを作ってカプセル化しておきましょうという話です。

要件

booksテーブル

カラム名 制約
title string NOT NULL
category_id integer NOT NULL

categoriesテーブル

カラム名 制約
name string NOT NULL
  • リクエストはcategory_name = ~~, title = ~~ という形で送られる。
  • リクエストのカテゴリー名がcategoriesテーブルにある場合は、そのidを利用して保存する。ない場合はcategoriesテーブルを新規作成し保存する。
  • category_nameまたはtitleが送られない場合は、422エラーを返す。

実装

完成形がこちらです。

books_controller
def create
  book = bookForm.new(book_params)

  if book.save
    render status: 201, json: { status: 201 }
  else
    render status: 422, json: { status: 422 }
  end
end
book_form.rb
class BookForm

  include ActiveModel::Model

  attr_accessor :body, :category_name

  validates :title, presence: true
  validates :category_name, presence: true

  def save
    return if invalid?

    ActiveRecord::Base.transaction do
      category = Category.find_or_create_by!(name: category_name)
      book = Book.new(title: title, category: category)
      book.save!
    end
  rescue ActiveRecord::RecordInvalid
    false
  end
end

ActiveModel::Model

ActiveModel::ModelをIncludeしています。これにより、バリデーションを行うモジュールなど様々なモジュールが使えるようになります。

ActiveModel::AttributeAssignment
# attr_accessor で定義されたキーに対して、値を設定することができる機能を提供する
# 例) Book.new(title: "7つの習慣", category_id: 1)
# これができるのはActiveModel::AttributeAssignmentの機能を使っているから

ActiveModel::Validations
# モデルの検証用の機能を提供する(バリデーションなど)

ActiveModel::Conversion

attr_accessor

ActiveModel::Modelの#initializeでは、書き込みメソッド(#title=(value)など)を用いて値を代入しています。 つまり、Formオブジェクトで用いる値は書き込みメソッドを定義する必要があります。

attr_accessorがない場合

ActiveModel::UnknownAttributeError: unknown attribute 'category_name' for BookForm.

忘れた人のために

attr_accessor :category_name, :titleは下記の省略形です。ビギナーはこう書いたほうがイメージしやすいんです。

  def title
    @title
  end

  def title=(title)
    @body = value
  end

  def category_name
    @category_name
  end

  def category_name=(value)
    @category_name = value
  end

書き込みメソッドがないと、book = BookForm.new(book_params)のところでエラーになります。

def title=(value)
  @title = value
end

読み込みメソッドがないと、少し下のbook = Book.new(title: title, category: category)のところでbodyとcategoryが読み込めずにエラーになります。

def title
  @title
end

バリデーションの検証を行う

validates :title, presence: true
validates :category_name, presence: true

これにより、ユーザが入力したtitleとcategory_nameのバリューに対してバリデーションが貼られます。これは繰り返しになりますが、ActiveModel::Modelをincludeしているため使えます。(メイン! これがやりたくてわざわざクラス作ってます)

return if invalid?

呼び出し元のインスタンスに対してバリデーションチェックを行い、falseならメソッドを中断しnilを返します。

ActiveRecord::Base.transaction do ~ end

トランザクションとは、一連の処理すべてが成功した場合にのみデータベースが更新される(コミットする)仕組みのことで、ActiveRecordでは、以下のように書くことができます。

ActiveRecord::Base.transaction do
  david.withdrawal(100)
  mary.deposit(100)
end

エラーが起きた際、rescueする

rescue ActiveRecord::RecordInvalid
  false

saveするときにバリデーションエラーが発生した場合はrescueしてfalseを返すようにします。現段階ではBookモデルにバリデーションをかけてないためなくても問題はないですが、後にBookモデル側のバリデーションを変更した際にエラーにならないよう入れています。

以上

参考

https://product-development.io/posts/rails-design-pattern-form-objects
https://api.rubyonrails.org/classes/ActiveModel/AttributeAssignment.html
https://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html
https://product-development.io/posts/rails-design-pattern-form-objects
https://naokirin.hatenablog.com/entry/2019/02/20/231317
https://gist.github.com/kyuma-git/093ac7c801df92a735e27b125112237a

7
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
3