🖌️

RailsのテーブルのIDにULIDを導入する

2023/06/01に公開

前提

既存のプロジェクトがあり、通常の連番を採用している。
すべてのテーブルのIDをULIDにしたいわけではなく、レコード数が増えると予想されるテーブルのみにULIDを使いたい。

参考記事

How to use ULID as primary key Rails - DEV Community
この記事の内容をほぼそのまま使う。

ULIDの導入

Gemfileにulidを追記してbundle installを実行。

Gemfile
gem 'ulid'

サンプルのER図

今回は、posts, post_photos, post_likesのIDにULIDを採用。
usersは既存のテーブルで通常の連番(BIGINT)を採用している。

マイグレーション

※PostgreSQLの使用を前提としてCHECK制約を書いている
※自分の場合、CHECK制約を書く事が多いのでup/downを使っている
changeではdb:rollback時にCHECK制約の記述でエラーが発生してしまう
CHECK制約はDROP TABLEで消えるのでこのような書き方にしている。
※t.referencesではなくadd_foreign_keyを使っているのはCASCADEを使いたいから
dependent: :destroyは使わず外部キーでレコードの整合性を保つべきなので

db/migrate/yyyymmddhhmmss_create_posts.rb
# 投稿
class CreateInterestPosts < ActiveRecord::Migration[6.1]

  def up
    create_table :posts, id: false do |t|
      t.binary    :id, limit: 16, primary_key: true # ULID PrimaryKey
      t.bigint    :user_id, null: false
      t.text      :content, null: false
      t.timestamp :posted_at, null: false
      t.timestamps
    end

    add_foreign_key :posts, :users, on_update: :cascade, on_delete: :cascade
  end

  def down
    drop_table :posts
  end

end
db/migrate/yyyymmddhhmmss_create_post_photos.rb
# 投稿写真
# アップロード先はminioなどのオブジェクトストレージを想定
# URLは自身のIDを元に決定できるのでカラムに持たずモデルのメソッドとして定義する
class CreatePostPhotos < ActiveRecord::Migration[6.1]

  def up
    create_table :post_photos, id: false do |t|
      t.binary  :id, limit: 16, primary_key: true # ULID PrimaryKey
      t.binary  :post_id, null: false, limit: 16 # 投稿ID
      t.integer :width, null: false
      t.integer :height, null: false
      t.integer :file_size, null: false
      t.string  :checksum, null: false, limit: 40 # SHA1ハッシュ値
      t.string  :content_type, null: false # image/png or image/jpeg
      t.timestamps
    end

    add_index :post_photos, :interest_post_id, unique: true

    add_foreign_key :post_photos, :posts,
      on_update: :cascade, on_delete: :cascade

    execute "ALTER TABLE post_photos ADD CHECK (width > 0)"
    execute "ALTER TABLE post_photos ADD CHECK (height > 0)"
    execute "ALTER TABLE post_photos ADD CHECK (file_size > 0)"
    execute "ALTER TABLE post_photos ADD CHECK (checksum ~ '\\A[0-9a-z]{40}\\Z')"
    execute "ALTER TABLE post_photos ADD CHECK (content_type ~ '\\Aimage\\/(png|jpeg)\\Z')"
  end

  def down
    drop_table :post_photos
  end

end
db/migrate/yyyymmddhhmmss_create_post_likes.rb
# 投稿に対するいいね
class CreatePostLikes < ActiveRecord::Migration[6.1]

  def up
    create_table :post_likes, id: false do |t|
      t.binary  :id, limit: 16, primary_key: true # ULID PrimaryKey
      t.bigint :user_id, null: false # いいねをしたユーザーID
      t.binary :post_id, limit: 16, null: false # 投稿ID
      t.timestamps
    end

    add_index :post_likes, [:user_id, :interest_post_id], unique: true

    add_foreign_key :post_likes, :users,
      on_update: :cascade, on_delete: :cascade
    add_foreign_key :post_likes, :posts,
      on_update: :cascade, on_delete: :cascade
  end

  def down
    drop_table :interest_post_likes
  end

end

IDにULIDを使うための準備

通常のモデルはApplicationRecordを継承しているように、ULIDを使うモデルも似たような感じにしたい。
ここではNlidApplicationRecordを継承するようにしたい。

app/models/concerns/ulid_primary_key.rb
# ULIDをPrimaryKeyとして使用するモデルの共通処理
module UlidPrimaryKey
  extend ActiveSupport::Concern

  included do
    before_create :set_ulid
  end

  def set_ulid
    # 呼び出し側で明示的にULIDを生成してidを設定したい場合があるので
    # idが未設定の場合のみ設定する
    unless self.id
      self.id = ULID.generate
    end
  end

end
app/models/ulid_application_record.rb
# idをULIDで保持するモデルの基底クラス
class UlidApplicationRecord < ActiveRecord::Base
  include UlidPrimaryKey
  self.abstract_class = true
end

モデルの定義

ApplicationRecordの部分をUlidApplicationRecordに修正してアソシエーションをいつも通りに設定するだけ。

class Post < UlidApplicationRecord
  belongs_to :user
  has_one :photo, class_name: 'PostPhoto'
end

class PostPhoto < UlidApplicationRecord
  belongs_to :user
  belongs_to: post
end

class PostLike < UlidApplicationRecord
  belongs_to :user
  belongs_to: post
end

Discussion