概要
子孫オブジェクトのデータを属性値として持つ非ActiveRecordインスタンスを利用。
前提条件
モデル
親:City
子:School
孫:Student
一つの市に複数の学校があり、それぞれの学校に複数の生徒がいるという想定
# 親
class City
has_many :school
# 子
class School
has_many :students
belongs_to :city
# 孫
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行目)。
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行目)。
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 }