はじめに
目的
みなさん、Railsで開発される際には、基本的にマイグレーションファイルを使用してDBの変更を行っていると思います。
個人開発の場合は、特に気にすることなく、マイグレーションファイルを積んでdb:migrate
を実行、とあまり迷うことはありませんが、
複数人での開発時に、どのマイグレーションファイルが実行されている/いないのか、db:migrateを行うとschema.rbのタイムスタンプが変更されるが、どんな意味を持っているのかなどが分からなく、混乱したことはないでしょうか
そこで、今回は
1.マイグレーションファイルの実行有無がどのように管理されてるのか
2.schema.rbのタイムスタンプにどのように反映され、なんの意味を持つのか
など、マイグレーションの実行の有無とタイムスタンプの関係について説明します。
実行環境
Ruby: 2.6.5
Rails: 5.2.3
DB: postgresql
コード:mastodon(https://github.com/tootsuite/mastodon)
をサンプルに使っています。コードが実際のものになるので分かりやすくなるかなと思って使っただけで、特に深い意味はありません。
マイグレーションの基礎
マイグレーションの基礎については、Railsガイドや他のQiitaの記事で十分に説明されているので、今回は軽く流すだけに留めます。
https://railsguides.jp/active_record_migrations.html
マイグレーションファイルとは
マイグレーションとは(引用:Railsガイド)
マイグレーション (migration) はActive Recordの機能の1つであり、データベーススキーマを長期にわたって安定して発展・増築し続けることができるようにするための仕組みです。マイグレーション機能のおかげで、Rubyで作成されたマイグレーション用のDSL (ドメイン固有言語) を用いて、テーブルの変更を簡単に記述できます。スキーマを変更するためにSQLを直に書いて実行する必要がありません。
簡単に言うと使用するDBMS(mysqlやpostgres)に関わらず、一定の書き方でテーブルに変更を加えることができる機能ですね。
マイグレーションの仕組み
マイグレーションを行う際には、3つの登場人物が存在します。
- マイグレーションファイル
- データベースに対して行いたい変更を記述するもの
- スキーマファイル(schema.rb)
- 現状のデータベースの状態を保存するもの
- データベース
実行と結果流れとしては以下の通りになります。
1. マイグレーションファイルに変更内容を書く
2. マイグレーションを実行(db:migrate
)することで、データベースの内容が変更される
3. 変更された内容のデータベースの状態で、schema.rbが更新される
4. schema.rbのタイムスタンプが更新される
※このあたりの詳しい使い方やコマンドについてはRailsガイドなどを参照すると良いかと思います。
実行有無の確認
ここからが本題です。
マイグレーションファイルは変更が加えられるごとにその数が増えていきます。
さらに複数人開発では、自分が作成したもの、他の人が作成したものなどが入ってきて、実行したか/していないかがわかりづらくなります。
ただ、Railsではdb:migrateのタスクを実行すると、未実行のもののみが反映され、DBが更新されます。
- このマイグレーションの実行有無を管理しているものは何でしょうか?
また、
- migrationファイルのファイル名、schema.rb内にそれぞれ使われているタイムスタンプはどのような意味を持っているのでしょうか?
ActiveRecord::Schema.define(version: 2019_10_31_163205) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
マイグレーションファイルの実行有無
マイグレーションファイルの中で、その実行有無を確認するためには以下のコマンドを使用します。
rake(rails) db:migrate:status
この結果は以下のようになります。
upとなっているのが実行済み、downは未実行のマイグレーションファイルです。
〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜
up 20190927232842 Add voters count to polls
up 20191001213028 Add lock version to account stats
down 20191031163205 Change list account follow nullable
マイグレーションファイルの実行有無を検証しているのは「schema_migrations」と言うテーブルです。
db:migrate
を実行した時にこのschema_migrationのversionカラムに以下のようにタイムスタンプが保存されます。
development=# select * from schema_migrations
order by version desc;
version
----------------
20191031163205
20191007013357
20191001213028
20190927232842
〜〜〜〜〜〜〜〜〜〜〜
このタイムスタンプはマイグレーションファイルの先頭のタイムスタンプと一致します。
実際にdb:migrate
を実行した際のログを見てみましょう。
今回は初めてdb:migrate
を実行したので、schema_migrationsテーブルがcreateされていることがわかります。
初回のdb:migrate
ではテーブルが作成され、versionにタイムスタンプのレコードが追加されます。その後は実行するたびに、versionにタイムスタンプがinsertされます。
be rails db:migrate
# 初めてdb:migrateを行なったので、schema_migrationsテーブルが作成される
(4.7ms) CREATE TABLE "schema_migrations" ("version" character varying NOT NULL PRIMARY KEY)
(0.2ms) BEGIN
== 20160221003140 CreateUsers: migrating ======================================
-- create_table(:users, {:id=>:integer})
(4.3ms) CREATE TABLE "users" ("id" serial NOT NULL PRIMARY KEY, "email" character varying DEFAULT '' NOT NULL, "account_id" integer NOT NULL, "created_at" timestamp NOT NULL, "updated_at" timestamp NOT NULL)
-> 0.0052s
== 20160221003140 CreateUsers: migrated (0.0087s) =============================
# 実行されたマイグレーションファイルのファイル名に入っているタイムスタンプが、versionカラムに追加されている
ActiveRecord::SchemaMigration Create (0.5ms) INSERT INTO "schema_migrations" ("version") VALUES ($1) RETURNING "version" [["version", "20160221003140"]]
(0.6ms) COMMIT
(0.2ms) SELECT "schema_migrations"."version" FROM "schema_migrations" ORDER BY "schema_migrations"."version" ASC
ちなみにdb:rollback
ではversionから該当するタイムスタンプのレコードが削除されます。
そして、schema_migrationsが更新され、schema.rbのタイムスタンプは、versionに入っている中で一番値の大きい(新しい)で更新が行われます。
これによって、どのマイグレーションファイルが実行されたのか/されていないのかを判断することができるのです。
schema.rbにおけるタイムスタンプの意味
以上のことからマイグレーションファイルとschema.rbとの関係性が以下であると言うことがわかりました。
- マイグレーションファイルの実行有無はschema_migrationsテーブルで管理されている
- schema.rbのタイムスタンプは、マイグレーション実行後に、schema_migrationsの最大値で更新される
ここから判断すると、schema.rbのタイムスタンプは
「マイグレーションファイルの実行有無の管理には影響を受けるものであり、影響を与えるものではない。」
ということがわかります。
schema.rbのタイムスタンプがなくとも、マイグレーションファイルの実行有無は管理できており、マイグレーションでテーブルを変更をしていく分には、schema.rbのタイムスタンプは特別な意味を持たないというになるでしょう。
試しに、schema.rbのタイムスタンプを数年前にしてみましたが、
ActiveRecord::Schema.define(version: 2017_10_31_163205) do
db:migrate:status
の実行結果は以下の通り、変わりません。
〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜
up 20190927232842 Add voters count to polls
up 20191001213028 Add lock version to account stats
down 20191031163205 Change list account follow nullable
では、なぜschema.rbのタイムスタンプが使用されるのはどのような場合でしょうか。
それは、テーブル情報をdb:schema:load
によって変更する場合(他にもあるかもしれませんが)です。
db:schema:loadについて
db:schema:load
についても簡単に説明しておきます。
db:schema:load
は、schema.rbの情報をもとに、テーブル情報を変更します。
ちなみに気をつけたいのは、すでにテーブルが存在した場合には、そのテーブルを削除して作り直します。基本的にDBを作成したときにのみ行うものですね。
db:migrate
はmigrationファイルを実行するので、schema_migrationsに変更したマイグレーションファイルのタイムスタンプが追加されるのはわかりやすいですが、db:schema:load
を行った場合にはschema_migrationsの扱いはどうなるのでしょうか。
実際にdbを新規作成して、db:schema:load
を行なってみます。
使用するschema.rbのタイムスタンプ、migrationファイルの一覧は以下のようになっています。
ActiveRecord::Schema.define(version: 2019_10_31_163205) do
db:create
した後に、db:schema:load
を行い、schema_migrationテーブルが作成されているかを確認しました。
development=# select * from schema_migrations
order by version desc;
version
----------------
20191031163205
20191007013357
20191001213028
20190927232842
〜〜〜〜〜〜〜〜〜〜〜
db:migrate:statusの結果
〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜
up 20190927124642 Remove invalid web push subscription
up 20190927232842 Add voters count to polls
up 20191001213028 Add lock version to account stats
up 20191007013357 Update pt locales
up 20191031163205 Change list account follow nullable
この場合にも、schema_migrationファイルが作成され、migrationファイルのタイムスタンプが入った状態が確認できました。
ここで使われるのているのが、schema.rbファイルに示されているタイムスタンプです。
ActiveRecord::Schema.define(version: 2019_10_31_163205) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
このタイムスタンプ(2019_10_31_163205)までの日時を示しているmigrationファイルを、実行されたこととして、schema_migrationファイルのversionが追加されます。
これを確認するため、一度db:drop
, db:create
で作成し直し、schema.rbのタイムスタンプの日付を変更した上でdb:schema:load
してみます。
ActiveRecord::Schema.define(version: 2019_10_01_213028) do
schema_migrationのversionは以下のようになりました
development=# select * from schema_migrations
order by version desc;
version
----------------
20191001213028
20190927232842
20190927124642
念のため、db:migrate:status
の結果も見ておきましょう
〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜
up 20190927232842 Add voters count to polls
up 20191001213028 Add lock version to account stats
down 20191007013357 Update pt locales
down 20191031163205 Change list account follow nullable
schema.rbの変更されたタイムスタンプまでのmigrationファイルが実行されている状態が確認できました!
まとめ
ここまでを簡単まとめると以下のようになります。
- マイグレーションファイルの実行有無は、ファイル名のタイムスタンプをもとに、schema_migrationsテーブルによって管理される
db:migrate
を行なった際に、ファイル名のタイムスタンプがschema_migrationsに保存された上、実行されたschema_migrations内の最新のタイムスタンプが、schema.rbのタイムスタンプとして登録されるdb:schema:load
は、schema.rbのタイムスタンプ以前のファイル名を持ったマイグレーションファイルを実行されたものとして記録する
と言うことです。
タイムスタンプ管理で問題がおきそうなケース
最後に、今回まとめた知識をもとに、複数人開発でタイムスタンプによる問題がおきそうなケースを考えて終わりにしましょう。
まず、マイグレーションでDBを更新していく場合には、実際に実行されたものだけがschema_migrationsに保存されているので、DBの更新のみで考えれば問題はなさそうです。(「schema.rbのテーブル情報が更新されておらず、差分が発生した」などのコード管理の問題は別として)
問題となりそうなのは、新しい開発者などが入って、db:schema:load
によって、テーブル情報を作成する場合です。
ここでもし、最新のコードが以下の状態になっていればどうなるでしょうか?
- 開発者AがマイグレーションファイルAを作成したが、
db:migrate
を行わずにschema.rbを更新せずにコミットした - その後、別の開発者Bが上記のマイグレーションファイルAよりも新しいタイムスタンプでマイグレーションファイルBを作成、
db:migrate
を実行し、schema.rbも更新した上でコミットした - 上記のコード状態で、新しく入った開発者Cが
db:create
、db:schema:load
を行なった
開発者Aから見たmigrationファイルの状態
up 20190927232842 Add voters count to polls
down 20191001213028 Add lock version to account stats
up 20191031163205 Change list account follow nullable
この場合、schema.rbのタイムスタンプはマイグレーションファイルBのものになっています。
ActiveRecord::Schema.define(version: 2019_10_31_163205) do
そのため、db:schema:load
を行うと、マイグレーションファイルAも実行されたこととして、schema_migrationsに記録されます。
この状態でdb:rollback
を行おうとすると、実行されていないマイグレーションファイルAもロールバック対象となるため、例えばマイグレーションファイルAの内容がテーブルの作成であれば、存在しないテーブルを削除しようとし、エラーが発生します。
db:schema:load
後に、マイグレーションファイルAのdb:rollback
を実行
StandardError: An error has occurred, all later migrations canceled:
PG::UndefinedColumn: ERROR: column "lock_version" of relation "account_stats" does not exist
: ALTER TABLE "account_stats" DROP COLUMN "lock_version"
他にも色々と問題は起こるかもしれませんが、一例として挙げてみました。
最後に
最後までご覧頂き、ありがとうございます。
もし内容の間違いや、こんなケースも考えられるということがあれば、是非おしらせください。