やりたいこと
単語帳アプリにおいて、1科目に登録されてある単語を編集&削除する機能の実装
# 前提
親テーブル: 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メソッドをいじってやる方法を紹介したい
### 送られるパラメーター構造
"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
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
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
accepts_nested_attributes_for :words, reject_if: :reject_both_blank, allow_destroy: true
上のコードで、reject_both_blank関数でTrueが出たパラメータは、送信データから除外させていく。
2
def reject_both_blank(attributes)
if attributes[:id]
引数のattributesには、送られてきたそれぞれのパラメータがはいる
"0"=>{"face"=>"テスト", "flip"=>"してます", "id"=>"54"}
さっきの例では、上みたいなやつ。
もしもidキーが存在していれば、すなわち、新しく作ったインスタンスではない場合、以下の処理が行われる。
attributes.merge!(_destroy: "1") if attributes[:face].blank? and attributes[:flip].blank?
!attributes[:face].blank? and attributes[:flip].blank?
表と裏が両方空ならば、ユーザーが消したいデータということなので、削除させる仕組みを作る。attributesに入っている1つのパラメーターに_destroyキーとそれに対応する値1が入ったハッシュを追加。
3
allow_destroy: trueオプションが読み込まれ、
paramsに入っていたパラメータ一覧の中から、_destroy: 1のハッシュが入っているパラメータに該当するインスタンスに対してDelete文がSQLで流される。
これで消せます。
### 番外
else
attributes[:face].blank? and attributes[:flip].blank?
end
ちなみに引数attributesにidキーがなければ、新しく作成したパラメータということで、ユーザは新規にこの単語をwordインスタンスとして保存したいということ。よって、通常のreject_ifオプションに組み込まれる関数のような処理をたどる。
表と裏が両方空ならば、trueが帰るので、reject_ifオプションが反応し、
"3"=>{"face"=>"", "flip"=>""}
上のパラメータはサーバー側で無視され、データベースには保存されない。