🖌️
RailsのテーブルのIDにULIDを導入する
前提
既存のプロジェクトがあり、通常の連番を採用している。
すべてのテーブルの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