困ったこと
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 を切るなら削除の順番も依存性に従って整列させてやらないとダメそうですが、どの辺を弄れば良いか、まだ分かりません。