【Rails】【Vue】Form Objectとの連携
はじめに
こんにちは!大ちゃんの駆け出し技術ブログです。
PF制作で使用したVueからForm Objectを使用した連携方法について記事が少なかったのでここに記事として残しておこうと思いました。
PF制作では以下のようなカードを実装したいと思いました。1つのカードにタイトルみたいなものがあり、その中に質問と回答が複数ある構成です。
まず必要な設計としてはquestion_block
みたいなモデルがあり、そしてその中にquestion_items
が3つまであるという関係です。
DBに表すと以下のようになります。
そのため、一度のフォーム入力で上記のカードを作成したいと考えていました。こんな時に必要なRailsの機能がForm Objectです!
Form Objectについての説明は割愛して、自分が上記の実装をVue × Railsどのように行なったのかを書いていきたいと思います。
実装する順番としては
- モデルの作成
↓
- コントローラーの編集
↓
- Form Objectの作成
↓
- Vueでのフォーム作成
モデルの作成
まずはモデルの作成です。
上記のDB設計をもとにモデルを作成すると以下のようになります。
# app/models/question_block.rb class QuestionBlock < ApplicationRecord # association ============= belongs_to :profile_block has_many :question_items, dependent: :destroy end
class QuestionItem < ApplicationRecord # association ============= belongs_to :question_block end
※ モデルの中にあるprofile_block
というのは、profile_block
has_many
question_blocks
の関係でアソシエーションを結んでいるだけの関係にある親モデルです。
コントローラーの編集
今回は親であるquestion_block
の作成機能を使用します。
# app/controllers/api/v1/question_blocks_controller.rb module Api module V1 class QuestionBlocksController < ApplicationController def create @question_block_item_register = QuestionBlockItemRegister.new(set_params) if @question_block_item_register.save render json: @question_block_item_register else render json: @question_block_item_register.errors, status: :bad_request end end private def set_params params.permit( :question_title, :question_item_content1, :question_item_answer1, :question_item_content2, :question_item_answer2, :question_item_content3, :question_item_answer3, :current_user).merge(profile_block_id: ProfileBlock.find_by(user_id: User.find(current_user.id)).id) end end end end
set_params
の箇所で複数のquestion_item
のパラメータを受け取ることができるようになります。
def set_params params.permit( :question_title, :question_item_content1, :question_item_answer1, :question_item_content2, :question_item_answer2, :question_item_content3, :question_item_answer3, :current_user).merge(profile_block_id: ProfileBlock.find_by(user_id: User.find(current_user.id)).id) end
最後にmerge
している箇所ですが、これはForm Objectの中ではcurrent_user
を使用することができないため、パラメータとして渡しておく必要があるためです。自分はdeviseを使用しているため、current_user
を使用することで、question_block
の親であるprofile_block
を紐づけるためのprofile_block_id
のパラメータを渡すことができます。
また、@question_block_item_register.attributes
とすることで、JSONの値を取得できます。
Form Objectの作成
Form Objectを作成するにあたり、必要そうなものをまずはinclude
します。QuestionBlockItemRegister
は仮のモデルとなるため、ActiveModelをincludeしておく必要が会います。これにより、バリデーション機能など各モデルの機能をForm Object内で使用することができます。
# app/forms/question_block_item_register.rb class QuestionBlockItemRegister include ActiveModel::Model include ActiveModel::Attributes include ActiveModel::Validations end
次にパラメータを受け取る値をattribute
を使用してForm Object内で定義します。これはコントローラーで指定した各パラメーターの値の定義です。
# app/forms/question_block_item_register.rb class QuestionBlockItemRegister include ActiveModel::Model include ActiveModel::Attributes include ActiveModel::Validations attribute :question_title, :string attribute :question_item_content1, :string attribute :question_item_answer1, :string attribute :question_item_content2, :string attribute :question_item_answer2, :string attribute :question_item_content3, :string attribute :question_item_answer3, :string attribute :profile_block_id, :integer end
次にバリデーションも定義してしまいましょう。これもモデルで定義する時と同じように設定できます。question_item_answer1
以降のpresence: true
を指定しない理由ですが、question_block
が持つitem
の数は必ずしも3つではなく、2つ、1つでも登録できるようにするためです。
# app/forms/question_block_item_register.rb class QuestionBlockItemRegister include ActiveModel::Model include ActiveModel::Attributes include ActiveModel::Validations attribute :question_title, :string attribute :question_item_content1, :string attribute :question_item_answer1, :string attribute :question_item_content2, :string attribute :question_item_answer2, :string attribute :question_item_content3, :string attribute :question_item_answer3, :string attribute :profile_block_id, :integer # validation ============= validates :question_title, presence: true, length: { maximum: 30 } validates :question_item_content1, presence: true, length: { maximum: 30 } validates :question_item_answer1, presence: true, length: { maximum: 30 } validates :question_item_content2, length: { maximum: 30 } validates :question_item_answer2, length: { maximum: 30 } validates :question_item_content3, length: { maximum: 30 } validates :question_item_answer3, length: { maximum: 30 } validates :profile_block_id, presence: true end
最後にDBに登録する際のトランザクションを記述します。
# app/forms/question_block_item_register.rb class QuestionBlockItemRegister include ActiveModel::Model include ActiveModel::Attributes include ActiveModel::Validations attribute :question_title, :string attribute :question_item_content1, :string attribute :question_item_answer1, :string attribute :question_item_content2, :string attribute :question_item_answer2, :string attribute :question_item_content3, :string attribute :question_item_answer3, :string attribute :profile_block_id, :integer # validation ============= validates :question_title, presence: true, length: { maximum: 30 } validates :question_item_content1, presence: true, length: { maximum: 30 } validates :question_item_answer1, length: { maximum: 30 } validates :question_item_content2, length: { maximum: 30 } validates :question_item_answer2, length: { maximum: 30 } validates :question_item_content3, length: { maximum: 30 } validates :question_item_answer3, length: { maximum: 30 } validates :profile_block_id, presence: true def save return if invalid? ActiveRecord::Base.transaction do question_block = QuestionBlock.create!(title: question_title, profile_block_id: profile_block_id) question_block.question_items.create!(content: question_item_content1, answer: question_item_answer1) if question_item_content1.present? question_block.question_items.create!(content: question_item_content2, answer: question_item_answer2) if question_item_content2.present? question_block.question_items.create!(content: question_item_content3, answer: question_item_answer3) if question_item_content3.present? end true end end
save
メソッドについてはクラスメソッドとなるので、先ほどcreate
アクション内で使用しているメソッド名と同じにします。ここは、普段使用しているDB保存のためのsave
メソッドとは異なるのでお間違いなく。
def create @question_block_item_register = QuestionBlockItemRegister.new(set_params) if @question_block_item_register.save render json: @question_block_item_register.attributes else render json: @question_block_item_register.errors, status: :bad_request end end
ActiveRecord::Base.transaction
を使用する理由ですが、これはトランザクションを一つにまとめることができる機能です。なぜまとめる必要があるのかというと、create!
メソッドは使用された時点で成功すればDBに登録されますが、一度に複数モデルを作成するForm Objectでは複数モデルが全て登録されたことを保証するためです。もし、仮にQuestionBlock.create!
が成功して、question_block.question_items.create!
が失敗すると、itemのないただのブロックが作成されてしまいます。
ActiveRecord::Base.transaction do question_block = QuestionBlock.create!(title: question_title, profile_block_id: profile_block_id) question_block.question_items.create!(content: question_item_content1, answer: question_item_answer1) if question_item_content1.present? question_block.question_items.create!(content: question_item_content2, answer: question_item_answer2) if question_item_content2.present? question_block.question_items.create!(content: question_item_content3, answer: question_item_answer3) if question_item_content3.present? end
また、トランザクションが完了した後にtrue
を返却しなければなりません。なぜかというと、@question_block_item_register.save
は普段使用しているsave
メソッドと同様に、真偽値を返す必要があるためです。true
を返却しなければnil
が返却されることになり、必ず条件分岐はelse
の結果を返すことになります。
def save return if invalid? ・ ・ true # 必ず必要 end
Vueでのフォーム作成
最後にvueでパラメータを送ってあげます。Vueのtemplate
タグ内は自分のPF上の見た目を表すコードになるため割愛します。どうやってaxiosを使用してパラメータを渡すかのみを使用します。
まずフォームのコンポーネントのscript
タグ内は以下のようになります。
<script> export default { data() { return { questionBlock: { title: "", }, questionItem: { content: "", answer: "", }, }; }, methods: { ...mapActions({ createQuestionBlock: "questionBlocks/createQuestionBlock", }), hundleCreateQuestionBlock(questionBlock, questionItem) { if (questionBlock.title == "" || questionItem.content == "") return; this.createQuestionBlock({ questionBlock: questionBlock, questionItem: questionItem, }); }, }, }; </script>
重要なのはvuexにて定義しているcreateQuestionBlock
メソッドの引数をオブジェエクトを使用して渡していることです。なぜかというと、vuexのactionsのメソッド、mutationsのメソッドは引数を一つしか持てないためです。そのため、オブジェクトの中に入れることで、引数を一つに保ちます。
...mapActions({ createQuestionBlock: "questionBlocks/createQuestionBlock", }), hundleCreateQuestionBlock(questionBlock, questionItem) { ・ ・ this.createQuestionBlock({ questionBlock: questionBlock, questionItem: questionItem, });
次にvuexで非同期処理を行うcreateQuestionBlock
メソッドについてです。
createQuestionBlock({ commit }, { questionBlock, questionItem }) { const params = { question_title: questionBlock.title, question_item_content1: questionItem.content, question_item_answer1: questionItem.answer, }; axios .post("question_blocks", params) .then((response) => { commit("addQuestionBlock", response.data); }) .catch((err) => console.log(err.status)); },
引数をオブジェクトを使用して表しています。ここでサーバー側で受け取るパラメータを定数params
として定義します。
const params = { question_title: questionBlock.title, question_item_content1: questionItem.content, question_item_answer1: questionItem.answer, };
あとはそのパラメータをpost
として送信するだけです。
post("question_blocks", params)
少し雑だったかもしれませんが、全部読めば外観はつかめたのではないかなと思います。特にVue側でのparamsの値に変換しておく方法はあまり記事では見かけませんでした。
const params = { question_title: questionBlock.title, question_item_content1: questionItem.content, question_item_answer1: questionItem.answer, };
この記事がRails × Vueの実装で誰かのお役に立てればと思います。
参考文献
- フォームオブジェクトについて
- Vuexでのaction, mutationの引数指定での注意点