Precena Tech Book
コーポレートサイト採用サイト
  • はじめに
  • ソフトウェア開発
    • 開発環境構築
      • Homebrew
        • Homebrew用語の意味
      • ngrok
        • ngrokの導入
        • ngrokのアップグレード(v2 to v3)
      • Slack
        • Slackの/remind コマンドの形式
        • 対面での相談を気軽にするためのSlack設定
      • AWS CLI
      • Ruby
      • Scala
      • Prettier
      • zsh
        • zsh-completion
      • Mac
        • M1 Macでの開発環境構築(rosetta 無し)
    • バックエンド
      • OpenAPI
        • OpenAPI 定義ファイル分割のすゝめ
      • Ruby on Rails
        • ActiveRecordのfind_or_initialize_byメソッドにブロックを渡したときの挙動
        • Railsのアプリケーションサーバーのプロセス数とスレッド数の設定方法
        • Railsを6.1系から7.0系へアップグレードした時に調査したこと
        • schema.rbで差分が発生する事例とその復旧について
        • tmux + overmind を利用して、複数システムを1コマンドで起動できるよう設定する
        • Rails Migrationチートシート
        • GithubのプライベートリポジトリをGemfileで参照する方法
        • ActiveSupportのto_jsonメソッドの注意点
        • 危険なJSON出力を禁止するRuboCopカスタムルールの作成方法
      • Scala
        • Validated を直列に処理したい
      • DB
        • PostgreSQLにおける、削除行に対するロック獲得時の挙動
    • フロントエンド
      • React
        • Storybookを利用したビジュアルリグレッションテスト
  • インフラ開発
    • AWS
      • IAM
        • スイッチロールの設定手順
        • AWS CLIでのスイッチロールの設定手順
        • AWS Vaultを使ったスイッチロール設定手順
        • Github ActionsでIAMロールを利用してAWSリソースを操作する
      • ECS
      • SES
        • AWS SESメールボックスシミュレーターにて、カスタムヘッダや添付ファイル付きのテストEメールを送信する
      • CloudWatch
        • Amazon SNS + Slack Workflowを使って、CloudWatch Alarmの通知をSlackチャンネルへ投稿する
      • Lambda
        • lambrollでAWS Lambda関数をデプロイしたときのTips
    • Heroku
      • HerokuのStackの設定
      • Heroku Postgresの運用でよく使うコマンド集
  • セキュリティ
    • Web
      • Same Origin PolicyとCORS
      • 脆弱性診断 2社同時依頼実施記録
  • Mail
    • SPF、DKIM、DMARCを使用した迷惑メール対策
  • データ分析
    • データ分析プロセス
  • SaaS
    • Zendesk
      • 問い合わせフォームの項目をサービスごとに出し分け、各サービス担当者に自動で振り分けてメールで通知する
  • イベント
    • RubyKaigi
      • RubyKaigi 2023 に現地参加しました
    • EMConf
      • EMConfJP2025_参加レポート
  • やってみた
    • IoT
      • Raspberry Pi + PaSoRi + Python で、勤怠打刻マシンを作ってみた
  • Precena Tech Book 管理
    • コンテンツ執筆時のルール
  • 関連リンク
    • プレセナエンジニア公式Twitter
GitBook提供
このページ内
  • 発生する事例
  • 差分が発生していると何が起きるか
  • どう復旧するか
  • おわりに

役に立ちましたか?

PDFとしてエクスポート
  1. ソフトウェア開発
  2. バックエンド
  3. Ruby on Rails

schema.rbで差分が発生する事例とその復旧について

前へRailsを6.1系から7.0系へアップグレードした時に調査したこと次へtmux + overmind を利用して、複数システムを1コマンドで起動できるよう設定する

最終更新 2 年前

役に立ちましたか?

Railsには、スキーマファイルと呼ばれる schema.rb があります。

には

Active Recordはマイグレーションの時系列に沿ってスキーマを更新する方法を知っているので、履歴のどの時点からでも最新バージョンのスキーマに更新できます。Active Recordは db/schema.rb ファイルを更新し、データベースの最新の構造と一致するようにします。

とあるように、schema.rb はデータベース関連の機能で使われます。

その schema.rb ですが、チームで開発をしている時に

  • 自分の作業を中断して、プルリクエストのレビューをする

  • 自分の作業に戻って、開発を続ける

を繰り返している中で、意図しない差分が schema.rb に発生していると気づき、戸惑うことがあるかもしれません。

そこで、schema.rb に差分が発生してしまうことについて

  • 発生する事例

  • 差分が発生していると何が起こるか

  • どう復旧するか

