LoginSignup
11
7

【Rails】途中からidカラムをuuidに変更する

Last updated at Posted at 2024-06-04

現在オンラインスクールにてプログラムの勉強をしているとぴ(@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に変換」する内容です。

流れ

  1. uuidカラム追加
  2. 外部キー設定されているところにpost_uuidカラムを追加
  3. プライマリーキーをuuidにして外部キーを変更
  4. indexを変更
  5. idカラムを削除
  6. 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_idindexを設定していますが、プライマリーキーに合わせてpost_uuidindexにします。

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: :uuidforeign_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で行う場合はあとから修正するより時間がかかっても先にやっておいたほうがいいと良い教訓になりました。
ご拝読いただきありがとうございました!

11
7
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
11
7