Rails Active RecordとLaravel Eloquentのマイグレーション機能比較: スキーマ管理の違いと実装方法
はじめに
データベーススキーマの管理は、アプリケーションの成長と共に重要性を増す要素です。Ruby on RailsのActive RecordとLaravelのEloquentは、それぞれ独自のマイグレーション機能を提供し、データベースの構造変更を効率的に管理できます。
この記事では、両フレームワークのマイグレーション機能を詳細に比較し、実装方法の違いや特徴を具体的なコード例と共に解説します。
1. マイグレーション機能の基本概念
Railsのマイグレーション
- タイムスタンプベース: ファイル名に実行順序を示すタイムスタンプを含む
- up/downメソッド: 変更の実行と巻き戻しを明示的に定義
- DSLベース: Rubyの直感的な記法でスキーマ定義
Laravelのマイグレーション
- タイムスタンプベース: Railsと同様のタイムスタンプ方式
- up/downメソッド: 変更の実行と巻き戻しを定義
- Schemaファサードベース: PHPの流暢なインターフェースでスキーマ定義
2. 基本的なマイグレーション作成
Rails
bash
# マイグレーションファイル作成
rails generate migration CreateUsers name:string email:string age:integer
rails generate migration AddPhoneToUsers phone:string
rails generate migration RemoveAgeFromUsers age:integer
ruby
# db/migrate/20231201000001_create_users.rb
class CreateUsers < ActiveRecord::Migration[7.0]
def change
create_table :users do |t|
t.string :name, null: false
t.string :email, null: false
t.integer :age
t.timestamps
end
add_index :users, :email, unique: true
end
end
ruby
# db/migrate/20231201000002_add_phone_to_users.rb
class AddPhoneToUsers < ActiveRecord::Migration[7.0]
def change
add_column :users, :phone, :string
end
end
Laravel
bash
# マイグレーションファイル作成
php artisan make:migration create_users_table
php artisan make:migration add_phone_to_users_table --table=users
php artisan make:migration drop_age_from_users_table --table=users
php
// database/migrations/2023_12_01_000001_create_users_table.php
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateUsersTable extends Migration
{
public function up()
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->integer('age')->nullable();
$table->timestamps();
});
}
public function down()
{
Schema::dropIfExists('users');
}
}
php
// database/migrations/2023_12_01_000002_add_phone_to_users_table.php
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddPhoneToUsersTable extends Migration
{
public function up()
{
Schema::table('users', function (Blueprint $table) {
$table->string('phone')->nullable();
});
}
public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('phone');
});
}
}
3. データタイプと制約の定義
Rails
ruby
# db/migrate/20231201000003_create_products.rb
class CreateProducts < ActiveRecord::Migration[7.0]
def change
create_table :products do |t|
# 基本的なデータタイプ
t.string :name, limit: 100, null: false
t.text :description
t.decimal :price, precision: 10, scale: 2
t.integer :stock_quantity, default: 0
t.boolean :is_active, default: true
t.datetime :published_at
t.date :expire_date
# 外部キー
t.references :category, foreign_key: true
t.belongs_to :user, null: false, foreign_key: true
# UUID
t.uuid :external_id, default: -> { "gen_random_uuid()" }
# JSON(PostgreSQL)
t.json :metadata
t.timestamps
end
# インデックス
add_index :products, :name
add_index :products, [:category_id, :is_active]
add_index :products, :external_id, unique: true
# 制約
add_check_constraint :products, "price >= 0", name: "positive_price"
end
end
Laravel
php
// database/migrations/2023_12_01_000003_create_products_table.php
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateProductsTable extends Migration
{
public function up()
{
Schema::create('products', function (Blueprint $table) {
$table->id();
// 基本的なデータタイプ
$table->string('name', 100);
$table->text('description')->nullable();
$table->decimal('price', 10, 2);
$table->integer('stock_quantity')->default(0);
$table->boolean('is_active')->default(true);
$table->timestamp('published_at')->nullable();
$table->date('expire_date')->nullable();
// 外部キー
$table->foreignId('category_id')->constrained();
$table->foreignId('user_id')->constrained();
// UUID
$table->uuid('external_id');
// JSON
$table->json('metadata')->nullable();
$table->timestamps();
// インデックス
$table->index('name');
$table->index(['category_id', 'is_active']);
$table->unique('external_id');
});
// 制約(Laravel 8+)
DB::statement('ALTER TABLE products ADD CONSTRAINT positive_price CHECK (price >= 0)');
}
public function down()
{
Schema::dropIfExists('products');
}
}
4. データベース固有機能の活用
Rails(PostgreSQL拡張)
ruby
# db/migrate/20231201000004_add_postgresql_features.rb
class AddPostgresqlFeatures < ActiveRecord::Migration[7.0]
def change
# 拡張機能の有効化
enable_extension 'pgcrypto'
enable_extension 'pg_trgm'
create_table :articles do |t|
t.string :title, null: false
t.text :content
t.string :tags, array: true, default: []
t.jsonb :metadata, default: {}
t.int4range :view_count_range
t.timestamps
end
# GINインデックス
add_index :articles, :tags, using: 'gin'
add_index :articles, :metadata, using: 'gin'
# 部分インデックス
add_index :articles, :title, where: "content IS NOT NULL"
# 全文検索インデックス
add_index :articles, :content, using: 'gin', opclass: :gin_trgm_ops
end
end
Laravel(PostgreSQL拡張)
php
// database/migrations/2023_12_01_000004_add_postgresql_features.php
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;
class AddPostgresqlFeatures extends Migration
{
public function up()
{
// 拡張機能の有効化
DB::statement('CREATE EXTENSION IF NOT EXISTS "pgcrypto"');
DB::statement('CREATE EXTENSION IF NOT EXISTS "pg_trgm"');
Schema::create('articles', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('content')->nullable();
$table->json('tags')->default('[]');
$table->jsonb('metadata')->default('{}');
$table->timestamps();
});
// GINインデックス
DB::statement('CREATE INDEX articles_tags_gin_idx ON articles USING gin(tags)');
DB::statement('CREATE INDEX articles_metadata_gin_idx ON articles USING gin(metadata)');
// 部分インデックス
DB::statement('CREATE INDEX articles_title_partial_idx ON articles(title) WHERE content IS NOT NULL');
// 全文検索インデックス
DB::statement('CREATE INDEX articles_content_gin_trgm_idx ON articles USING gin(content gin_trgm_ops)');
}
public function down()
{
Schema::dropIfExists('articles');
}
}
5. 複雑なスキーマ変更の実装
Rails
ruby
# db/migrate/20231201000005_complex_schema_changes.rb
class ComplexSchemaChanges < ActiveRecord::Migration[7.0]
def up
# 1. 新しいテーブル作成
create_table :user_profiles do |t|
t.references :user, null: false, foreign_key: true
t.string :first_name
t.string :last_name
t.date :birth_date
t.timestamps
end
# 2. 既存データの移行
User.find_each do |user|
names = user.name.split(' ', 2)
UserProfile.create!(
user: user,
first_name: names.first,
last_name: names.second
)
end
# 3. 古いカラムの削除
remove_column :users, :name, :string
# 4. インデックスの再構築
remove_index :users, :email
add_index :users, :email, unique: true, algorithm: :concurrently
end
def down
add_column :users, :name, :string
User.joins(:user_profile).find_each do |user|
profile = user.user_profile
full_name = [profile.first_name, profile.last_name].compact.join(' ')
user.update_column(:name, full_name)
end
drop_table :user_profiles
end
end
Laravel
php
// database/migrations/2023_12_01_000005_complex_schema_changes.php
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use App\Models\User;
use App\Models\UserProfile;
class ComplexSchemaChanges extends Migration
{
public function up()
{
// 1. 新しいテーブル作成
Schema::create('user_profiles', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained();
$table->string('first_name')->nullable();
$table->string('last_name')->nullable();
$table->date('birth_date')->nullable();
$table->timestamps();
});
// 2. 既存データの移行
User::chunk(100, function ($users) {
foreach ($users as $user) {
$names = explode(' ', $user->name, 2);
UserProfile::create([
'user_id' => $user->id,
'first_name' => $names[0],
'last_name' => $names[1] ?? null,
]);
}
});
// 3. 古いカラムの削除
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('name');
});
}
public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->string('name')->nullable();
});
User::with('userProfile')->chunk(100, function ($users) {
foreach ($users as $user) {
$profile = $user->userProfile;
$fullName = trim($profile->first_name . ' ' . $profile->last_name);
$user->update(['name' => $fullName]);
}
});
Schema::dropIfExists('user_profiles');
}
}
6. マイグレーションの実行と管理
Rails
bash
# マイグレーション実行
rails db:migrate
# 特定バージョンまで実行
rails db:migrate VERSION=20231201000003
# ロールバック
rails db:rollback
rails db:rollback STEP=3
# マイグレーション状態確認
rails db:migrate:status
# データベースリセット
rails db:reset
rails db:drop db:create db:migrate
# シードデータ投入
rails db:seed
rails db:setup # create + migrate + seed
Laravel
bash
# マイグレーション実行
php artisan migrate
# 特定バッチまで実行
php artisan migrate --step=1
# ロールバック
php artisan migrate:rollback
php artisan migrate:rollback --step=3
# マイグレーション状態確認
php artisan migrate:status
# データベースリセット
php artisan migrate:reset
php artisan migrate:refresh # reset + migrate
php artisan migrate:fresh # drop + migrate
# シードデータ投入
php artisan db:seed
php artisan migrate:refresh --seed
7. 本番環境での安全なマイグレーション
Rails
ruby
# config/environments/production.rb
config.active_record.migration_error_on_skip_failure = true
# 本番環境向けマイグレーション
class SafeAddIndexToUsers < ActiveRecord::Migration[7.0]
disable_ddl_transaction! # 大きなテーブルでのCONCURRENTLY使用
def up
add_index :users, :email, algorithm: :concurrently
end
def down
remove_index :users, :email
end
end
Laravel
php
// 本番環境向けマイグレーション
class SafeAddIndexToUsers extends Migration
{
public function up()
{
// インデックス作成時のタイムアウト設定
DB::statement('SET SESSION lock_wait_timeout = 60');
Schema::table('users', function (Blueprint $table) {
$table->index('email');
});
}
public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->dropIndex(['email']);
});
}
}
8. ベストプラクティスと注意点
Rails
- スキーマファイルの活用:
db/schema.rb
で現在のスキーマ状態を確認 - 可逆性の確保:
change
メソッドで自動的に可逆な操作を定義 - データ移行の分離: データ移行は別のマイグレーションで実施
Laravel
- スキーマダンプの活用:
php artisan schema:dump
でスキーマを統合 - Foreign Key制約:
constrained()
メソッドで適切な外部キー制約を設定 - 環境別実行:
--env
オプションで環境別にマイグレーション実行
9. 実際のプロジェクトでの選択指針
Railsマイグレーションの特徴
- 強力なDSL: 直感的で読みやすいスキーマ定義
- 統合性: Active Recordとの密接な統合
- Convention over Configuration: 規約に従った自動的な動作
Laravelマイグレーションの特徴
- 柔軟性: 複雑なスキーマ変更にも対応
- バッチ管理: マイグレーションのバッチ単位での管理
- 豊富なヘルパー: 様々なデータベース操作のヘルパーメソッド
まとめ
Rails Active RecordとLaravel Eloquentのマイグレーション機能は、どちらも強力で実用的なスキーマ管理機能を提供しています。Railsは簡潔で直感的な記法を重視し、Laravelは柔軟性と詳細な制御を重視しています。
プロジェクトの規模、チームの経験、データベースの複雑さなどを考慮して、適切なフレームワークを選択することが重要です。どちらを選択しても、継続的なスキーマ管理と安全なデータベース操作を実現できます。