🐒

[Ridgepole/MySQL]Railsのreferencesとcreate_tableで作られるidカラムを必ずUNSIGNEDにする

2021/04/23に公開

Railsのマイグレーションでreferencesとcreate_tableで作られるidカラムはBIGINT型としてADD COLUMNされるため -9223372036854775808から9223372036854775807 までの値を格納できます。しかし主キー・外部キーには負数が入ることはないので(ないよね?w)UNSIGNEDをつけておきたいのですね。ただ毎回書くのが面倒ですし、つけ忘れてしまうことやレビューで指摘が漏れることがありました。

例えば、下記の場合は users.id はUNSIGNEDですが、posts.id, posts.user_id がただのBIGINT型のカラムになるので負数が格納できてしまいます。

# ./Schemafile

create_table(:users, unsigned: true) do |t|
  t.timestamps
end

create_table(:posts) do |t|
  t.references, :user, null: false
  
  t.timestamps
end

ここを気にするのは面倒なので暗黙的にUNSIGNEDなカラムになるようにしてみようと思います。

どうやってやるのか

Ridgepoleを拡張するファイルを用意しておき、Schemafileの内部でそのファイルをrequireしてモンキーパッチ🐒を当てる形式を採用してます。

# ./Schemafile

+ require 'lib/ridgepole/extension'

- create_table(:users, unsigned: true) do |t|
+ create_table(:users) do |t|
  t.timestamps
end

create_table(:posts) do |t|
  t.references, :user, null: false
  
  t.timestamps
end

こちらのモンキーパッチではデフォルトのマイグレーションと同じようにconfig/database.ymlのtable_optionsに記載している設定がテーブルに当たるようにもしています。

# lib/ridgepole/extension.rb

module UnsingedReferences
  def references(*args)
    options = args.extract_options!
    options[:unsigned] = true
    args.push(options)

    super(*args)
  end

  Ridgepole::DSLParser::TableDefinition.prepend(self)
end

module WithDefaultTableOptions
  DEFAULT_TABLE_OPTIONS = {
    id: :unsigned_bigint,
    options: YAML.safe_load(
      ERB.new(File.read('config/database.yml')).result,
      [],
      [],
      true
    )[Rails.env]['table_options']
  }.freeze

  def create_table(table_name, options = {})
    super(table_name, DEFAULT_TABLE_OPTIONS.dup.merge(options))
  end

  Ridgepole::DSLParser::Context.prepend(self)
end

ネームスペースが残るのが気になる方は下記のようにすると無名モジュールがモンキーパッチを当ててくれるはずです。(こちらは動作未確認です)

# lib/ridgepole/extension.rb

Module.new do
  def references(*args)
    options = args.extract_options!
    options[:unsigned] = true
    args.push(options)

    super(*args)
  end

  Ridgepole::DSLParser::TableDefinition.prepend(self)
end

Module.new do
  DEFAULT_TABLE_OPTIONS = {
    id: :unsigned_bigint,
    options: YAML.safe_load(
      ERB.new(File.read('config/database.yml')).result,
      [],
      [],
      true
    )[Rails.env]['table_options']
  }.freeze

  def create_table(table_name, options = {})
    super(table_name, DEFAULT_TABLE_OPTIONS.dup.merge(options))
  end

  Ridgepole::DSLParser::Context.prepend(self)
end

終わりに

モンキーパッチなのでRidgepoleの挙動が壊れてDDLの内容が変わったらどうするんだ!という方はRidgepoleが発行するDDLをGitHub PR上で確認できるようにすることができるので、Ridgepoleのバージョンアップ時にこちらで気づくことができるかと思います!

今回紹介したUNSIGNEDを暗黙的に付与する方法は会社の先輩であるalpaca_tcが教えてくれたものでした。いつもありがとうございます🙏

Discussion