accepts_nested_attributes_forメソッドを使って子テーブルを編集&気に入らないレコードは削除する方法

# やりたいこと
単語帳アプリにおいて、１科目に登録されてある単語を編集&削除する機能の実装

![スクリーンショット 2018-05-14 15.54.55.png](https://qiita-image-store.s3.amazonaws.com/0/252596/53ce2591-4500-f9a3-52a9-b27c7764cc00.png)


#　前提
親テーブル: subject(科目)
子テーブル： word, 主要カラム: "face", "flip"

wordを編集するときにsubjectの編集画面に遷移させ、subjectモデルのインスタンスとwordモデルのインスタンスを同時にいじりたいので、railsのaccepts_nested_attributes_forメソッドの使用が前提

form_forメソッドにより、編集画面において、それぞれのインスタンスにはhiddenタイプのinputタグでインスタンスのidが挿入されており、ハッシュパラメータの中に格納されて送信される。

##　豆知識
allow_destroy: trueオプションをモデルのattributesメソッドに追加してやれば、_destroy: 1が付いているパラメータに該当するデータベースのインスタンスは削除される。

View側で適宜、パラメータに_destroy:1を追加してやる方法があるが、今回はreject_ifメソッドをいじってやる方法を紹介したい

###　送られるパラメーター構造
```rb

"subject"=>{
"title"=>"国語", 
"words_attributes"=>{
 "0"=>{"face"=>"テスト", "flip"=>"してます", "id"=>"54"}, // wordインスタンスの表データ
 "1"=>{"face"=>"", "flip"=>"", "id"=>"55"}, // wordインスタンスの裏面データ。この欄はView側で削除した。　こいつに該当するDBのレコードを消したい！
 "2"=>{"face"=>"明日は", "flip"=>"最高だ"}, //編集画面で新しく追加したインスタンスなのでidがない
 "3"=>{"face"=>"", "flip"=>""}}}, // 新しく作成したデータだが、何もデータが入っていない
"id"=>"35"} //subjectのid
```

#　一番伝えたいこと
今回の最大の難関は、カードを消したときにそれをどうメソッド側で認識して、Delete文をSQLで発行させるかということ。



#　答えのコード

## Model
```rb

class Subject < ApplicationRecord
  belongs_to :user
  has_many :words, dependent: :destroy
  accepts_nested_attributes_for :words, reject_if: :reject_both_blank, allow_destroy: true
  validates :title, presence: true

  def reject_both_blank(attributes)
    if attributes[:id]
      attributes.merge!(_destroy: "1") if attributes[:face].blank? and attributes[:flip].blank?
      !attributes[:face].blank? and attributes[:flip].blank?
    else
      attributes[:face].blank? and attributes[:flip].blank?
    end
  end
end
```

## Controller
```rb

before_action :set_subject, only: [:show, :edit, :update]

def update
    @subject.update(create_params)
    redirect_to "/"
end

private
def create_params
  params.require(:subject).permit(:title, words_attributes: [:face, :flip, :id, :_destroy]).merge(user_id: current_user.id)
end

def set_subject
  @subject = Subject.find(params[:id])
end
```

## Model部分の解説
### 1
```rb

accepts_nested_attributes_for :words, reject_if: :reject_both_blank, allow_destroy: true
```
上のコードで、reject_both_blank関数でTrueが出たパラメータは、送信データから除外させていく。

### 2
```rb

def reject_both_blank(attributes)
    if attributes[:id]
     
```
引数のattributesには、送られてきたそれぞれのパラメータがはいる
```
"0"=>{"face"=>"テスト", "flip"=>"してます", "id"=>"54"}
```
さっきの例では、上みたいなやつ。
もしもidキーが存在していれば、すなわち、新しく作ったインスタンスではない場合、以下の処理が行われる。
```rb

 attributes.merge!(_destroy: "1") if attributes[:face].blank? and attributes[:flip].blank?
      !attributes[:face].blank? and attributes[:flip].blank?
```
表と裏が両方空ならば、ユーザーが消したいデータということなので、削除させる仕組みを作る。attributesに入っている１つのパラメーターに_destroyキーとそれに対応する値１が入ったハッシュを追加。

### 3
allow_destroy: trueオプションが読み込まれ、
paramsに入っていたパラメータ一覧の中から、_destroy: 1のハッシュが入っているパラメータに該当するインスタンスに対してDelete文がSQLで流される。
これで消せます。

###　番外
```rb

else
      attributes[:face].blank? and attributes[:flip].blank?
    end
```
ちなみに引数attributesにidキーがなければ、新しく作成したパラメータということで、ユーザは新規にこの単語をwordインスタンスとして保存したいということ。よって、通常のreject_ifオプションに組み込まれる関数のような処理をたどる。
表と裏が両方空ならば、trueが帰るので、reject_ifオプションが反応し、
```
 "3"=>{"face"=>"", "flip"=>""}
```
上のパラメータはサーバー側で無視され、データベースには保存されない。