LoginSignup
11
13

More than 5 years have passed since last update.

外部キー制約と fixtures の共存

Posted at

困ったこと

fixtures を使ってテストを書いているプロジェクトで、DB のテーブルに外部キー制約を設定したところ、 ActiveRecord::InvalidForeignKey が出てテストが実行出来なくなりました。

migration.rb
class CreatePrefectures < ActiveRecord::Migration
  def change
    create_table :prefectures do |t|
      t.text :name

      t.timestamps
    end
  end
end

class CreatePatients < ActiveRecord::Migration
  def change
    create_table :patients do |t|
      t.text :name
      t.references :prefecture, index: true

      t.timestamps
    end

    add_foreign_key(:patients, :prefectures)
  end
end
patients.yml
pat_1:
  name: hoge
  prefecture: tokyo

↑これを入れる時にエラー

prefectures.yml
tokyo:
  name: 東京都
saitama:
  name: 埼玉県

原因

fixtures のファイルをデータベースに読み込む順番が単なる名前順なので、先に読み込もうとした fixture が未読み込みの fixture に依存している場合、外部キー制約に引っかかります。

解決策

データ投入時だけ制約を外す

fixtures はラベルから一意の id を生成して利用するので、この方法で最終的には辻褄が合うような気がしますが、楽に自動化する方法が思いつきませんでした。

正しい順番で読み込む

やはりこれが王道だと思うわけです。
このためのモンキーパッチ(PostgreSQL 8.4 以上限定)を書きました。これを test_helper.rb かなんかに読み込ませてやれば、システムカタログから依存関係を抽出して、正しい順番で fixtures を読み込む事が出来るようになります。

fixtures_with_constraint.rb
module ActiveRecord
  module TestFixtures
    module ClassMethods
      def fixtures(*fixture_set_names)
        fixture_set_names = sort_by_dependence(
          if fixture_set_names.first == :all
            Dir["#{fixture_path}/{**,*}/*.{yml}"].map{|f| f[(fixture_path.to_s.size + 1)..-5]}
          else
            fixture_set_names.flatten.map{|n| n.to_s}
          end
        )

        self.fixture_table_names |= fixture_set_names

        require_fixture_classes(fixture_set_names, self.config)
        setup_fixture_accessors(fixture_set_names)
      end

      private
      def sort_by_dependence(fixture_set_names)
        fixture_set_names.flat_map{|name| dependent_table_names(name)}.uniq
      end

      def dependent_table_names(fixture_set_name)
        sql = <<-SQL
               WITH RECURSIVE r AS (
             SELECT pc_1.oid AS p_id,
                    pc_1.relname AS parent,
                    pc_2.oid AS c_id,
                    pc_2.relname AS child
               FROM pg_class AS pc_1
               JOIN pg_constraint ON pc_1.oid = conrelid
               JOIN pg_class AS pc_2 ON pc_2.oid = confrelid
              WHERE pc_1.relname = ?
          UNION ALL
             SELECT pc_1.oid AS p_id,
                    pc_1.relname AS parent,
                    pc_2.oid AS c_id,
                    pc_2.relname AS child
               FROM pg_class AS pc_1
               JOIN pg_constraint ON pc_1.oid = conrelid
               JOIN pg_class AS pc_2 ON pc_2.oid = confrelid
               JOIN r ON r.c_id = pc_1.oid
                  )
             SELECT child
               FROM r
        SQL

        ActiveRecord::Base.connection.execute(
          ActiveRecord::Base.send(:sanitize_sql_array ,[sql, fixture_set_name])
        ).map{|h| h["child"]}.reverse.push(fixture_set_name).map(&:to_sym)
      end
    end
  end
end

 課題

self.use_transactional_fixtures = false などとすると、Delete Fixtures が走って外部キー制約に引っかかってしまいます。use_transactional_fixtures を切るなら削除の順番も依存性に従って整列させてやらないとダメそうですが、どの辺を弄れば良いか、まだ分かりません。

11
13
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
13