🩹

データベースに接続中のセッションがあってもrails db:resetを成功させる

2022/08/24に公開

rails serverが起動している状態でrails db:reset等を実行すると以下のようなエラーを出力して失敗してしまいます。

$ ./bin/rails db:reset                                                               
PG::ObjectInUse: ERROR:  database "app" is being accessed by other users
DETAIL:  There is 1 other session using the database.
Couldn't drop database 'app'
rails aborted!
ActiveRecord::StatementInvalid: PG::ObjectInUse: ERROR:  database "app" is being accessed by other users
DETAIL:  There is 1 other session using the database.
/workspace/app/bin/rails:4:in `<main>'

ローカル・プレビュー環境など、何度もdb:resetすることになるので、このエラーを回避したいですね。

原因

PostgreSQLはデータベースに接続中のセッションが別にある状態でDROP DATABASEを実行することを許していません。

PostgreSQL13以降ではDROP DATABASEにFORCEオプションが追加された

https://www.postgresql.org/docs/release/13.0/

Allow DROP DATABASE to disconnect sessions using the target database, allowing the drop to succeed (Pavel Stehule, Amit Kapila)
This is enabled by the FORCE option.

対処

Railsで rails db:drop を実行した際の処理の流れ

PostgreSQLを使用しているRailsアプリケーションでrails db:dropを実行した時、以下のタスクが実行されます。

https://github.com/rails/rails/blob/f95c0b7e96eb36bc3efc0c5beffbb9e84ea664e4/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb#L30-L33

connection.drop_databaseActiveRecord::Base.connection#drop_databaseを実行しています。PostgreSQLを使用している場合は下記が実行されます。

https://github.com/rails/rails/blob/67feef37a70af6e3dd71cfe9bbe62cc629831572/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb#L53-L55

パッチを当てる

FORCEオプション未対応の最後のメジャーバージョンであるPostgreSQL 12のEOLは2024年11月であるそうです。まだ少し先ですね。
今回はモンキーパッチを当てることで対応します。

WITH (FORCE)を追加するパッチを作成

以下のようなモンキーパッチを作成しました。

lib/patches/activerecord_postgresql.rb
# PostgreSQL13>で使えるFORCEオプションを使用してコネクションがあってもDROP DATABASEできるようにする
ActiveSupport.on_load(:active_record) do
  ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.prepend(
    Module.new do
      def drop_database(name)
        execute "DROP DATABASE IF EXISTS #{quote_table_name(name)} WITH (FORCE)"
      end
    end
  )
end

パッチが読み込まれるように設定

initializersにファイルを作成して、モンキーパッチがRailsの起動時に読み込まれるようにします。

config/initializers/000_monkey_patches.rb
Dir[Rails.root.join('lib/patches/**/*.rb')].each do |file|
  require file
end

テスト

$ ./bin/rails db:reset                                                               
Dropped database 'app'
Created database 'app'

うまくいきました!

参考リンク

株式会社モニクル

Discussion