uuid-primary-keys について
PostgreSQL または SQLite で UUID 主キーを実装する Rails 向けスキルです。UUIDv7、マイグレーション、ジェネレーター、フィクスチャ、よくある誤りを案内します。
主な機能
- 01PostgreSQL のネイティブ UUID 設定
- 02SQLite のバイナリ UUID と Base36
- 03UUIDv7 の生成指針
- 04ジェネレーターとマイグレーション設定
- 05フィクスチャと関連付けの説明
活用シーン
- 01Rails アプリで UUID 主キーを導入
- 02UUID 外部キーを一貫して追加
- 03PostgreSQL または SQLite の UUID 保存方式を選択
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ thinkoodle/ rails-skills --skill uuid-primary-keysインストール後、Claude Code で/uuid-primary-keysを入力して読み込まれたか確認します。
同梱ファイルreference.md
詳細
Rails の UUID 主キー
Rails で UUID 主キーを正しく実装する方法です。PostgreSQL のネイティブ型、または SQLite のバイナリストレージを使用し、UUIDv7 とオプションの base36 エンコードに対応します。
方針:UUIDv4 より UUIDv7
常に UUIDv7 を推奨します。 時系列順に並び、ソート可能で、UUIDv4 よりインデックスのパフォーマンスに優れています。Ruby 3.3 以降には SecureRandom.uuid_v7 が標準搭載されているため、gem は不要です。
| 機能 | オートインクリメント | UUIDv4 | UUIDv7 |
|---|---|---|---|
| 時刻順にソート可能 | はい | いいえ | はい |
| インデックスのパフォーマンス | 最良 | 低い(ランダム) | 良好(連続的) |
| 衝突のリスク | なし | 非常に低い | 非常に低い |
| 外部から推測可能 | はい | いいえ | いいえ |
| マージに安全 | いいえ | はい | はい |
ID が URL、API、または複数データベース間の同期に現れる場合は UUID を使用してください。外部に公開しない内部専用テーブルには整数を使用してください。
2 つの方法
PostgreSQL:ネイティブ UUID カラム型
- 128 ビットのネイティブ型(16 バイト)として保存される
uuidカラム型 - PostgreSQL 13 以降では、拡張機能なしで
gen_random_uuid()を利用可能 - 標準のハイフン付き 36 文字形式:
019533c0-cf4c-7c67-bfad-b7bc2832e3c5 - 基本的なセットアップにカスタムコードは不要
SQLite:バイナリストレージ+Base36 エンコード
- 生のバイナリを保存する
blob(16)カラム(16 バイトで効率的) - URL に使いやすい 25 文字の文字列にする Base36 エンコード:
0k5p9qi1v8j3h6m2n4b7 - カスタム型の登録とアダプター拡張が必要
- Basecamp の Fizzy 向けに当初開発されました
PostgreSQL のセットアップ
手順 1:ジェネレーターのデフォルトを設定する
これはエージェントが最もよく間違える点です。設定を忘れないでください。 これを設定しないと、rails generate model を実行するたびに整数 ID が作成され、すべてのマイグレーションを手動で修正することになります。
# config/initializers/generators.rb
Rails.application.config.generators do |g|
g.orm :active_record, primary_key_type: :uuid
end
これにより、生成されるすべてのマイグレーションで id: :uuid が使用され、生成されるすべての外部キーのカラムで type: :uuid が使用されます。
手順 2:pgcrypto を有効にする(PostgreSQL 13 未満の場合のみ)
PostgreSQL 13 以降では gen_random_uuid() が組み込まれています。それ以前のバージョンでは、次のようにします。
class EnablePgcrypto < ActiveRecord::Migration[8.0]
def change
enable_extension "pgcrypto"
end
end
手順 3:UUID のデフォルト生成を設定する
UUIDv7(推奨)の場合は、ApplicationRecord で設定します。
# app/models/application_record.rb
class ApplicationRecord < ActiveRecord::Base
primary_abstract_class
# Generate UUIDv7 for all UUID primary keys
before_create :set_uuid_v7_primary_key
private
def set_uuid_v7_primary_key
pk = self.class.primary_key
return unless pk
column = self.class.columns_hash[pk]
return unless column&.type == :uuid
self[pk] ||= SecureRandom.uuid_v7
end
end
default: "gen_random_uuid()" を使わない理由は? これはデータベース側で UUIDv4 を生成します。UUIDv7 は Ruby で生成する必要があります(PostgreSQL にネイティブの v7 サポートが追加されるまでは)。ApplicationRecord でデフォルトを設定すると、あらゆる場所で UUIDv7 が使われます。
UUIDv4 で問題なければ、ApplicationRecord のコールバックを省略して、データベースのデフォルトを設定できます。
create_table :posts, id: :uuid, default: "gen_random_uuid()" do |t|
# ...
end
手順 4:マイグレーションを作成する
class CreatePosts < ActiveRecord::Migration[8.0]
def change
create_table :posts, id: :uuid do |t|
t.string :title, null: false
t.text :body
t.timestamps
end
end
end
外部キーは UUID を参照します。
class CreateComments < ActiveRecord::Migration[8.0]
def change
create_table :comments, id: :uuid do |t|
t.references :post, type: :uuid, null: false, foreign_key: true, index: true
t.text :body, null: false
t.timestamps
end
end
end
重要: t.references には必ず type: :uuid を指定してください。手順 1 を実施していればジェネレーターのデフォルトで設定されますが、必ず確認してください。
SQLite のセットアップ(バイナリストレージ+Base36)
この方法では UUID を 16 バイトのバイナリブロブとして保存し、Ruby では 25 文字の base36 文字列として表現します。効率的で、URL に使いやすい簡潔な ID を生成できます。
手順 1:UUID 型を作成する
# lib/rails_ext/active_record_uuid_type.rb
# frozen_string_literal: true
module ActiveRecord
module Type
class Uuid < Binary
BASE36_LENGTH = 25 # 36^25 > 2^128
class << self
def generate
uuid = SecureRandom.uuid_v7
hex = uuid.delete("-")
hex_to_base36(hex)
end
def hex_to_base36(hex)
hex.to_i(16).to_s(36).rjust(BASE36_LENGTH, "0")
end
def base36_to_hex(base36)
base36.to_s.to_i(36).to_s(16).rjust(32, "0")
end
end
def serialize(value)
return unless value
binary = Uuid.base36_to_hex(value).scan(/../).map(&:hex).pack("C*")
super(binary)
end
def deserialize(value)
return unless value
hex = value.to_s.unpack1("H*")
Uuid.hex_to_base36(hex)
end
def cast(value)
value
end
end
end
end
ActiveRecord::Type.register(:uuid, ActiveRecord::Type::Uuid, adapter: :sqlite3)
手順 2:イニシャライザーを作成する
# config/initializers/uuid_primary_keys.rb
# frozen_string_literal: true
require_relative "../../lib/rails_ext/active_record_uuid_type"
# Auto-generate UUID for models with UUID primary keys
module UuidPrimaryKeyDefault
def load_schema!
define_uuid_primary_key_pending_default
super
end
private
def define_uuid_primary_key_pending_default
if uuid_primary_key?
pending_attribute_modifications << PendingUuidDefault.new(primary_key)
end
rescue ActiveRecord::StatementInvalid
# Table doesn't exist yet
end
def uuid_primary_key?
table_name && primary_key &&
schema_cache.columns_hash(table_name)[primary_key]&.type == :uuid
end
PendingUuidDefault = Struct.new(:name) do
def apply_to(attribute_set)
attribute_set[name] = attribute_set[name].with_user_default(-> { ActiveRecord::Type::Uuid.generate })
end
end
end
# SQLite adapter extensions for UUID support
module SqliteUuidAdapter
extend ActiveSupport::Concern
def lookup_cast_type(sql_type)
if sql_type == "blob(16)"
ActiveRecord::Type.lookup(:uuid, adapter: :sqlite3)
else
super
end
end
def fetch_type_metadata(sql_type)
if sql_type == "blob(16)"
ActiveRecord::ConnectionAdapters::SqlTypeMetadata.new(
sql_type: sql_type, type: :uuid, limit: 16
)
else
super
end
end
class_methods do
def native_database_types
@native_database_types_with_uuid ||= super.merge(uuid: { name: "blob", limit: 16 })
end
end
end
# Schema dumper: output :uuid instead of blob(16)
module SchemaDumperUuidType
def schema_type(column)
column.sql_type == "blob(16)" ? :uuid : super
end
end
# Table definition: support t.uuid columns
module TableDefinitionUuidSupport
def uuid(name, **options)
column(name, :uuid, **options)
end
end
# Apply extensions
ActiveSupport.on_load(:active_record) do
ActiveRecord::Base.singleton_class.prepend(UuidPrimaryKeyDefault)
ActiveRecord::ConnectionAdapters::TableDefinition.prepend(TableDefinitionUuidSupport)
end
ActiveSupport.on_load(:active_record_sqlite3adapter) do
ActiveRecord::ConnectionAdapters::SQLite3Adapter.prepend(SqliteUuidAdapter)
ActiveRecord::ConnectionAdapters::SQLite3::SchemaDumper.prepend(SchemaDumperUuidType)
end
# Apply immediately if already loaded
if defined?(ActiveRecord::ConnectionAdapters::SQLite3Adapter)
unless ActiveRecord::ConnectionAdapters::SQLite3Adapter.ancestors.include?(SqliteUuidAdapter)
ActiveRecord::ConnectionAdapters::SQLite3Adapter.prepend(SqliteUuidAdapter)
end
end
if defined?(ActiveRecord::ConnectionAdapters::SQLite3::SchemaDumper)
unless ActiveRecord::ConnectionAdapters::SQLite3::SchemaDumper.ancestors.include?(SchemaDumperUuidType)
ActiveRecord::ConnectionAdapters::SQLite3::SchemaDumper.prepend(SchemaDumperUuidType)
end
end
手順 3:ApplicationRecord にフォールバックを追加する
# app/models/application_record.rb
class ApplicationRecord < ActiveRecord::Base
primary_abstract_class
before_create :ensure_uuid_primary_key
private
def ensure_uuid_primary_key
pk = self.class.primary_key
return unless pk
column = self.class.columns_hash[pk]
return unless column&.type == :uuid
self[pk] ||= ActiveRecord::Type::Uuid.generate
end
end
手順 4:ジェネレーターを設定する
# config/initializers/generators.rb
Rails.application.config.generators do |g|
g.orm :active_record, primary_key_type: :uuid
end
手順 5:マイグレーション(PostgreSQL と同じ)
class CreatePosts < ActiveRecord::Migration[8.0]
def change
create_table :posts, id: :uuid do |t|
t.string :title, null: false
t.text :body
t.timestamps
end
end
end
外部キーには t.uuid を直接使用します。
class CreateComments < ActiveRecord::Migration[8.0]
def change
create_table :comments, id: :uuid do |t|
t.uuid :post_id, null: false, index: true
t.text :body, null: false
t.timestamps
end
end
end
UUID を使ったフィクスチャ
PostgreSQL(標準 UUID 形式)
# test/fixtures/posts.yml
published_post:
id: <%= SecureRandom.uuid_v7 %>
title: "Published Post"
created_at: <%= 1.week.ago %>
draft_post:
id: <%= SecureRandom.uuid_v7 %>
title: "Draft Post"
フィクスチャ ID を決定的にする場合(実行をまたいで安定させる場合):
published_post:
id: <%= Digest::UUID.uuid_v5(Digest::UUID::OID_NAMESPACE, "published_post") %>
title: "Published Post"
SQLite Base36
# test/fixtures/posts.yml
published_post:
id: <%= ActiveRecord::Type::Uuid.generate %>
title: "Published Post"
フィクスチャ内の外部キー参照
必ず生の ID ではなく、フィクスチャ名で参照してください。
# test/fixtures/comments.yml
first_comment:
post: published_post # ← Rails resolves this to the UUID automatically
body: "Great post!"
アソシエーション
アソシエーションは整数の主キーと同じように機能します。特別な設定は不要です。
class Post < ApplicationRecord
has_many :comments, dependent: :destroy
end
class Comment < ApplicationRecord
belongs_to :post
end
変更されるのはマイグレーションだけです。 モデル、アソシエーション、クエリはすべて同じです。
よくある間違い
1. ジェネレーターのデフォルト設定を忘れる
ジェネレーターに primary_key_type: :uuid を設定しないと、scaffold のたびに整数 ID が作成されます。すべてのマイグレーションを手動で修正することになります。
2. UUID 型ではなく文字列カラムを使う
# WRONG — stores UUID as variable-length string (36 bytes + overhead)
t.string :post_id
# RIGHT — PostgreSQL native uuid (16 bytes)
t.references :post, type: :uuid
# RIGHT — SQLite binary (16 bytes)
t.uuid :post_id
3. references に type: :uuid を指定し忘れる
# WRONG — creates integer foreign key column
t.references :post, foreign_key: true
# RIGHT — creates UUID foreign key column
t.references :post, type: :uuid, foreign_key: true
4. v7 が利用可能な場合に UUIDv4 を使う
UUIDv4 は完全にランダムなため、インデックスの局所性が低く、大規模になると挿入が遅くなります。UUIDv7 は時系列順のため、順次挿入となり、パフォーマンスが向上します。Ruby 3.3 以降が必要です。
5. 整数と UUID の主キーを混在させる
UUID から始める場合は、すべての箇所で UUID を使用してください。ID の型が混在すると、結合時の混乱や移行の煩雑さにつながります。例外は、独自の ID が不要な結合テーブル(id: false)です。
6. 既存のテーブルへの対応を忘れる
既存のアプリを整数の主キーから UUID の主キーに変換する場合は、データ移行の戦略が必要です。既存の主キーをその場で変更しようとしないでください。手間がかかります。代わりに、新しい UUID カラムを作成してデータを移行し、その後に切り替えるのがよい方法です。
URL に適した ID
Base36(SQLite の方法)
上記の SQLite セットアップにすでに組み込まれています。ID は次のようになります:0k5p9qi1v8j3h6m2n4b7
PostgreSQL:標準の UUID を使用する
最新のフレームワークや API では、標準の UUID を問題なく扱えます。URL を短くしたい場合は、主キーをエンコードするのではなく、別の slug または short_id カラムを追加してください。
ルートパラメーター
UUID は変更せずにルートで使用できます:
# config/routes.rb
resources :posts # Works identically
# Controller
Post.find(params[:id]) # Finds by UUID string
現在のセットアップを確認する
実装する前に、必ず既存のプロジェクトを確認してください:
# Check if generator defaults are set
grep -r "primary_key_type" config/
# Check existing migrations for UUID usage
grep -r "id: :uuid" db/migrate/
# Check for pgcrypto extension
grep -r "pgcrypto" db/migrate/ db/schema.rb
# Check database adapter
grep "adapter:" config/database.yml
# Check Ruby version (need 3.3+ for uuid_v7)
ruby -v
参考資料
このスキルディレクトリにある reference.md を参照してください。内容は次のとおりです:
- SQLite イニシャライザーの完全なコード
- Base36 のエンコード/デコードの詳細
- 移行時の変換戦略
- テストのパターンとエッジケース
- パフォーマンスのベンチマーク
- マルチデータベースに関する考慮事項