🐕

【Rails】rails db:seedの冪等性を保ちseedデータを安全に投入する

2024/07/16に公開

はじめに

業務で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メソッドは次のように動作します。

  1. 指定された条件に合致するレコードを検索
  2. レコードが存在する場合はそのレコードを返す
  3. レコードが存在しない場合は新しいインスタンスを作成(DBに保存はしない)

Railsのコードをみると、find_byメソッドで条件を指定して検索し、結果がfalseならnewメソッドを実行してインスタンス生成しているようです。

activerecord/lib/active_record/relation.rb
# 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データを投入する際の実装例です。

db/seeds.rb
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

コードを解説します。

  1. find_or_initialize_by: emailに基づいてユーザを検索 or インスタンス生成
  2. user.attributes = user_data: ユーザ属性を更新
  3. user.save! if ...: 新規レコード or 変更があった場合のみ保存

あとはrails db:seedコマンドを叩けば、seedデータをUserテーブルに反映できます。

また、データ投入後に、一部のseedデータを変更した場合でも、再度rails db:seedを実行すれば、エラーなく更新されます。

複数回コマンドを叩いてもデータの重複は発生しません。

パフォーマンス上の注意点

find_or_initialize_byメソッドは便利ですが、大量のデータを扱う場合はパフォーマンスに要注意です。

各レコードごとにクエリが発行されてしまうからです。

代替方法

データ投入の頻度が多かったり、あまりにも大量のデータを処理する場合は、insert_allメソッドを使った方が良いかもしれません。

※ただしinsert_allメソッドはバリデーションがスキップされてしまいます。要件に合わせて使いわけましょう。

参考:insert_allメソッドのドキュメント

まとめ

find_or_initialize_byメソッドをつかうことで、冪等性を保ちつつseedデータを安全にDBへ投入できました。

Discussion