現在オンラインスクールにてプログラムの勉強をしているとぴ(@topi_log)と申します。
個人開発をしていく中で、idカラムをuuidカラムに変更したくなったので、その備忘録として残します。
初学者ゆえ、間違いなどありましたらそっと教えていただけますと幸いです。
開発環境
- WSL2(Ubuntu22.4)
- Docker
- PostgreSQL ver16
- Ruby on Rails ver7.1.3 APIモード
- Next.js ver14
※フロントとしてNext.jsを使っていますが今回はRails側の実装のみ扱います。
対象者
Ruby on Railsで開発途中にidをuuidに変更したい方
ここでの変更は、idをuuid型にしつつuuidというカラム名でプライマリーキーに設定することです。
変更理由
現在投稿型のアプリを作成していますが、公開範囲の中に「URLを知っている人」があります。
単純なidだとURLを知らなくても推測できてしまい秘匿性が薄れてしまうため、URLとしてidではなくuuidを使用することにしました。
ER図
記事用に簡略化したERがこちらです。
今回は「postのidをuuidに変換」する内容です。
流れ
- uuidカラム追加
- 外部キー設定されているところにpost_uuidカラムを追加
- プライマリーキーをuuidにして外部キーを変更
- indexを変更
- idカラムを削除
- idでやっている実装をuuidに変更
今回はマイグレーションを5つに分け最後にモデル等の実装を修正しました。
実装
1. uuidカラムを追加
uuidカラムをpostに追加します。また今回はPostgreSQLを使っているのでそれを前提に実装します。
class AddUuid < ActiveRecord::Migration[7.1]
def change
# uuidカラムの追加
add_column :posts, :uuid, :uuid, default: 'gen_random_uuid()', null: false
# uuidの一意制約
add_index :posts, :uuid, unique: true
# 既存データのuuidカラムにデータ追加
Post.reset_column_information # モデルの操作を行うのでキャッシュを削除
Post.find_each { |post| post.update_column(:uuid, SecureRandom.uuid) }
end
end
PostgreSQLのバージョンによってはuuidを使うために拡張機能の有効化が必要になります。
その際はenable_extension 'pgcrypto'
をchange
の最初に追記します。
2. 外部キー設定されているところにpost_uuidカラムを追加
class AddUuidForeignKey < ActiveRecord::Migration[7.1]
def change
# いいねテーブルにpost_uuidを追加
add_column :favorites, :post_uuid, :uuid
# タグの中間テーブルにpost_uuidを追加
add_column :post_tags, :post_uuid, :uuid
# 既存のデータにpost_uuidを設定
# いいねテーブル
Favorite.reset_column_information
Favorite.find_each do |fav|
fav.update_column(:post_uuid, Post.find(fav.post_id).uuid)
end
# タグの中間テーブル
PostTag.reset_column_information
PostTag.find_each { |pt| pt.update_column(:post_uuid, Post.find(pt.post_id).uuid) }
# null制約を追加
change_column_null :favorites, :post_uuid, false
change_column_null :post_tags, :post_uuid, false
end
end
post_uuid
カラムを追加したとしても、既存のデータのpost_uuidは当然null値になってしまうので、null false
の設定はあとから追加するようにしました。
3. プライマリーキーをuuidにして外部キーを変更
class ChangePrimaryKey < ActiveRecord::Migration[7.1]
def change
# 外部キー制約を一旦解除
remove_foreign_key :favorites, :posts
remove_foreign_key :post_tags, :posts
# プライマリキーをuuidに変更
# 投稿テーブルのuuidをプライマリーキーに設定
execute 'ALTER TABLE posts DROP CONSTRAINT posts_pkey;'
execute 'ALTER TABLE posts ADD PRIMARY KEY (uuid);'
# 外部キー制約を再設定
add_foreign_key :favorites, :posts, column: :post_uuid, primary_key: :uuid
add_foreign_key :post_tags, :posts, column: :post_uuid, primary_key: :uuid
end
end
PostgreSQLにはテーブル定義を変更するSQLがあるので、それを利用してプライマリーキーをidからuuidに変更します。
ALTER TABLE distributors DROP CONSTRAINT distributors_pkey,
ADD CONSTRAINT distributors_pkey PRIMARY KEY USING INDEX dist_id_temp_idx;
このようなサンプルがあったので分解して使いました。
①post_idの外部キー制約を外す
②Postテーブルのプライマリーキーをuuidに変更
③アソシエーションを結んでいるテーブルに先ほど追加したpost_uuidを外部キーとして設定する
この流れで実装しています。
4. indexを変更
class ChangeIndex < ActiveRecord::Migration[7.1]
def change
# 既存のindexを削除
# いいね
remove_index :favorites, name: 'index_favorites_on_post_id'
# タグの中間テーブル
remove_index :post_tags, name: 'index_post_tags_on_post_id'
# uuidをindexとして追加
add_index :favorites, :post_uuid
add_index :post_tags, :post_uuid
end
end
post_id
でindex
を設定していますが、プライマリーキーに合わせてpost_uuid
をindex
にします。
5. idカラムを削除
class DeleteUserIdAndPostId < ActiveRecord::Migration[7.1]
def change
# post_idを削除
# いいね
remove_column :favorites, :post_id, :bigint
# タグの中間テーブル
remove_column :post_tags, :post_id, :bigint
# postのidを削除
remove_column :posts, :id, :bigint
end
end
外部キー制約は外してあるのでどちらを先に消してもいい気がします。
6. idでやっている実装をuuidに変更
モデルファイルの修正
外部キーがidではなくuuidになったので明示的に指定します。
class Post < ApplicationRecord
belongs_to :user
has_many :post_tags, primary_key: :uuid, foreign_key: :post_uuid, dependent: :destroy
has_many :tags, through: :post_tags, source: :tag
has_many :favorites, primary_key: :uuid, foreign_key: :post_uuid, dependent: :destroy
class User < ActiveRecord::Base
has_many :posts, dependent: :destroy
has_many :favorites, dependent: :destroy
class Favorite < ApplicationRecord
belongs_to :user
belongs_to :post, primary_key: :uuid, foreign_key: :post_uuid
end
class PostTag < ApplicationRecord
belongs_to :post, foreign_key: :post_uuid
class Tag < ApplicationRecord
has_many :post_tags, dependent: :destroy
has_many :posts, primary_key: :uuid, foreign_key: :post_uuid, through: :post_tags
Postモデルをhas_many
しているモデル(User, Tag)にprimary_key: :uuid
とforeign_key: :post_uuid
を明示します。
Postモデルにbelongs_to
しているモデル(Favorite, PostTag)にはforeign_key: :post_uuid
のみ追記します。
上記の流れでidをuuidに変更することができます。
おまけ:uuidの短縮
DBにはuuidとして保存しつつ、実際のURLは短縮して表示したいです。
なぜならuuidは「8桁-4桁-4桁-4桁-12桁」の36文字からなり、URLにするととても長いです
(例:https://hogehoge.com/posts/123e4567-e89b-12d3-a456-426614174000)
なのでbase64で16進数に変換して短縮します。
# post.rb
# uuidの短縮
def short_uuid
# base64で短縮
# - を削除したあと16進数に変換、パディングの=を削除
Base64.urlsafe_encode64([uuid.delete('-')].pack("H*")).tr('=', '')
end
# 短縮uuidから検索
def self.find_by_short_uuid(short_uuid)
# base64でデコード
# uuidは「8-4-4-4-12」の形式(例:550e8400-e29b-41d4-a716-446655440000)
# なので16進数から変換して-を挿入
decode_uuid = Base64.urlsafe_decode64(short_uuid).unpack1("H*").insert(8, '-').insert(13, '-').insert(18, '-').insert(23, '-')
find_by(uuid: decode_uuid)
end
これで22文字に短縮することができます。
終わりに
今回はPostモデルのみでしたが、開発ではUserモデルもuuidに変更したかったのでかなり複雑且つ量の多い修正になりました。
予めuuidで行う場合はあとから修正するより時間がかかっても先にやっておいたほうがいいと良い教訓になりました。
ご拝読いただきありがとうございました!