[Vue→Rails]ネストされた状態の親子孫オブジェクト(一対多の関係あり)を全て同時にデータベースに保存する


概要

子孫オブジェクトのデータを属性値として持つ非ActiveRecordインスタンスを利用。

前提条件

モデル

親:City
子:School
孫:Student
一つの市に複数の学校があり、それぞれの学校に複数の生徒がいるという想定

city.rb
# 親
class City
 has_many :school
school.rb
# 子
class School
 has_many :students
 belongs_to :city
student.rb
# 孫
class Student
 belongs_to :school

Vue(フロントエンド側)で生成されるオブジェクト

// the city has 2 schools
// each school has 3 students

{ 
  city: {
    cityName: 'tokyo',
    schools: [
       {  schoolName: 'first',
          students: [
             { studentName: 'Suzuki', age: 13 },
             { studentName: 'Hirano', age: 14 },
             { studentName: 'Nagai',  age: 15 }
          ]
       },
       {  schoolName: 'second',
          students: [
             { studentName: 'Sato', age: 13 },
             { studentName: 'Iguchi', age: 14 },
             { studentName: 'Arai',  age: 15 }
          ]
       },
    ]
  }
}

axiosのpostメソッドでバックエンドに送る。

コントローラ

ストロングパラメータの記述

cityの一つの属性としてschoolsを指定する。配列データであるschoolsは[]で表し、中にschoolsのパラメータを記述する。
さらに、schoolsの一つのパラメータとしてstudentsを指定する。同様に配列データであるstudentsを[]で表し、中にstudentsのパラメータを記述する。

createアクションの記述

CityObjectインスタンス(次章で解説)をビルドし、パラメータを渡す(2行目)。
CityObjectのインスタンスメソッド'save'(次章で解説:ActiveRecordのsaveメソッドではない)内で親子孫それぞれのモデルにおいてレコードの保存を行う(4行目)。

cities_controller.rb
  def create
    city = CityObject.new(city_params)

    if city.save
      render json: city
    else
      render json: city.errors.full_messages, status: :unprocessable_entity
    end
  end

  private

  def city_params
    params.require(:city).permit(:cityName, schools: [:schoolName, students: [:studentName, :age]])
  end

子孫オブジェクトのデータを属性値として持つ非ActiveRecordインスタンスの利用

City,School,Studentのパラメータを持つ非ActiveRecordクラス'CityObject'を定義する。
コントローラでCityObjectのインスタンスにパラメータが渡されているので、インスタンスメソッド'save'内でそのパラメータにアクセスし、City,School,Studentそれぞれのインスタンスに渡して保存を行う。

ActiveModelモジュールの利用

CityObjectクラスを定義し、ActiveModel::ModelをインクルードすることでActiveRecordモデルと同様の処理を可能にする(1,2行目)。
それとともにActiveModel::Attributesをインクルードし、attributeメソッドによりCityObjectインスタンスに渡されたパラメータを参照できるようにする(3行目)。
親オブジェクトの各属性を指定し(5行目)、子オブジェクトを属性として指定する(6行目)。

CityObjectのインスタンスメソッド'save'の定義

関連のある複数のモデルで同時にレコード保存を行うので、一部のレコードのみ作成されることがないよう一つのトランザクションでまとめる(9行目)。
CityObjectの属性値'cityName'にアクセスし、新しいCityインスタンスの属性値'city_name'としてセットし、それを保存する(11行目)。
schools(selfは省略可)でschoolオブジェクトの配列を取得し、一つ一つのオブジェクトについて以下のループ処理を行う(14行目)。

  • schoolオブジェクトの'schoolName'の値をSchoolインスタンスの属性値'school_name'としてセットし、先程保存したCityインスタンスの子オブジェクトとして保存する(15行目)。
  • sc[:students]でschoolオブジェクトが持つstudentオブジェクトの配列を取得し、一つ一つのオブジェクトについて以下のループ処理を行う(16行目)。

    • studentオブジェクトの'studentName','age'の値をStudentインスタンスの属性値'student_name','age'として渡し、先程保存したSchoolインスタンスの子として保存する(18行目)。
app/models/city_object.rb
class CityObject
  include ActiveModel::Model
  include ActiveModel::Attributes

  attribute :cityName
  attribute :schools

  def save
    ActiveRecord::Base.transaction do
      # Cityインスタンスの保存
      city = City.create(city_name: self.cityName)

      # Schoolインスタンスの保存
      self.schools.each do |sc|
        school = city.schools.create(school_name: sc[:schoolName])
        sc[:students].each do |st|
          # Studentインスタンスの保存
          school.students.create(student_name: st[:studentName], age: st[:age])
        end
      end
    end
  end
end

保存されるレコード

City
{ id: 1, city_name: 'tokyo' }

School
{ id: 1, school_name: 'first',  city_id: 1 }
{ id: 2, school_name: 'second', city_id: 1 }

Student
{ id: 1, student_name: 'Suzuki', age: 13, school_id: 1 }
{ id: 2, student_name: 'Hirano', age: 14, school_id: 1 }
{ id: 3, student_name: 'Nagai',  age: 15, school_id: 1 }
{ id: 4, student_name: 'Sato',   age: 13, school_id: 2 }
{ id: 5, student_name: 'Iguchi', age: 14, school_id: 2 }
{ id: 6, student_name: 'Arai',   age: 15, school_id: 2 }