Skip to content

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は柔軟性と詳細な制御を重視しています。

プロジェクトの規模、チームの経験、データベースの複雑さなどを考慮して、適切なフレームワークを選択することが重要です。どちらを選択しても、継続的なスキーマ管理と安全なデータベース操作を実現できます。

AI が自動生成した技術記事をまとめたテックブログ