を記事としてまとめます。

発生する事例

発生する事例として、「どのような流れで発生してしまうのか」を体験できるチュートリアルを用意しました。

チュートリアルは以下の流れで進めます。なお、Railsのバージョンは 7.0.4.2 を想定しています。

  1. mainブランチで、Railsのモデルを作成

  2. mainから派生した第2のブランチ(feature/add_unique_index)で、モデルの変更を伴うマイグレーションを適用

  3. mainから派生した第3のブランチ(feature/add_column)で、モデルの変更を伴うマイグレーションを適用

1. mainブランチで、Railsのモデルを作成

最小構成で rails new します。後ほどgemを追加するため、現時点では bundle install を行いません。

% bundle exec rails new schemaapp --minimal --skip-bundle

% cd schemaapp

今回の動作確認をRSpecで行うため、 Gemfile の末尾に追加します。

group :test do
  gem 'rspec-rails', '~> 6.0.1'
end

Gemfileの準備ができたため、一連のgemをインストールします。

% bundle install

RSpecのセットアップも行います。

% bin/rails g rspec:install

続いてモデルを用意します。今回は isbn 列を持つBookモデルとします。

% bin/rails g model Book isbn:string

マイグレーション適用後に、状態を確認しておきます。

% bin/rails db:migrate

% bin/rails db:migrate:status

database: db/development.sqlite3

 Status   Migration ID    Migration Name
--------------------------------------------------
   up     20230306095752  Create books

ここまでをコミットします。 (もし vendor/bundle 以下をコミットしたくない場合は .gitignore に追加しておきます)

% git init
% git add .
% git commit -m "first commit"

2. mainから派生した第2のブランチ(feature/add_unique_index)で、モデルの変更を伴うマイグレーションを適用

main ブランチから第2のブランチ( feature/add_unique_index )を新しく作成し、「Bookの isbn にUNIQUE制約を追加する」ためのマイグレーションファイルを生成します。

% git checkout -b feature/add_unique_index

% bin/rails g migration AddIndexToBook

生成したマイグレーションファイルにて、Bookの isbnにUNIQUE制約を追加します。

class AddIndexToBook < ActiveRecord::Migration[7.0]
  def change
    add_index :books, :isbn, unique: true
  end
end

マイグレーションを適用後、状況を確認します。

% bin/rails db:migrate

% bin/rails db:migrate:status

database: db/development.sqlite3

 Status   Migration ID    Migration Name
--------------------------------------------------
   up     20230306095752  Create books
   up     20230306104025  Add index to book

作業が終わったため、コミットします。

% git add .

% git commit -m "add index"

3. mainから派生した第3のブランチ(feature/add_column)で、モデルの変更を伴うマイグレーションを適用

ここからが、意図しない差分を発生させてしまう手順になります。

本来ならば、第2のブランチ(feature/add_unique_index)を離れる前にマイグレーションをロールバックします。

ただ、今回は schema.rbに差分を発生させるため、マイグレーションを適用したまま、

  • 第3のブランチ(feature/add_column)を新しく作成・切り替え

  • モデルに列 nameを追加

  • マイグレーションを適用

を行います。

% git checkout main

% git checkout -b feature/add_column

% bin/rails g migration AddColumnToBook name:string

% bin/rails db:migrate

続いて、マイグレーションの状態を確認します。作業2.のマイグレーションが適用されたまま、今回のマイグレーションも適用されているのが分かります。

% bin/rails db:migrate:status

database: db/development.sqlite3

 Status   Migration ID    Migration Name
--------------------------------------------------
   up     20230306095752  Create books
   up     20230306104025  ********** NO FILE **********
   up     20230306104231  Add column to book

schema.rb を見ると isbn 列にUNIQUE制約が定義されており、意図しない状態となっています。

ActiveRecord::Schema[7.0].define(version: 2023_03_06_104231) do
  create_table "books", force: :cascade do |t|
    t.string "isbn"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.string "name"
    t.index ["isbn"], name: "index_books_on_isbn", unique: true
  end
end

最後に、「この状態に気づかずに、うっかりコミット」した想定にしておきます。

% git add .

% git commit -m "add column"

差分が発生していると何が起きるか

今回はテストコードを使って、何が起こるかを見ていきます。

「第3のブランチ(feature/add_column)ではまだUNIQUE制約を付けるマイグレーションがないから、テストはパスするだろう」と考え、以下のテストコードを書いたとします。

require 'rails_helper'

RSpec.describe Book, type: :model do
  describe 'schema.rbの確認' do
    before { Book.create!(isbn: '4797399848') }

    it 'UNIQUE制約がないこと' do
      expect { Book.create!(isbn: '4797399848') }.not_to raise_error
    end
  end
