Railsには、スキーマファイルと呼ばれる schema.rb があります。
Railsガイドには
Active Recordはマイグレーションの時系列に沿ってスキーマを更新する方法を知っているので、履歴のどの時点からでも最新バージョンのスキーマに更新できます。Active Recordは
db/schema.rbファイルを更新し、データベースの最新の構造と一致するようにします。
とあるように、schema.rb はデータベース関連の機能で使われます。
その schema.rb ですが、チームで開発をしている時に
- 自分の作業を中断して、プルリクエストのレビューをする
- 自分の作業に戻って、開発を続ける
を繰り返している中で、意図しない差分が schema.rb に発生していると気づき、戸惑うことがあるかもしれません。
そこで、schema.rb に差分が発生してしまうことについて
- 発生する事例
- 差分が発生していると何が起こるか
- どう復旧するか
を記事としてまとめます。
発生する事例
発生する事例として、「どのような流れで発生してしまうのか」を体験できるチュートリアルを用意しました。
チュートリアルは以下の流れで進めます。なお、Railsのバージョンは 7.0.4.2 を想定しています。
- mainブランチで、Railsのモデルを作成
- mainから派生した第2のブランチ(
feature/add_unique_index)で、モデルの変更を伴うマイグレーションを適用 - 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:
とテストが失敗してしまいます。
これは、Railsガイドで解説があるように、テストでは schema.rb を見てテスト用のデータベースを作っているためです。
今回の場合、UNIQUE制約を追加するマイグレーションファイルが存在しない第3のブランチ(feature/add_column)でも、UNIQUE制約付きでテスト用のデータベースが作成されてしまった結果、テストが失敗しました。
どう復旧するか
今回は、以下の流れで復旧していきます。
- UNIQUE制約を追加した第2のブランチ(
feature/add_unique_index)へ切り替え、UNIQUE制約のマイグレーション適用をロールバックする - 第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 に余分な更新がないかチェックするのが良さそうです。
本サイトの更新情報は、Twitterの株式会社プレセナ・ストラテジック・パートナーズエンジニア公式で発信しています。ご確認ください。