Rails: マイグレーションを実行せずにマイグレーションのSQLを表示する(翻訳)

概要

原著者の許諾を得て翻訳・公開いたします。

Rails: マイグレーションを実行せずにマイグレーションのSQLを表示する(翻訳)

「マイグレーションを実行しないでSQLを取る方法はありますか?」という質問を何度か目にしたことがあります。芸のない回答としては、質問の「マイグレーションを実行しないで」を無視してマイグレーションを実行し、ログファイルをgrepしてSQL出力を取り出し、db:rollbackを実行せよというのが考えられます。しかしこれはズルですし手間もかかります。もっとマシな方法はないものでしょうか。

私の最初のアプローチは、ActiveRecordスタックの相当深いところでメソッド呼び出しをインターセプトし、欲しいマイグレーションの場合は実行せずにSQLを出力するというものでした。私はPostgreSQLを使っているので、ActiveRecord::ConnectionAdapters::PostgreSQLAdapter#executeメソッドをインターセプトしてみたいと思います。以下はプロキシです。

module MyTweak
  def execute(sql, name=nil)
    if caller.detect {|x| x =~ /20171010151334/ } && sql !~ /SHOW TIME ZONE/
      puts sql
    else
      super
    end
  end
end

しかし実際にこれを使ってみるとお世辞にも美しいとは言えませんでした。コンソールでこのコード変更を適用し、マイグレーションを明示的に呼び出さないといけません。

class ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
  prepend MyTweak
end
require "#{Rails.root}/db/migrate/20171010151334_add_wing_count_to_jets"
AddWingCountToJets.new.change

サンプルの出力結果をいくつかGistに置きました。しかし見てのとおり、この方法は相当イケてないうえに何というか苦痛です。

StackOverflowのこのスレでもっとよいアプローチをいくつか見つけました。1つ目の回答はalias_methodActiveRecord::ConnectionAdapters::AbstractMysqlAdapter#executeを再定義していますが、私が上のMyTweakでやったことと大差はなく、特定のメソッドをショートさせてSQLを出力しています。しかし本当の改善は、fake_db_migrateというRakeタスクを定義してモンキーパッチを当ててからdb:migrateを実行するという方法でした。これならハードコードも不要ですし、どんなマイグレーションでも動きます。

そのスレの別の回答はもっとうまく動くのですが、少々不安定な感じでもありました。その方法ではSQLの取得にトランザクションとロールバックを使っていました。この方法もコンソールで特定のマイグレーションをrequireしなければなりませんが、コードにパッチを当てるのではなく、マイグレーションをトランザクションで実行して明示的にロールバック例外をraiseしています。

ActiveRecord::Base.connection.transaction do
  AddWingCountToJets.new.migrate :up
  raise ActiveRecord::Rollback
end

このアプローチも、質問の「マイグレーションを実行しないで」を無視していますが、その点はおそらく大丈夫でしょう。やりたいのは、データベースを元の状態のままSQLを取り出すことだからです。SQLを実行してからロールバックすればこの目的を達成できます。

Railsコアプロジェクトで、この機能のためのプルリク#31630がオープンされました。このプルリクのアプローチでは、「dry run」フラグを取り入れたロールバック戦略を用いています。この実装の今後の移り変わりや、ActiveRecordコアに取り入れられるかどうかについては興味を惹かれます。

マイグレーションのArel AST(抽象構文木)を取り出してto_sqlを呼べばいいのになぜそうしないのかとお思いの方もいるかもしれませんが、これはマイグレーションの実際の動作とは異なっています。マイグレーションは、ActiveRecordクエリのように途中でツリーを生成したりプロセスをフルスキャンしたりするのではなく、必要に応じてSQLからビルドされます。たとえば以下は、マイグレーションの非常に便利なメソッドであるchange_column_defaultのPostgreSQLアダプタ版から抜粋したものです。ここから文字列が結合される様子がわかります。

  alter_column_query = "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} %s"
  if default.nil?
    # DEFAULT NULLの振る舞いはDROP DEFAULTと同じ結果になる。
    # ただしPostgreSQLはデフォルトをカラム型にキャストし、
    # "default NULL::character varying"のようにデフォルトにする
    execute alter_column_query % "DROP DEFAULT"
  else
    execute alter_column_query % "SET DEFAULT #{quote_default_expression(default, column)}"
  end

ArelはDDL ASTをサポートしませんが、DDL文法が存在しているので、これらがマイグレーションの中間層にまとまっていることは想像できます。しかし、現在のアプローチでこの作業を長年に渡って完了できているので、私にはこの部分で頑張るのがよいとは思えません。

結論としては、ロールバック戦略が明確さにおいてベストではないかと思います。データベースアダプタにモンキーパッチを当てたりしないからです。しかし一回こっきりの雑なハックで構わないのであれば、モンキーパッチで切り抜けるのも悪くないでしょう。これがRailsのコアに取り入れられるかどうか、今後もRailsのchangelogに注目しましょう。

訳注: #31630はマージされずにクローズしました。

関連記事

[Rails 5] マイグレーション時にデータベースのカラムにコメントを追加する

Rails: データベースのパフォーマンスを損なう3つの書き方(翻訳)

Ruby on RailsによるWEBシステム開発、Android/iPhoneアプリ開発、電子書籍配信のことならお任せください この記事を書いた人と働こう! Ruby on Rails の開発なら実績豊富なBPS

この記事の著者

hachi8833

Twitter: @hachi8833、GitHub: @hachi8833 コボラー、ITコンサル、ローカライズ業界、Rails開発を経てTechRachoの編集・記事作成を担当。 これまでにRuby on Rails チュートリアル第2版の半分ほど、Railsガイドの初期翻訳ではほぼすべてを翻訳。その後も折に触れてそれぞれ一部を翻訳。 かと思うと、正規表現の粋を尽くした日本語エラーチェックサービス enno.jpを運営。 実は最近Go言語が好き。 仕事に関係ないすっとこブログ「あけてくれ」は2000年頃から多少の中断をはさんで継続、現在はnote.muに移転。

hachi8833の書いた記事

週刊Railsウォッチ

インフラ

BigBinary記事より

ActiveSupport探訪シリーズ