0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

RailsでUUIDを使うときに`references`で必ず`type: :uuid`を指定すべき理由とハマりどころ

Posted at

概要

Railsアプリケーションで主キーにUUIDを使っているとき、関連テーブル側の外部キーを普通にt.references :userのように書くと、意図せずnil0として扱われて外部キー制約エラーになったり、関連が正しく解決されなかったりすることがあります。これは、Railsのデフォルトが外部キーを整数(bigint/integer)として扱うためで、UUIDの主キーと型が一致していないことが原因です。
この問題を防ぐには、マイグレーションファイルで明示的にtype: :uuidを指定してreferencesを定義する必要があります。

典型的な症状(ハマり例)

  • find_or_initialize_by(user: current_user, chatroom: chatroom) を呼び出しているのに、user_id / chatroom_idnil 扱いされて INSERT 時に 0 になり、外部キー制約違反になる。
  • すでにオブジェクトのID(UUID)がログに出ているのに、関連検索で user_id = nil のようなクエリが投げられる。
    → これは内部的に型不一致によりActiveRecordが関連を正しく解釈できていないケースが多い。

なぜ起きるのか

Railsのreferencesbelongs_toは、デフォルトでは整数型(bigint/integer)の外部キーを想定しており、主キーとしてUUIDを使っているテーブルと関連付けるとき、外部キー側もUUIDの型に合わせて明示的に指定しないと型の不一致が起きます。
結果として、current_user.id(UUID)を正しく渡しているはずでも、ActiveRecord内部で比較に使う値が期待とずれ、nil扱いになったり、nil.to_i0 としてSQLに入ってしまったりします。これは少なくともStack Overflowや公式ガイド、実務での知見としても報告されている問題です。

正しい定義の例(マイグレーション)

UUIDを主キーにしたテーブルを作成する例:

class CreateUsers < ActiveRecord::Migration[7.0]
  def change
    enable_extension 'pgcrypto' unless extension_enabled?('pgcrypto') # PostgreSQL の場合
    create_table :users, id: :uuid, default: 'gen_random_uuid()' do |t|
      t.string :name
      t.timestamps
    end
  end
end

この users テーブルの主キーはUUIDになっています。
この users を参照する側(例:last_reads)では、以下のように書かないと型の不一致で問題になります:

class CreateLastReads < ActiveRecord::Migration[7.2]
  def change
    create_table :last_reads do |t|
      t.references :user, null: false, foreign_key: true, type: :uuid 
      t.references :chatroom, null: false, foreign_key: true, type: :uuid 
      t.integer :last_read_message_id, null: false

      t.timestamps
    end

    add_index :last_reads, [:user_id, :chatroom_id], unique: true
  end
end

このように type: :uuid を付けることで外部キーのカラムもUUIDになり、参照先の主キーと一致する。公式ガイドでも「外部キーのカラムが参照先の主キーと同じデータ型になるように明示的に指定する」ことが推奨されている。

補足:なぜ type: :uuid を書く必要があるか

公式ドキュメントでは、UUID主キーを使うテーブルに対して外部キーを作るとき、defaultsが整数なので不一致になると明記されており、t.references :author, type: :uuid, foreign_key: true のように型を合わせる例が紹介されている。
また、Railsの内部実装や実務の知見として「デフォルトではBigIntを想定しているので、UUIDの外部キーを使いたければ明示的に指定する必要がある」ことも言及されている。

関連する設定(全体をUUID前提にする)

毎回マイグレーションで id: :uuid を書くのが煩雑なら、ジェネレータのデフォルトを変更して全テーブルの主キーをUUIDにする設定が可能:

# config/initializers/generators.rb
Rails.application.config.generators do |g|
  g.orm :active_record, primary_key_type: :uuid
end

この設定を入れるとrails g model Post などで自動的に id: :uuid になる。

実装時のチェックリスト

  • 参照される側(親)のテーブルが id: :uuid で作成されているか。
  • 参照する側の t.referencesbelongs_totype: :uuid を指定しているか。
  • foreign_key: true をつけて外部キー制約を貼っているか(型が一致していれば安全)。
  • モデルの関連は belongs_to :user / belongs_to :chatroom のようにオブジェクトで渡す(user_id を直にいじらず)。
  • マイグレーションを逆に戻したいときは rails db:rollbackrails db:migrate:down VERSION=... で該当バージョンを指定する。

よくある誤解・落とし穴

  • t.references :user だけ書いておけば良いと考えてしまい、type: :uuid を忘れてしまうと、Railsは外部キーを整数(bigint)として扱おうとし、UUID主キーとの関連で失敗する。
  • current_user.id を渡していても、実際のSQLでは user_id = NULL0 になってしまうケースがある(内部で型変換の齟齬が原因)。
  • find_or_initialize_by(user_id: current_user.id, chatroom_id: chatroom.id) が期待どおりに動かず、find_or_initialize_by(user: current_user, chatroom: chatroom) を使うことで関連解決が安定する場面もある。

参考リンク

  • Rails公式ガイド: Active Record Migrations(UUID外部キーの型指定について).
  • Stack Overflow: UUID主キーの関連に対して type: :uuid を付ける必要性の議論.
  • Hashrocket TIL: RailsはデフォルトでBigIntを想定しているので、UUID外部キーには明示的に型指定が必要.
  • Reddit /r/rails: UUIDを使う場合、関連側もUUID型を指定する必要があるというアドバイス.
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?