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で既存テーブルを壊さずにURLだけUUID化する安全な実装方法(PostgreSQL)

Last updated at Posted at 2026-01-19

Ⅰ. はじめに(この記事で解決できること)

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

補足:この手法のメリット

  • 既存の integer ID を 壊さない

  • データ移行なしで導入できる

  • URLが推測されにくくなり安全

  • 共有・公開・招待URLと相性が良い

  • 本番環境への影響が小さい

UUIDを主キーに置き換える必要はないため、
Railsの内部処理も軽く、扱いやすい構成です。

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?