Rails の migration の仕組みを理解する

普段 Rails で開発をしていて、migration の仕組みって、日付で migration ファイルが作成されて管理されてるってイメージで確認したことなかったので、動かしながら確認してみた。

とりあえず `rails new` してアプリを立ち上げるところまではやって DB を作るところからいじってみた。
DBは PostgreSQL を使っています。

# 初期状態
```console:rails
➜  rails_migration git:(master) bin/rails db:create
Created database 'rails_migration_development'
```

この時点ではまだテーブルはない。

```console:postgresql
rails_migration_development=# \d
Did not find any relations.
```

# migration を実行して中身を確認
とりあえず model を作成

```console:rails
➜  rails_migration git:(master) bin/rails g model Article title body
      invoke  active_record
      create    db/migrate/20190707095850_create_articles.rb
      create    app/models/article.rb

➜  rails_migration git:(master) ✗ bin/rails db:migrate
== 20190707095850 CreateArticles: migrating ===================================
-- create_table(:articles)
   -> 0.0129s
== 20190707095850 CreateArticles: migrated (0.0131s) ==========================
```

すると 4 つテーブルができる。今回見ていくのは `schema_migrations` 。

```console:postgresql
rails_migration_development=# \d
                 List of relations
 Schema |         Name         |   Type   | Owner
--------+----------------------+----------+--------
 public | ar_internal_metadata | table    | ikepon
 public | articles             | table    | ikepon
 public | articles_id_seq      | sequence | ikepon
 public | schema_migrations    | table    | ikepon
```

この `schema_migrations` には `version` ってカラムが一つだけある。

```console:postgresql
rails_migration_development=# \d schema_migrations
               Table "public.schema_migrations"
 Column  |       Type        | Collation | Nullable | Default
---------+-------------------+-----------+----------+---------
 version | character varying |           | not null |
Indexes:
    "schema_migrations_pkey" PRIMARY KEY, btree (version)
```

中の値を見ると、さっきの migration ファイルの日付の情報が入ってる。
(db/migrate/20190707095850_create_articles.rb)

```console:postgresql
rails_migration_development=# SELECT * FROM schema_migrations;
    version
----------------
 20190707095850
(1 row)
```

# rollback してみる
これを rollback すると値も消える。

```console:rails
➜  rails_migration git:(master) ✗ bin/rails db:rollback
== 20190707095850 CreateArticles: reverting ===================================
-- drop_table(:articles)
   -> 0.0245s
== 20190707095850 CreateArticles: reverted (0.0287s) ==========================
```

```console:postgresql
rails_migration_development=# SELECT * FROM schema_migrations;
 version
---------
(0 rows)
```

# もう一つテーブルを追加
テーブルを2つにして確認してみる

```console:rails
➜  rails_migration git:(master) ✗ bin/rails g model User name email
      invoke  active_record
      create    db/migrate/20190707101406_create_users.rb
      create    app/models/user.rb
➜  rails_migration git:(master) ✗ bin/rails db:migrate
== 20190707095850 CreateArticles: migrating ===================================
-- create_table(:articles)
   -> 0.0129s
== 20190707095850 CreateArticles: migrated (0.0131s) ==========================

== 20190707101406 CreateUsers: migrating ======================================
-- create_table(:users)
   -> 0.0054s
== 20190707101406 CreateUsers: migrated (0.0056s) =============================
```

すると PostgreSQL には 2つの `version` ができる。
(db/migrate/20190707095850_create_articles.rb)
(db/migrate/20190707101406_create_users.rb)

```console:postgresql
rails_migration_development=# SELECT * FROM schema_migrations;
    version
----------------
 20190707095850
 20190707101406
(2 rows)
```

# 日付が前の migration を実行してみる
rollback して user のファイル名を
`db/migrate/20190707101406_create_users.rb` から
`db/migrate/20190706101406_create_users.rb` に修正して migration を実行する。

すると変更した日付の値が入る。

```console:postgresql
rails_migration_development=# SELECT * FROM schema_migrations;
    version
----------------
 20190707095850
 20190706101406
(2 rows)
```

`version` にない日付の migration は実行されないと判断されて、実行される。

これを `bin/rails db:rollback` すると新しいものから消えていく

```console:postgresql
rails_migration_development=# SELECT * FROM schema_migrations;
    version
----------------
 20190706101406
(1 row)
```

# 同じファイルで日付を変えてみる
`bin/rails db:migrate` でテーブルを戻しておいて、 user のファイルをコピーして
`db/migrate/20190707101406_create_users.rb` と一番新しい日付に修正後 migration を実行する。

```console:rails
➜  rails_migration git:(master) ✗ bin/rails db:migrate
rails aborted!
ActiveRecord::DuplicateMigrationNameError:

Multiple migrations have the name CreateUsers.
```

# migration の名前を変えてみる
`Multiple migrations have the name CreateUsers.` 同じ名前の migration があるって言われてるので、 migration の名前を `class CreateUser < ActiveRecord::Migration[5.2]` (Users の s を削除)に変えて実行してみる。

するとエラーが変わった。

```console:rails
➜  rails_migration git:(master) ✗ bin/rails db:migrate
== 20190707101406 CreateUser: migrating =======================================
-- create_table(:users)
rails aborted!
StandardError: An error has occurred, this and all later migrations canceled:

PG::DuplicateTable: ERROR:  relation "users" already exists
```

さらに古い方(db/migrate/20190706101406_create_users.rb) を削除して再実行してみる。

```console:rails
➜  rails_migration git:(master) ✗ bin/rails db:migrate
== 20190707101406 CreateUser: migrating ======================================
-- create_table(:users)
rails aborted!
StandardError: An error has occurred, this and all later migrations canceled:

PG::DuplicateTable: ERROR:  relation "users" already exists
```

# まとめ
- migration ファイルは日付で判断して、 `schema_migrations` にないものを実行するようになっている。
- migration 内では同じ名前の class は使えない。

```rails
class CreateUser < ActiveRecord::Migration[5.2]
    # ~~~~~~~~~~ この部分
```

改めて手を動かして、テーブルのデータを確認しながら見ていくとちゃんと理解できた。

# 参考
- https://medium.com/@tushartuteja/demystifying-rails-migrations-53abcf3a7ddd