概要
原著者の許諾を得て翻訳・公開いたします。
- 英語記事: Viewing migration SQL without running the migration
- 原文公開日: 2018/01/10
- 著者: Tom Copeland
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_method
でActiveRecord::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はマージされずにクローズしました。