【Rails】rails db:seedの冪等性を保ちseedデータを安全に投入する
はじめに
業務でseedデータを投入する機会がありました。
似たようなタスクがあった際に忘れないよう、seedデータを安全に、そして何度実行しても同じ結果になるよう投入する方法をまとめます。
冪等性とは?
冪等性(べきとうせい)とは、おなじ操作を何度実行してもおなじ結果になる特性のことです。
seedデータの投入以外に、POST通信のAPIの実装などでも考慮する必要があります。
seedデータを投入する際の問題
上記で触れたように、seedデータを作成する際はrails db:seed
を何度実行しても結果が変わらないように実装する必要があります。
以下のような事態は避けなくてはいけません。
- 複数回の実行によりデータが重複してしまう
- 2回目以降の実行時にエラーが発生する
しかしどのように実現すればいいのか、少し手こずりました、、。
find_or_initialize_by
メソッド
解決策: ActiveRecordで提供するfind_or_initialize_by
メソッドを使うことで、この問題を解決することができます。
find_or_initialize_by
メソッドの挙動
find_or_initialize_by
メソッドは次のように動作します。
- 指定された条件に合致するレコードを検索
- レコードが存在する場合はそのレコードを返す
- レコードが存在しない場合は新しいインスタンスを作成(DBに保存はしない)
Railsのコードをみると、find_by
メソッドで条件を指定して検索し、結果がfalse
ならnew
メソッドを実行してインスタンス生成しているようです。
# Like #find_or_create_by, but calls {new}[rdoc-ref:Core#new]
# instead of {create}[rdoc-ref:Persistence::ClassMethods#create].
def find_or_initialize_by(attributes, &block)
find_by(attributes) || new(attributes, &block)
end
実装例:Userテーブルへのseedデータ投入
Userテーブルへseedデータを投入する際の実装例です。
users_data = [
{ email: 'user1@example.com', name: 'Alice' },
{ email: 'user2@example.com', name: 'Bob' }
]
users_data.each do |user_data|
user = User.find_or_initialize_by(email: user_data[:email]) # 1
user.attributes = user_data # 2
user.save! if user.new_record? || user.changed? # 3
end
コードを解説します。
-
find_or_initialize_by
:email
に基づいてユーザを検索 or インスタンス生成 -
user.attributes = user_data
: ユーザ属性を更新 -
user.save! if ...
: 新規レコード or 変更があった場合のみ保存
あとはrails db:seed
コマンドを叩けば、seedデータをUserテーブルに反映できます。
また、データ投入後に、一部のseedデータを変更した場合でも、再度rails db:seed
を実行すれば、エラーなく更新されます。
複数回コマンドを叩いてもデータの重複は発生しません。
パフォーマンス上の注意点
find_or_initialize_by
メソッドは便利ですが、大量のデータを扱う場合はパフォーマンスに要注意です。
各レコードごとにクエリが発行されてしまうからです。
代替方法
データ投入の頻度が多かったり、あまりにも大量のデータを処理する場合は、insert_all
メソッドを使った方が良いかもしれません。
※ただしinsert_all
メソッドはバリデーションがスキップされてしまいます。要件に合わせて使いわけましょう。
まとめ
find_or_initialize_by
メソッドをつかうことで、冪等性を保ちつつseedデータを安全にDBへ投入できました。
Discussion