Ⅰ. はじめに(この記事で解決できること)
Railsでアプリを作っていると、
次のように思ったことはありませんか?
- URLに
/products/1のような 連番IDをそのまま出したくない - 他人にURLを渡したときに 推測されそうで不安
- UUIDを使いたいが、既存テーブルの主キーを変えるのは怖い
私自身も、開発を進める中で
「URLだけでもUUIDにできないだろうか?」
と感じたのがこの記事を書いたきっかけです。
この記事で紹介する方針
この記事では、次の方針でUUIDを導入します。
- 既存の integer id はそのまま残す
- URLに使うIDだけをUUIDにする
- 既存URLも壊さず、段階的に移行できる
つまり、
データ構造は変えず、見せ方(URL)だけを安全に変更する
というアプローチです。
この記事を読むとできるようになること
この記事を最後まで読むと、次のことができるようになります。
- PostgreSQLでUUIDを安全に使う方法がわかる
- 既存テーブルにUUIDカラム(public_id)を追加できる
- RailsのURLをUUID形式に切り替えられる
- 旧URL(integer id)を壊さずに移行できる
※不備があれば優しく教えてください笑
Ⅱ. UUID(public_id)を導入する手順
— integer id のまま、安全に URL を UUID 化する方法 —
この手順では、既存のテーブル構造は変更せず、
URL に使うIDだけを UUID に切り替える
という、安全で現実的な方式を採用します。
主キー(integer id)はそのまま残すため、
既存データや関連への影響を最小限に抑えられます。
1. PostgreSQLでUUIDを生成できるようにする
Railsでは UUID を生成するために
gen_random_uuid() を使用します。
この関数を使うには、PostgreSQLの pgcrypto 拡張を有効にしておく必要があります。
マイグレーションを作成
$ docker compose exec web bin/rails g migration EnablePgcryptoExtension
生成されたマイグレーションを編集します。
class EnablePgcryptoExtension < ActiveRecord::Migration[7.1]
def change
enable_extension 'pgcrypto' unless extension_enabled?('pgcrypto')
end
end
編集後、実行します。
docker compose exec web bin/rails db:migrate
1. 各テーブルにUUID用カラム(public_id)を追加する
ここでは例として "products" テーブルにUUID用のカラムを追加します。
(categories や他のモデルにも、同じ手順で追加できます)
マイグレーションを作成
docker compose exec web bin/rails g migration AddPublicIdToProducts
作成したマイグレーションを編集
class AddPublicIdToProducts < ActiveRecord::Migration[7.1]
def change
add_column :products, :public_id, :uuid,
default: -> { "gen_random_uuid()" },
null: false
add_index :products, :public_id, unique: true
end
end
ポイント
default: -> { "gen_random_uuid()" } により
カラム追加時に既存レコードにもUUIDが付与され、以降の新規レコードも自動生成される
PostgreSQLでは、別途 UPDATE を実行する必要はありません
unique: true によりUUIDの重複を防止 します
2. URLを public_id に切り替える(to_param を上書き)
次に、URLで使われるIDを public_id に切り替えます。
今回の場合だとモデルの、
app/models/product.rb に以下を追記します。
class Product < ApplicationRecord
def to_param
public_id.to_s
end
end
これにより、product_path(@product) は次のように変わります。
/products/1
↓
/products/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
Railsのルーティングはそのままで、
URLだけがUUID形式に変わる のがポイントです。
3. コントローラで public_id を使ってレコードを取得する
URLがUUIDになるため、
コントローラ側でも public_id を使って検索します。
ここでは ProductsController の set_product を修正します。
def set_product
# 優先:UUID(public_id)で検索
@product = current_user.products.find_by(public_id: params[:id])
# 旧URL(integer id)の救済
@product ||= current_user.products.find_by(id: params[:id])
raise ActiveRecord::RecordNotFound unless @product
end
ポイント
- find_by は 見つからない場合に nil を返す ため安全
- UUID → integer id の順で探すことで 段階的移行が可能
- 旧URL(/products/1)も引き続き動作する
※ current_user.products はログインユーザーが所有するレコードのみ取得する前提の例
※ raise ActiveRecord::RecordNotFound unless @productで id がなければ、
404を返す。
※ resource :profile など、params[:id] を使わない単数リソースの場合、
この対応は不要 です。
4. 動作確認(Rails console)
Rails consoleで、UUIDが正しく設定されているか確認します。
ここで確認できれば問題ないかと思います。
docker compose exec web bin/rails c
上から順番に入力してください。(ご自身の内容に合わせてください)
product = Product.first
product.id # 従来の integer ID
product.public_id # UUID
product.to_param # UUID が返ればOK
5. ブラウザでの最終確認
商品一覧から商品詳細を開く
URLがUUID形式になっている
そのURLを別タブで開いても正しく表示される
例:
/products/4d92cbb1-3d4b-4d4e-ae41-8a2b1b84f67c
今後の展開(他モデルにも適用可能)
他にもUUIDにしたいテーブルがある場合、
上記の「1.」から順に進めると、そのまま適用できます。
例:price_records shops categories
補足:この手法のメリット
UUIDを主キーに置き換える必要はないため、
Railsの内部処理も軽く、扱いやすい構成です。