読者です 読者をやめる 読者になる 読者になる

scramble cadenza

技術ネタのガラクタ置き場

rails で id を BIGINT 型 + primary key + AUTO_INCREMENT する

イントロ

rails のモデルは自動で id というカラムを作ってくれますが、これは常に INT 型。
BIGINT 型にしようとしたら、結構ハマったのでメモする。

使っている database はmysql です。
(postgresql だとこの罠は回避できるのだろうか?)

tl;dr

  • 普通に頑張ると...
    • db:migratedb:setup の実行結果が異なる
      • migration ファイルの内容が、schema.rb に(一部)反映されないため
    • db:migratedb:setup の実行結果が異なると、 db:rollback ができなくなるので、辛くなりそう。
      • db:migratedb:setup どちらかを使わない、という運用方式もある
      • だが、面倒くさいので、そういうことを意識したくない
  • 解決方法は 2 つ

普通にやってみる

Rails探訪 ~ create_table 編 ~ | マネーフォワード エンジニアブログ の素晴らしい参考記事によると、こんな感じ。

(ところで参考記事を見ると、色々地雷を踏み抜いてこの結論なので、この時点で既に普通でない気もする)

class CreateUsers < ActiveRecord::Migration
  def change
    create_table :users, id: false do |t|
      t.column :id, 'BIGINT PRIMARY KEY AUTO_INCREMENT'
      t.string :name

      t.timestamps null: false
    end
  end
end

rake db:migrate

% rake db:drop db:create db:migrate
mysql> desc users;
+------------+--------------+------+-----+---------+----------------+
| Field      | Type         | Null | Key | Default | Extra          |
+------------+--------------+------+-----+---------+----------------+
| id         | bigint(20)   | NO   | PRI | NULL    | auto_increment |
| name       | varchar(255) | YES  |     | NULL    |                |
| created_at | datetime     | NO   |     | NULL    |                |
| updated_at | datetime     | NO   |     | NULL    |                |
+------------+--------------+------+-----+---------+----------------+
4 rows in set (0.00 sec)

無事成功。

rake db:setup

一見うまくいってそうだが、実はこれは問題がある。

db/scheme.rbBIGINT 型にしたよ、という内容が反映されていないために、db:setup をやると db:migrate の結果と異なったものになってしまう。

% rake db:setup
mysql> desc users;
+------------+--------------+------+-----+---------+----------------+
| Field      | Type         | Null | Key | Default | Extra          |
+------------+--------------+------+-----+---------+----------------+
| id         | int(11)      | NO   | PRI | NULL    | auto_increment |
| name       | varchar(255) | YES  |     | NULL    |                |
| created_at | datetime     | NO   |     | NULL    |                |
| updated_at | datetime     | NO   |     | NULL    |                |
+------------+--------------+------+-----+---------+----------------+
4 rows in set (0.01 sec)

INT型 に戻ってしまった。
じゃあどうするの?と言うと、schema_format = :sql 方式か activerecord-mysql-awesome 方式の二通りがある。

schema_format = :sql

よく見かける解決法。 デフォルトでは schema_formatruby なので、これを sql にする。
:ruby では db/schema.rb を生成するが、:sql にすると db/structure.sql を生成してくれる。

config/application.rb 等に予め設定しておく。

config.active_record.schema_format = :sql

rake db:migrate

migration ファイルはここと同じものを使う。

mysql> desc users;
+------------+--------------+------+-----+---------+----------------+
| Field      | Type         | Null | Key | Default | Extra          |
+------------+--------------+------+-----+---------+----------------+
| id         | bigint(20)   | NO   | PRI | NULL    | auto_increment |
| name       | varchar(255) | YES  |     | NULL    |                |
| created_at | datetime     | NO   |     | NULL    |                |
| updated_at | datetime     | NO   |     | NULL    |                |
+------------+--------------+------+-----+---------+----------------+
4 rows in set (0.01 sec)

bigint 型になっている。ここまでは良い

rake db:setup

% rake db:setup
mysql> desc users;
+------------+--------------+------+-----+---------+----------------+
| Field      | Type         | Null | Key | Default | Extra          |
+------------+--------------+------+-----+---------+----------------+
| id         | bigint(20)   | NO   | PRI | NULL    | auto_increment |
| name       | varchar(255) | YES  |     | NULL    |                |
| created_at | datetime     | NO   |     | NULL    |                |
| updated_at | datetime     | NO   |     | NULL    |                |
+------------+--------------+------+-----+---------+----------------+
4 rows in set (0.00 sec)

今度は migratesetup の結果が同じになりました。

activerecord-mysql-awesome

kamipo/activerecord-mysql-awesome

を使う。 この場合 schema_format:ruby のままで良い。

class CreateUsers < ActiveRecord::Migration
  def change
    create_table :users, id: :bigint do |t|
      t.string :name

      t.timestamps null: false
    end
  end
end

のような migration ファイルを作る

rake db:migrate

% rake db:drop db:create db:migrate
mysql> desc users;
+------------+--------------+------+-----+---------+----------------+
| Field      | Type         | Null | Key | Default | Extra          |
+------------+--------------+------+-----+---------+----------------+
| id         | bigint(20)   | NO   | PRI | NULL    | auto_increment |
| name       | varchar(255) | YES  |     | NULL    |                |
| created_at | datetime     | NO   |     | NULL    |                |
| updated_at | datetime     | NO   |     | NULL    |                |
+------------+--------------+------+-----+---------+----------------+
4 rows in set (0.00 sec)

rake db:setup

% rake db:setup
mysql> desc users;
+------------+--------------+------+-----+---------+----------------+
| Field      | Type         | Null | Key | Default | Extra          |
+------------+--------------+------+-----+---------+----------------+
| id         | bigint(20)   | NO   | PRI | NULL    | auto_increment |
| name       | varchar(255) | YES  |     | NULL    |                |
| created_at | datetime     | NO   |     | NULL    |                |
| updated_at | datetime     | NO   |     | NULL    |                |
+------------+--------------+------+-----+---------+----------------+
4 rows in set (0.00 sec)

migratesetup の結果が同じになった。

change_column 編

create_table ではなく、change_column でやったらどうなるか。 create_table の時点で INTBIGINT にしようと思うことが少ないので、実は change_column で対応できるかどうかが重要。

create_table の時と違って、今度は db:rollback が成功するかどうかもチェックしなければならない。

activerecord-mysql-awesome / schema_format = :sql

sql の確認コマンドは鬱陶しいので省略。

class ChangeIdColumnToUsers < ActiveRecord::Migration
  def change
    reversible do |dir|
      dir.up do
        change_column :users, :id, :bigint, auto_increment: true
      end

      dir.down do
        change_column :users, :id, :int, auto_increment: true
      end
    end
  end
end

この migration ファイルで

  • rake db:drop db:create db:migrate
  • rake db:setup
  • rake db:rollback

が成功する。ばっちり。
activerecord-mysql-awesome でも schema_format = :sql でも同じ結果になったので、change_column の時はこうすれば良さそう。

普通にやる

勿論これも activerecord-mysql-awesomeschema_format = :sql をせずに migration しても、 db:setup で id が INT型 になってしまう。

おまけ

https://github.com/rails/rails/pull/18220 がマージされているので、現時点での rails の master では対応済みのよう。
なので、 第三の選択「もうしばらく待つ」 も有りかも。

少なくとも Rails 4.2.1 では修正は含まれていなかったです。