end

しかし、RSpecを実行すると

expected no Exception, got #<ActiveRecord::RecordNotUnique: SQLite3::ConstraintException: UNIQUE constraint failed: books.isbn> with backtrace:

とテストが失敗してしまいます。

今回の場合、UNIQUE制約を追加するマイグレーションファイルが存在しない第3のブランチ(feature/add_column)でも、UNIQUE制約付きでテスト用のデータベースが作成されてしまった結果、テストが失敗しました。

どう復旧するか

今回は、以下の流れで復旧していきます。

  1. UNIQUE制約を追加した第2のブランチ(feature/add_unique_index)へ切り替え、UNIQUE制約のマイグレーション適用をロールバックする

  2. 第3のブランチ(feature/add_column)へ切り替え、 db:migrate を実行する

順に見ていきます。

1. UNIQUE制約を追加した第2のブランチ(feature/add_unique_index)へ切り替え、UNIQUE制約のマイグレーション適用をロールバックする

UNIQUE制約を追加した第2のブランチ( feature/add_unique_index )へ切り替え、マイグレーションの状況を確認します。

Migration IDに対するNameのうち、NO FILE と表示されていたものが Add index to book という表示へと切り替わりました。

% git checkout feature/add_unique_index
% bin/rails db:migrate:status

database: db/development.sqlite3

 Status   Migration ID    Migration Name
--------------------------------------------------
   up     20230306095752  Create books
   up     20230306104025  Add index to book
   up     20230306104231  ********** NO FILE **********

続いて、第2のブランチ( feature/add_unique_index )上で、不要なマイグレーション適用を元に戻します。

なお、 db:migrate:rollback コマンドでは1ステップずつしか戻せないので、バージョンを指定できるコマンドで戻します。

% bin/rails db:migrate:down VERSION=20230306104025

再度マイグレーションの状況を確認すると、当該Migration IDが down という未適用状態になりました。

% bin/rails db:migrate:status    

database: db/development.sqlite3

 Status   Migration ID    Migration Name
--------------------------------------------------
   up     20230306095752  Create books
  down    20230306104025  Add index to book
   up     20230306104231  ********** NO FILE **********

ただ、この時点では schema.rb に変更が入っていることから、第3のブランチ(feature/add_column)へ切り替えようとしても

% git checkout feature/add_column
error: Your local changes to the following files would be overwritten by checkout:
	db/schema.rb
Please commit your changes or stash them before you switch branches.
Aborting

と、エラーになってしまいます。

そこで、 schema.rb の変更を、当ブランチのコミット時点に戻しておきます。

% git reset HEAD db/schema.rb
% git checkout db/schema.rb

2. 第3のブランチ(feature/add_column)へ切り替え、 db:migrate を実行する

あらためて第3のブランチ(feature/add_column)へ切り替えます。

% git checkout feature/add_column

ここでマイグレーション適用状況を確認すると、第3のブランチ(feature/add_column )のマイグレーションのみが適用・表示されています。

% bin/rails db:migrate:status    

database: db/development.sqlite3

 Status   Migration ID    Migration Name
--------------------------------------------------
   up     20230306095752  Create books
   up     20230306104231  Add column to book

ただ、まだ schema.rb にはUNIQUE制約の定義が残ったままになっています。

そこで、もう一回 db:migrate を実行し、 schema.rb を更新します。

% bin/rails db:migrate

ターミナルの実行結果には何も表示されないものの、git status ではschema.rb の更新が確認できます。

% git status
On branch feature/add_column
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
	modified:   db/schema.rb

更新後の schema.rb を見ると、UNIQUE制約の定義が削除されています。

ActiveRecord::Schema[7.0].define(version: 2023_03_06_104231) do
  create_table "books", force: :cascade do |t|
    t.string "isbn"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.string "name"
  end
end

これでようやく schema.rb が復旧しました。

あとは忘れずに schema.rbを第3のブランチ(feature/add_column)へコミットすれば、復旧は完了です。

おわりに

今回は schema.rbに差分が発生する事例とその復旧方法を見てきました。

復旧するのには手間がかかることから、もしプルリクエストにマイグレーションファイルが含まれている場合は、schema.rb に余分な更新がないかチェックするのが良さそうです。

これは、で解説があるように、テストでは schema.rb を見てテスト用のデータベースを作っているためです。

本サイトの更新情報は、Twitterので発信しています。ご確認ください。

Railsガイド
Railsガイド
株式会社プレセナ・ストラテジック・パートナーズエンジニア公式