Rails 4.2で新規プロジェクトを作ってPostgreSQLを使ったときにDatabaseRewinderが使えなかった話。私が使っていたのがDatabaseRewinderだったという話でたぶんDatabaseCleanerでも同じ現象は起こると思う。というかdisable_referential_integrityを使っている限り起こると思う。
あとRails 4.2で外部キー制約をサポートしたから今ハマっただけで、実際のところ自分で外部キー制約を付与するとかしてたら同じ問題が起きていたはずで、ハマりやすくなった、というだけだとも思う。
原因
-
Rails 4.2からmigrationにおいて外部キー制約をサポートした(ref: Ruby on Rails 4.2 Release Notes)。scaffoldとかで
referencesやbelongs_toを使うと、生成されるmigrationファイルにはadd_foreign_keyが使われる。 -
PostgreSQLでは外部キー制約はシステムトリガーでチェックされている。
-
システムトリガーはスーパーユーザーでないと無効に出来ない。そのデータベースやテーブルのオーナーでも不可。
-
DatabaseRewinderは
disable_referential_integrityを使ってトリガーを無効にするが、上述の理由によりスーパーユーザーでないと無理。 -
トリガーを無効にしようとした時点で例外が出て、トランザクションはロールバック、同一トランザクションの後続クエリは無視される。
-
例外が出てるのでテストは失敗する。
ちなみにここでいう「スーパーユーザー」はPostgreSQLのroleの話であって、システムのスーパーユーザーとは関係ない。
解決策
- スーパーユーザーでテストを走らせる。テストを動かすユーザーをスーパーユーザー権限を持ったユーザーに変更するか、テスト用のユーザーにスーパーユーザー権限を付与する:
ALTER ROLE username WITH SUPERUSER;
当たり前だがスーパーユーザー権限の付与にはスーパーユーザー権限が必要。
-
(一時的に)外部キー制約を外す。
-
PostgreSQL使うのやめる(他のデータベースでどうなるのかは調べてない)。
ダメだった方法
- migrationの
add_foreign_keyでon_deleteオプションをcascadeかnullifyにしてみる。
on_delete: :cascadeにしてみたがダメだった。解決しない理由は、「スーパーユーザーでないのにトリガーを無効にしようとした」後に来るDELETE文の発行までそもそも辿り着いてないからだと思う。
つまるところ、外部キー制約に違反するかどうかは問題ではないという感じがある。
(2015-03-13 追記)
ダメじゃなくなるかもしれない方法
ActiveRecord::ConnectionAdapters::PostgreSQL::ReferentialIntegrity#supports_disable_referential_integrity?がfalseを返すようにすれば、そもそも問題のdisable_referential_integrityが呼ばれない。このとき、上記のようにon_delete: :cascade(or :nullify)を指定して、外部キー制約に違反したときにDELETEが失敗しないようになっていれば、特に問題なく動作を続行できるはず。
※試してない。
Links
この記事に「制約外せるのにその制約をチェックするトリガーを無効に出来ないのっておかしな話だよな」みたいなコメントがあって、確かになぁという気分になった。