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

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エラーを返す。

## 実装
完成形がこちらです。

```ruby: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
```
```ruby: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しています。これにより、バリデーションを行うモジュールなど様々なモジュールが使えるようになります。

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

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

ActiveModel::Conversion
```


#### attr_accessor

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

#### attr_accessorがない場合
```ruby
ActiveModel::UnknownAttributeError: unknown attribute 'category_name' for BookForm.
```

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

```ruby
  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)のところでエラーになります。

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

```ruby
def title
  @title
end
```

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

```ruby
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では、以下のように書くことができます。

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

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

```ruby
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