経緯
投稿型のアプリにて、投稿の詳細画面のURLをpost/1といったURLでなく、post/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxといった推測困難なURLにすべく主キーのuuid化を検討していました。
しかし、今回uuid化を行いたいpostsテーブルの主キーは、
- Bookmark機能に使われるpost_id
- ActiveStorageによる画像添付に使われるpost.id
といった依存関係が既に存在し、この状態で主キーをuuidに変更すると関連するすべての外部キーをuuidに変更する必要があります。
特にActiveStorageの場合は、例えば以下のテーブルもuuid前提で作り直す必要があります。
- active_storage_attachments.record_id
- active_storage_variant_records.blob_id
- active_storage_blobs.id
Rails標準のActiveStorageはbigint前提で設計されているため、途中からuuid化を行うと、既存の画像とレコードの紐付けが切れるなど本番データを破壊するリスクが高く、あまり現実的ではありません。
私の場合、ブックマーク機能はcurrent_userのみアクセス出来る設計ということもあり、今回はpost.idはそのまま残し、postsテーブルにuuidカラムを追加し公開URLのみuuidで生成する方式を採用しました。
実装内容
- 外部公開URL(誰でもアクセス可能なページ)はuuidを使用
- 内部処理(ActiveStorage・Bookmark等の参照)は従来通りpost.idを使用
環境
- Windows
- WSL2
- Docker
- Rails 7.2
- Ruby 3.3
- PostgreSQL
実装手順
1. uuidカラムの追加
マイグレーションファイルを生成しpostsテーブルにuuidカラムを追加します。
class AddUuidToPosts < ActiveRecord::Migration[7.2]
def change
## PostgreSQLのuuid生成機能(pgcrypto)を有効化
enable_extension 'pgcrypto' unless extension_enabled?('pgcrypto')
# postsにuuidカラムを追加(レコード作成時に自動生成、nullを許可しない)
add_column :posts, :uuid, :uuid, default: "gen_random_uuid()", null: false
# uuidに一意性を持たせる
add_index :posts, :uuid, unique: true
end
end
マイグレーションを実行すると以下の内容がpostsテーブルに反映され、postsテーブルにuuidカラムが追加されたことが確認できます。
create_table "posts", force: :cascade do |t|
# 省略
t.uuid "uuid", default: -> { "gen_random_uuid()" }, null: false
# 省略
t.index ["uuid"], name: "index_posts_on_uuid", unique: true
end
2. postsのルーティングをuuidベースに変更
現在ルーティングの生成はpost.idベースになっているのでconfig/routes.rbを編集してuuidベースでルーティングが行われるようにします。
# config/routes.rb
# param: :uuidを追加
resources :posts, param: :uuid, only: [ :new, :create, :show, :index, :edit, :update, :destroy ]
編集後ルーティングテーブルを確認してみると、postsのルーティングがuuidベースに変わっていることが確認できました。
post GET /posts/:uuid(.:format)
PATCH /posts/:uuid(.:format)
PUT /posts/:uuid(.:format)
DELETE /posts/:uuid(.:format)
3. リンクの生成がuuidで行われるように修正
ルーティングがuuidベースになっても、リンクの生成がidベースのままなのでuuidを使って生成されるようmodels/post.rbを編集します。
# models/post.rb
def to_param
uuid
end
RailsはURL生成にto_paramというメソッドを使用しています。
今回はuuidでURLを生成したいので、post.rbにてto_paramの値をオーバーライドします。
この記述により、Post/:uuidという形でURLが生成されるようになります。
4. 変数がuuidを取得出来るようにする
現在はレコードの取得をidを使って行っているので、公開URLで表示されるものはuuidでレコードを取得できるようにコントローラーを修正します
変更前
def show
@post = Post.find(params[:id])
end
変更後
def show
@post = Post.find_by(uuid: params[:uuid])
end
その他のアクションにおける@postの値も同じようにuuidで取得出来るよう編集します。
これでpostsのURLがuuidで表示され、関連するテーブルとのやりとりはpost.idで行うという実装が完了しました。
私の場合は以上の作業で目的とするuuid化を実装出来ましたが、例えばpostsに関連する機能が誰でもアクセスできる場合など、設計によっては関連テーブル側にもuuidカラムを追加し、公開用IDと内部IDを分けて管理するなどの必要があります。
また、ActiveStorage を使用していない場合や、アプリを新規に構築している段階であれば、主キーごとuuidに変更するという選択肢も取り得ると考えています。
参考記事
