Yanonoblog!

こつこつと

seed_fuとseedについて調べてみた

はじめに

seedseed_fuRailsにおいてデータベースの初期データを投入するために使われる機能ですが、微妙に違いがあるようですので備忘録としてまとめておきたいと思います。 主にseed_fuをメインにまとめています。

本記事では、seedとseed_fuに関する説明を簡単に解説していきます。

seed

seedRailsに標準で組み込まれている機能です。

db/seeds.rbファイルに初期データを記述して、rails db:seedコマンドで実行します。

seedは基本的に初期データを一度だけ投入するためのものであり、再度実行すると既存のデータが削除されて新しいデータが挿入されます。

seed_fu

seedがRailsに標準で組み込まれている機能に対し、seed_fugemになります。

seed_fuseedと異なりデータベースに対して既存のデータを削除せずに初期データを追加できるため、データがある状態で再度実行しても問題ありません。

seed_fuRailsアプリケーションのルートディレクトリのdb/fixturesディレクトリ内に配置されます。

db/fixturesディレクトリ内には、開発、テスト、本番など、各環境に対応するファイルを置くことができます。

├── db
│   ├── fixtures
│   │   ├── development
│   │   │   ├── 001_user.rb
│   │   │   ├── 002_post.rb
│   │   │   └── ...
│   │   ├── staging
│   │   │   └── ...
│   │   └── production
│   │       └── ...
│   │   └── ...
│   └── migrate
│       ├── 20230401000001_create_users.rb
│       ├── ...

以下のような形式でUserのseedデータを投入するためのファイルを記述することができます。

# db/fixtures/development/001_user.rb

User.seed(
  { 
        id: 1,
      name: 'LUFFY'
  },
  { 
        id: 2,
      name: 'UTA'
  },
)

# db/fixtures/development/002_post.rb

Post.seed(
  { 
    id: 1,
    user: User.find(2)
  },
  { 
    id: 2,
    user: User.find(2)
  },
  { 
    id: 3,
    user: User.find(2)
  }
)

上記の書き方でも可能ですが、以下の形式で記述することも可能です。

User.seed do |s|
  s.id = 1
  s.name = 'LUFFY'
end

rails db:seed_fuコマンドで実行することでデータが投入されます。

特定のファイルを指定したい時

以下のようにすることでファイル名にマッチするシードファイルが実行されます。

実運用していてFIXTURE_PATH=が上手く動作しなかったことがありましたが以下のコマンドでは動作しました。

rails db:seed_fu FILTER=001,002

YAMLで記述することもできる

seedでもseed_fuでも共通していますがYAMLファイルを用いてデータの投入を行うことも可能のようです。

# db/fixtures/users.yml

- name: John
  email: john@example.com
- name: Jane
  email: jane@example.com

RubyファイルやYAMLファイルの他にも、CSVファイルやJSONファイルなどでも記述することができます。

所感としては記述が簡単なYAMLが良さそうと思いましたが、動的なデータを使用することが出来るRubyの方が柔軟に使えるため良いかもしれません。

seed_fuを使うべき理由

  • データを更新できる

seed_fuは、更新されたデータを自動的に更新することができます。

seedでは、既存のデータを削除して作り直すため、開発する上での使い勝手が悪いです。

  • データの妥当性を検証できる

seed_fu データの妥当性を検証して、不正なデータがあれば例外を発生させます。どこかのファイルに問題がある場合、実行はされず例外だけ発生するため気軽にコマンドを実行することができます。

seedでは、妥当性の検証を行うことができません。

  • テーブルの依存関係を解決する

seed_fuでは、テーブル間の依存関係を自動的に解決し、依存関係に従ってデータを挿入することができます。

seedでは、テーブル間の依存関係を考慮してデータを挿入することができません。

GitHubのソース

おまけ的なところで /lib/seed-fu/seeder.rb こちらのソースコード気になって見ていたので書き残しておきます。

RailsのSeedデータを作成・更新するためのSeedingクラスが定義されていました。

require 'active_support/core_ext/hash/keys'

