🦁

[Rails]メンテされなくなったseed_fuの代わりとなるseed.rbの実装案

2023/06/26に公開

概要

Railsのseed用のgemではseed_fuというのがかなりメジャーでした。
しかし、最終更新が2018年となっており5年以上メンテされていないのにも関わらず現在でも多くの紹介記事が見られます。
この記事では、メンテされなくなったseed_fuの代わりとなるseedの実装方法を紹介します。

本題の前に

そもそもメンテが止まっていると何が問題なのか

以下のような可能性が想定されます。

  • セキュリティリスク(脆弱性が発見されたとき修正されない)
  • 不具合が発見されたとき修正されない
  • Rails, Rubyバージョンとの互換性(gem内のメソッドがdepricatedになる可能性など)

こういったことから使用するにあたって、少なくともメンテが継続されているものよりはリスクがあるといえるでしょう。deprecatedの警告が出ているメソッドを使っている状態に近いと言えるかもしれません。

seed_fuに限らずこういったメンテが止まっているgemというのは意外と多く、見過ごされがちです。seed_fuについては、今のところそういった大きな問題が出ていないようである(ちゃんと確認はしていません)ことや、過去にかなりおすすめされてきたgemであることから現在でも使われることが多いようです。

既存システムで使っているgemがメンテが継続されていないことに気付いた時、それを即座に置き換えなければならない、というほどではないかもしれませんが新しいプロジェクトで採用するのはできれば控えたいところです。

更新日時はどこを確認すれば良いの?

gitリポジトリの最終更新日時を見ましょう。
https://github.com/mbleigh/seed-fu

最終更新がREADMEやコメントの修正であることもあるのでできればこの隣のcommitsから直近のcommitを確認できると万全です。

どれくらい過去なら問題なの?

よく耳にするのは1年以内に更新があるかどうかですが、一概に更新が止まっている期間で問題があるかどうかを判断することは難しく、gemが持つ機能によっても変わってくるでしょう。

1年以上更新がなくても安定したgemであれば問題ない場合がありますし、セキュリティ的にセンシティブな部分の機能を持つgemであれば半年以上更新がないだけでも少し危険だと考えても良いかもしれません。

seed_fuの場合、一見安定しているようには見えますが長期間メンテがされなくなってからここまで使われ続けているというのはかなり珍しいのではないでしょうか。

seed_fuを使わない場合の実装方針案

db/seed.rbの書き方はそこまで厳密にルールがなく実装方法はプロジェクトによってまちまちです。望ましい実装方針を挙げるとすれば、以下のような点になるでしょう。

  • yamlなどでテーブル毎にデータ(attributes)ファイルを分離して持つ
  • 冪等性を持たせる(何度実行してもエラーにならないようにしたり、データが重複しないようにする)
  • 環境ごとに異なるデータを入れられるようにする
  • 処理をベタ書きするのではなくクラス・メソッド定義してそれをコールする形にする
    • サービスクラス用の実装ルールがある場合、それに準じた実装にすると読みやすくなる

実装サンプル

以下は私がよく使うseedのベースとなる実装サンプルです。
※記事用に修正を入れているので動作保証はありません。

# frozen_string_literal: true

class Seeder
  BASE_PATH = Rails.root.join('db/seeds/base/*.yml')
  # 実行時の環境名を取得
  ENV_PATH = Rails.root.join('db/seeds/', Rails.env.downcase, '*.yml')
  PATHS = [BASE_PATH, ENV_PATH].freeze

  class << self
    def call
      target_files.each do |file|
        attributes = YAML.safe_load(File.read(file), [], [], true)

        file_name = File.basename(file)
        model = file_name.singularize.classify.constantize
        model.upsert_all(attributes)
      end
    end

    private

    def target_files
      files =
        PATHS.map do |path|
          Dir.glob(path).map do |file|
            file
          end
        end
      files.flatten
    end
  end
end
Seeder.call

実行時に現在の環境名を取得し、そのディレクトリ配下のデータファイルからattributesを取得します。データファイルのファイル名はテーブル名とします。

ディレクトリ構造

db┬seed.rb
 └seeds
  ├base どの環境にも入れるデータフォルダ
  ├production
  ├staging
  └develop

call内で使っているメソッド

  • singularize 複数形の名詞を単数形に変換
  • classify スネークケース文字列を、クラス名用のアッパーキャメルケース文字列に変換
  • constantize 文字列からクラス/定数を取得

これらのメソッドはできるだけ乱用は避け、使う場合はできるだけシンプルに扱えるようにしたいところです。

  • upsert_all

これは場合によってinsert_allにすることで意図してないデータ更新を防げます(insert_allid値を含むattributesを渡すことで、すでに存在しているデータをスキップできます)。
バリデーションやコールバックが必要な場合は、eachfind_or_initialize_by find_or_create_byを使います。

データファイルの形式

必ずしもYAMLである必要はありません。CSVなど扱いやすいと感じる形式を選んで良いでしょう。

FactoryBotを使う選択肢

そこまで厳密に管理する必要がないという場合は、FactoryBot.create_listFactoryBot.attributes_forを使うようにするのも良いでしょう。FactoryBotの場合、スキーマ変更に対応しやすい反面、思わぬデータが入る可能性が高まるため使うかどうかは状況によりそうです。

もっと細かく実行したい場合

私はここ最近ではやった覚えはないですが、追加用のファイルを作成しそのファイルの指定をして実行するような工夫は以下のようになります。

  • seed.rb自体を使いまわしたい場合は、環境変数を渡してスクリプト内で分岐する
    例) SEED_NUM=1 rails db:seed
  • 単純にrunnerやrakeコマンドとして実装する
    例) rails r {ファイル名}, rails g task {ファイル名}
    これはseedというよりはタスク処理の範疇で、ファイルの置き場所など使い分けのルールは決めたほうが良さそうです。

宣伝

こういったRailsの技法・tipsをまとめた本をkindle unlimitedで公開中です。
良かったら読んでみてください。
https://www.amazon.co.jp/dp/B0BNKTNV6M

Discussion