DatabaseRewinder with Rails 4.2 & PostgreSQL

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とかでreferencesbelongs_toを使うと、生成されるmigrationファイルにはadd_foreign_keyが使われる。

  • PostgreSQLでは外部キー制約はシステムトリガーでチェックされている。

  • システムトリガーはスーパーユーザーでないと無効に出来ない。そのデータベースやテーブルのオーナーでも不可。

  • DatabaseRewinderはdisable_referential_integrityを使ってトリガーを無効にするが、上述の理由によりスーパーユーザーでないと無理。

  • トリガーを無効にしようとした時点で例外が出て、トランザクションはロールバック、同一トランザクションの後続クエリは無視される。

  • 例外が出てるのでテストは失敗する。

ちなみにここでいう「スーパーユーザー」はPostgreSQLのroleの話であって、システムのスーパーユーザーとは関係ない。

解決策

  • スーパーユーザーでテストを走らせる。テストを動かすユーザーをスーパーユーザー権限を持ったユーザーに変更するか、テスト用のユーザーにスーパーユーザー権限を付与する:
ALTER ROLE username WITH SUPERUSER;

当たり前だがスーパーユーザー権限の付与にはスーパーユーザー権限が必要。

  • (一時的に)外部キー制約を外す。

  • PostgreSQL使うのやめる(他のデータベースでどうなるのかは調べてない)。

ダメだった方法

  • migrationのadd_foreign_keyon_deleteオプションをcascadenullifyにしてみる。

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

この記事に「制約外せるのにその制約をチェックするトリガーを無効に出来ないのっておかしな話だよな」みたいなコメントがあって、確かになぁという気分になった。