module SeedFu
  # Creates or updates seed records with data.
  #
  # It is not recommended to use this class directly. Instead, use `Model.seed`, and `Model.seed_once`,
  # where `Model` is your Active Record model.
  #
  # @see ActiveRecordExtension
  class Seeder
    # @param [ActiveRecord::Base] model_class The model to be seeded
    # @param [Array<Symbol>] constraints A list of attributes which identify a particular seed. If
    #   a record with these attributes already exists then it will be updated rather than created.
    # @param [Array<Hash>] data Each item in this array is a hash containing attributes for a
    #   particular record.
    # @param [Hash] options
    # @option options [Boolean] :quiet (SeedFu.quiet) If true, output will be silenced
    # @option options [Boolean] :insert_only (false) If true then existing records which match the
    #   constraints will not be updated, even if the seed data has changed
    def initialize(model_class, constraints, data, options = {})
      @model_class = model_class
      @constraints = constraints.to_a.empty? ? [:id] : constraints
      @data        = data.to_a || []
      @options     = options.symbolize_keys

      @options[:quiet] ||= SeedFu.quiet

      validate_constraints!
      validate_data!
    end

    # Insert/update the records as appropriate. Validation is skipped while saving.
    # @return [Array<ActiveRecord::Base>] The records which have been seeded
    def seed
      records = @model_class.transaction do
        @data.map { |record_data| seed_record(record_data.symbolize_keys) }
      end
      update_id_sequence
      records
    end

    private

      def validate_constraints!
        unknown_columns = @constraints.map(&:to_s) - @model_class.column_names
        unless unknown_columns.empty?
          raise(ArgumentError,
            "Your seed constraints contained unknown columns: #{column_list(unknown_columns)}. " +
            "Valid columns are: #{column_list(@model_class.column_names)}.")
        end
      end

      def validate_data!
        raise ArgumentError, "Seed data missing" if @data.empty?
      end

      def column_list(columns)
        '`' + columns.join("`, `") + '`'
      end

      def seed_record(data)
        record = find_or_initialize_record(data)
        return if @options[:insert_only] && !record.new_record?

        puts " - #{@model_class} #{data.inspect}" unless @options[:quiet]

        # Rails 3 or Rails 4 + rails/protected_attributes
        if record.class.respond_to?(:protected_attributes) && record.class.respond_to?(:accessible_attributes)
          record.assign_attributes(data,  :without_protection => true)
        # Rails 4 without rails/protected_attributes
        else
          record.assign_attributes(data)
        end
        record.save(:validate => false) || raise(ActiveRecord::RecordNotSaved, 'Record not saved!')
        record
      end

      def find_or_initialize_record(data)
        @model_class.where(constraint_conditions(data)).take ||
        @model_class.new
      end

      def constraint_conditions(data)
        Hash[@constraints.map { |c| [c, data[c.to_sym]] }]
      end

      def update_id_sequence
        if @model_class.connection.adapter_name == "PostgreSQL" or @model_class.connection.adapter_name == "PostGIS"
          return if @model_class.primary_key.nil? || @model_class.sequence_name.nil?

          quoted_id       = @model_class.connection.quote_column_name(@model_class.primary_key)
          sequence = @model_class.sequence_name

          # TODO postgresql_version was made public in Rails 5.0.0, remove #send when support for earlier versions are dropped
          if @model_class.connection.send(:postgresql_version) >= 100000
            sql =<<-EOS
              SELECT setval('#{sequence}', (SELECT GREATEST(MAX(#{quoted_id})+(SELECT seqincrement FROM pg_sequence WHERE seqrelid = '#{sequence}'::regclass), (SELECT seqmin FROM pg_sequence WHERE seqrelid = '#{sequence}'::regclass)) FROM #{@model_class.quoted_table_name}), false)
            EOS
          else
            sql =<<-EOS
              SELECT setval('#{sequence}', (SELECT GREATEST(MAX(#{quoted_id})+(SELECT increment_by FROM #{sequence}), (SELECT min_value FROM #{sequence})) FROM #{@model_class.quoted_table_name}), false)
            EOS
          end

          @model_class.connection.execute sql
        end
      end
  end
end
  • 初期化時に、モデルクラス、制約、データ、オプションの4つのパラメータを取る
    • モデルクラスは、Seedデータを作成・更新するためのActive Recordモデル
    • 制約はSeedデータの一意性を確保するための属性のリスト
    • Seedデータの属性情報を含むハッシュの配列
    • quietinsert_onlyの2つのプロパティ(コメントアウトに説明がある)
  • seed_recordメソッド
    • 与えられたデータに基づいてレコードを作成・更新するためのメソッド
  • find_or_initialize_recordメソッド
    • メソッドでレコードを探し、見つからなかった場合には新規に作成を行う
    • insert_onlyオプションが有効な場合には、既存のレコードは更新されず、新しいレコードのみが作成される。

おわりに

コメント

本記事の内容は以上になります!

ふと気になったことがきっかけで気がついたら結構調べていたのでまとめました。

参考になったり学びのきっかけになりますと幸いです。

間違いがありましたら修正いたしますので、ご指摘ください。

興味があれば他の記事も更新していきますので是非ご覧になってください♪


◇ プログラミングスクールのご紹介 (卒業生より)

お世話になったプログラミングスクールであるRUNTEQです♪

https://runteq.jp/r/ohtFwbjW

ご不明な点ありましたらお気軽にコメントか、TwitterのDMでお答えします♪

https://twitter.com/outputky

参考