Ruby on Railsのマイグレーション管理を生のSQLファイルで行いたい人のための備忘録
Rails、はじめました
あけましておめでとうございます。このアカウントでは初めての記事になります。
今回は「Railsのマイグレーションを生SQLでやっちゃおうぜ」という趣旨の記事となります。
やりたいこと
ここで言う「マイグレーション」とは、いわゆる「データベースマイグレーション」を指します。「データベースマイグレーションとはなにか?」という野暮な説明は省きまして、ここではRailsガイドを引用させていただきます。
マイグレーションは、再現可能な方法でデータベーススキーマを継続的に進化させる便利な方法です。
マイグレーションではRubyのDSLを利用しているので、SQLを手動で記述しなくても済み、スキーマやスキーマの変更がデータベースに依存しないようにできます。
ざっくりと、データベーススキーマの変更をRubyのコードで使って管理するので、テーブルの作成や更新のためのSQL(DDL)を書く必要のない便利な機能ということになります。
この記事では、あえてデータベーススキーマの管理にRubyのコードを使わず、旧来のSQLファイルを用いるようにするための手順を紹介します。ActiveRecordに頼らない、SQLファイルによるマイグレーション管理にはメリットもデメリットもありますが、スキーマ管理をActiveRecordに任せたくない、SQLファイルを直接記述してDBMS固有の機能を扱いたい、といった(ちょっと特殊な)ユースケースでご活用いただければ幸いです。
前提条件
$ ruby --version
ruby 3.3.6 (2024-11-05 revision 75015d4c1f) [aarch64-linux]
$ rails --version
Rails 8.0.1
実際にやってみた
SQL
にする
1. マイグレーションフォーマットをconfig/application.rb
を開き、class Application < Rails::Application
内に追記します。(追記する位置はclass内の任意の位置でよいと思います)
require_relative "boot"
require "rails/all"
# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)
module Sample # ここはプロジェクト名によって異なります
class Application < Rails::Application
# Initialize configuration defaults for originally generated Rails version.
config.load_defaults 8.0
# Please, add to the `ignore` list any other `lib` subdirectories that do
# not contain `.rb` files, or that should not be reloaded or eager loaded.
# Common ones are `templates`, `generators`, or `middleware`, for example.
config.autoload_lib(ignore: %w[assets tasks])
config.autoload_paths << Rails.root.join("lib")
# Configuration for the application, engines, and railties goes here.
#
# These settings can be overridden in specific environments using the files
# in config/environments, which are processed later.
# config.time_zone = "Central Time (US & Canada)"
# config.eager_load_paths << Rails.root.join("extras")
+ config.active_record.schema_format = :sql
end
end
基本的にはこれだけでOKです。この設定を行うと、rails db:migrate
を実行した際に作成されるスキーマファイルは標準のschema.rb
ではなく、structure.sql
が作成されるようになります。
rails g migration {モデル名}
コマンドを実行すると、通常どおりRubyのマイグレーションファイルが作成されます。例えばマイグレーションファイルを以下のように実装すると、SQLを直接実行することができます。
class CreateUsers < ActiveRecord::Migration[6.1]
def up
execute <<-SQL
CREATE TABLE users (
id serial PRIMARY KEY,
name varchar(255) NOT NULL,
email varchar(255) UNIQUE NOT NULL,
created_at timestamp NOT NULL,
updated_at timestamp NOT NULL
);
SQL
end
def down
execute "DROP TABLE users;"
end
end
2. SQLを別ファイルから読み込むようにする
上の手順だけだと、SQLはファイルとして切り出すことができず、またRubyコードの一部となってしまうためシンタックスチェックやフォーマッタの恩恵を受けることができません。
以下のような条件のもと、マイグレーションファイルとは別のファイルからSQLを読み込むようにしてみます。
- マイグレーションファイルのあるディレクトリに
sql
サブディレクトリを作る。SQLファイルはこの中に配置する - SQLファイルの名称は、
{拡張子を除くマイグレーションファイルの名称}.{up|down}.sql
とする- たとえば
20250108063214_create_users.rb
というマイグレーションファイルがあった場合、実行するSQLファイルは20250108063214_create_users.up.sql
、および20250108063214_create_items.down.sql
となるようにします
- たとえば
ヘルパーモジュールを作る
SQLファイルを読み込むためのヘルパーモジュールとメソッドを作ります。
module MigrationHelpers
DEST = { up: "up", down: "down" }
def get_sql_filename(migration_file_name, dest: DEST[:up])
base_name = File.basename(migration_file_name, ".*")
sql_file = File.join(File.dirname(migration_file_name), "sql", "#{base_name}.#{dest}.sql")
if File.exist?(sql_file)
sql_file
else
puts "対応するSQLファイルが見つかりません: #{sql_file}"
end
end
end
class CustomMigration < ActiveRecord::Migration[6.1]
include MigrationHelpers
end
このモジュールを作成すると、ActiveRecord
を継承したCustomMigration
クラスの中でget_sql_filename
メソッドを呼び出せるようになります。
マイグレーションファイルの変更
マイグレーションファイルを変更し、作成したヘルパーメソッドを使ってSQLファイルを読み込むようにします。以下は一例です。
require 'migration_helpers'
class CreateHogehoges < CustomMigration
def up
sql_file = get_sql_filename(__FILE__, dest: :up)
if sql_file.nil?
raise "SQLファイル(up)が見つかりません"
end
sql = File.read(sql_file)
execute sql
end
def down
sql_file = get_sql_filename(__FILE__, dest: :down)
if sql_file.nil?
raise "SQLファイル(down)が見つかりません"
end
sql = File.read(sql_file)
execute sql
end
end
__FILE__
特殊変数を使って自分自身のファイル名をメソッドに渡し、返されたSQLファイルを読み込んで実行しています。
あとは、マイグレーションファイルのあるディレクトリにsql
サブディレクトリを作成し、先ほどのルールに従って命名したSQLファイルを配置します。
ここまでの手順で、rails db:migrate
を実行するとマイグレーションが行われ、命名ルールに沿ったSQLファイルを読み込む仕組みができました。
3. マイグレーションファイルを自動作成する
仕組みはできましたが、今の状態だとマイグレーションファイルを毎回手作業で編集する必要があります。ヘルパーメソッドのおかげでマイグレーションファイルの内容はすべて共通化できるので、せっかくならrails g migration
で自動で作成できるようにしたいところです。
次の手順として、Railsのマイグレーションジェネレータをカスタマイズし、マイグレーションファイルの内容が自動生成されるようにします。
プロジェクト直下のlib
ディレクトリにgenerators/migration/
ディレクトリを作成し、以下の内容でジェネレータファイルを作成します。
class MigrationGenerator < Rails::Generators::NamedBase
source_root File.expand_path("templates", __dir__)
def create_migration_file
timestamp = Time.now.utc.strftime("%Y%m%d%H%M%S")
migration_filename = "#{timestamp}_#{file_name}"
template "migration.tt", "db/migrate/#{migration_filename}.rb"
end
private
def migration_class_name
file_name.camelize
end
end
さらにtemplates
サブディレクトリを作成し、ジェネレータが読み込むテンプレートファイルを以下の内容で作成します。テンプレート内でクラス名の作成に用いているmigration_class_name
関数は、先ほどのジェネレータファイルで定義しています。
require 'migration_helpers'
class <%= migration_class_name %> < CustomMigration
def up
sql_file = get_sql_filename(__FILE__, dest: :up)
if sql_file.nil?
raise "SQLファイル(up)が見つかりません"
end
sql = File.read(sql_file)
execute sql
end
def down
sql_file = get_sql_filename(__FILE__, dest: :down)
if sql_file.nil?
raise "SQLファイル(down)が見つかりません"
end
sql = File.read(sql_file)
execute sql
end
end
仕上げとして、ジェネレータファイルをRailsが読み込むために、application.rb
に自動ロードのパスを追記します。
require_relative "boot"
require "rails/all"
# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)
module Sample
class Application < Rails::Application
# Initialize configuration defaults for originally generated Rails version.
config.load_defaults 8.0
# Please, add to the `ignore` list any other `lib` subdirectories that do
# not contain `.rb` files, or that should not be reloaded or eager loaded.
# Common ones are `templates`, `generators`, or `middleware`, for example.
config.autoload_lib(ignore: %w[assets tasks])
+ config.autoload_paths << Rails.root.join("lib")
# Configuration for the application, engines, and railties goes here.
#
# These settings can be overridden in specific environments using the files
# in config/environments, which are processed later.
#
# config.time_zone = "Central Time (US & Canada)"
# config.eager_load_paths << Rails.root.join("extras")
config.active_record.schema_format = :sql
end
end
これで、マイグレーションファイルのカスタマイズができました。rails g migration {モデル名}
を実行して生成されるマイグレーションファイルは、SQLファイルを読み込む処理があらかじめ記述された状態で生成されます。
4. SQLファイルも自動作成する
さて、もう一歩改善してみます。ここまでの手順でマイグレーションファイルをカスタマイズし、決まった名称のSQLファイルを読み込み実行するまではできるようになりましたが、まだ以下の手順が手動のままです。
- マイグレーションファイルの名称をコピーする
- コピーした名前で
sql
サブディレクトリ下にファイルを作成し、拡張子を.up.sql
にする - もう1つファイルを作成し、拡張子を
down.sql
にする
これも面倒なので、ジェネレータの処理に含めて自動化してしまいます!
class MigrationGenerator < Rails::Generators::NamedBase
source_root File.expand_path("templates", __dir__)
def create_migration_file
timestamp = Time.now.utc.strftime("%Y%m%d%H%M%S")
migration_filename = "#{timestamp}_#{file_name}"
template "migration.tt", "db/migrate/#{migration_filename}.rb"
+ base_sql_path = "db/migrate/sql/#{migration_filename}"
+ create_file "#{base_sql_path}.up.sql", ""
+ create_file "#{base_sql_path}.down.sql", ""
end
private
def migration_class_name
file_name.camelize
end
end
migration_filename
変数を使い回して、それぞれup
とdown
のSQLファイルを作成しています。SQLファイルの内容は空なので、実行したいSQLを適宜記述してください。
まとめ
以下の手順を行うことで、Railsのマイグレーション管理をSQLファイルで行うことができるようになりました。
- マイグレーションフォーマットを「SQL」に指定する
- SQLファイルを読み込んで実行するヘルパーを作る
- マイグレーションジェネレータをカスタマイズする
「ここ間違ってるよ」「こうしたほうがいいよ」などのご指摘があれば、何卒お手柔らかに、よろしくお願いします。
ではまた!
Discussion