やりたいこと
モデルのPKをULIDに変更し、モデル生成時に自動でULIDがPKとして設定されるようにしたい。
なぜULIDをPKに利用しようと思ったか
Railsは初期値としてPKがbigint auto_increment
で設定されています。
そのため、edit
やshow
などのPKを含むルーティングを行う際、PKがURIに表示されており、rails generate model xxx
で生成したものをそのまま利用すると、別レコードのIDが推測できる状態になってしまいます。
http://localhost:3000/user/1/edit
-> もしかしたら、user/2/editというルートもあるかも。。。?
-> もし適切なアクセス制限ができていなければ、ユーザが直接URIを変更すると、ユーザに関係のないデータまで閲覧・編集ができてしまう恐れがあります。
開発環境
- WSL2
- Ubuntu 20.04 LTS
- Rails 6.1.3.1
- ruby 2.7.3p183 (2021-04-05 revision 6847ee089d) [x86_64-linux]
- DB
- SQLite3
ULIDとは
Universally Unique Lexicographically Sortable Identifier(原文抜粋)
Universally Unique Lexicographically Sortable Identifier
- 128-bit compatibility with UUID
- 1.21e+24 unique ULIDs per millisecond
- Lexicographically sortable!
- Canonically encoded as a 26 character string, as opposed to the 36 character UUID
- Uses Crockford's base32 for better efficiency and readability (5 bits per character)
- Case insensitive
- No special characters (URL safe)
- Monotonic sort order (correctly detects and handles the same millisecond)
↓
DeepLで日本語翻訳
↓
Universally Unique Lexicographically Sortable Identifierの略。
- UUIDとの128ビット互換
- ミリ秒あたり1.21e+24個のユニークなULID
- Lexicographically Sortable!
- 36文字のUUIDに対し、26文字の文字列として正規にエンコードされています。
- 効率と読みやすさを高めるため、Crockfordのbase32を使用(1文字あたり5ビット
- 大文字小文字を区別しない
- 特殊文字を使用しない(URLセーフ)
- 単調なソート順(同じミリ秒を正しく検出して処理する
最初はUUIDにしようとも思いましたが、生成時間を保持する必要のないモデルであってもレコード生成順にソートをする場合もあると考えたため、時間情報IDに組み込まれているULIDを利用することとしました。
基本的に利用する際は文字列として利用すると思うので、文字列としてどのように定義されているのかも抜粋します↓
01AN4Z07BY 79KA1307SR9X4MV3
|----------| |----------------|
時刻情報 ランダムID
10文字 16文字
実装方法
ulid gemを利用します。
ULIDの生成だけをgemに依存するため、RailsのモデルのPKをULIDにするロジックは自身で実装する必要があります。
1. Gemfileにgemを追加
RailsでULIDを生成するため、ulid gemを追加します。
# Gemfile
gem 'ulid'
bundle install
でモジュールを取得します。
$ bundle install
このgemを入れることによって、ULID.generate
という関数を利用して、ULIDが生成できるようになります。
2. ULID生成ロジックを作成する
ULIDを生成するロジックは全モデルで共通の処理になるためconcern
として定義します。
今回はulid_pk.rb
ファイルを作成し、その中で定義を行いました。
# models/concerns/ulid_pk.rb
# ulid gem を読み込む
require 'ulid'
module UlidPk
extend ActiveSupport::Concern
# include時に実施した対応
included do
# モデル生成前(insert時)にidにulidを設定する
before_create :set_ulid
end
# ULIDをPKに設定する
def set_ulid
# 自身のインスタンスで保持しているidへULIDを代入する
self.id = ULID.generate
end
end
3. ULIDモジュールを読み込み
全モデルで共通して生成するため、ApplicationRecord
クラスでモジュールの読み込みを行います。
# app/models/application_record.rb
class ApplicationRecord < ActiveRecord::Base
# ULIDを適用する処理を反映
include UlidPk
self.abstract_class = true
end
4. migrationファイルの変更
migration
ファイルで設定されている定義を更新します。
更新箇所のポイントは以下です。
-
id: false
を設定すること- これによりRailsのid自動採番機能を無効化できる
-
id
の型をstring
に変更し、プライマリーキーとして明示的に定義すること
# 20211218xxxxxx_create_tests.rb
create_table :tests, id: false do |t|
# ULIDは26文字で生成されるため、limit:26で文字列長を26文字に設定します
t.string :id, limit: 26, primary_key: true
# ~~~ 各種モデルのカラム定義 ~~~
t.string :message
end
5. DB定義を反映させる
以下のコマンドでmigration
ファイルの内容をDBに反映させれば設定完了です。
$ rails db:migrate
== 20211218xxxxxx Tests: migrating ================================
-- create_table(:tests, {:id=>false})
-> 0.0021s
== 20211218xxxxxx Tests: migrated (0.0022s) =======================
6. うまく生成できるか確認
Rails Consoleでロジックが正しく動作しているか確認します。
$ rails c -s
Any modifications you make will be rolled back on exit
irb(main):001:0> Test.create(message: "test")
(0.3ms) SELECT sqlite_version(*)
TRANSACTION (0.1ms) begin transaction
TRANSACTION (0.1ms) SAVEPOINT active_record_1
Unit Create (0.3ms) INSERT INTO "tests" ("id", "message") VALUES (?, ?) [["id", "01FQ6QJA3T00CAFGY67AVJ4KQR"], ["message", "test"]]
TRANSACTION (0.1ms) RELEASE SAVEPOINT active_record_1
=> #<Test id: "01FQ6QJA3T00CAFGY67AVJ4KQR", name: "message">
id
にULIDが生成されて格納されていることが確認できました!
+α. FKの設定方法
PKを文字列に変更したため、FKの設定も変更が必要です。
# 20211218xxxxxx_create_fk_tests.rb
class CreateFkTests < ActiveRecord::Migration[6.1]
def change
create_table :fk_tests, id: false do |t|
# ULIDは26文字で生成されるため、limit:26で文字列長を26文字に設定します
t.string :id, limit: 26, primary_key: true
# ~~~ 各種モデルのカラム定義 ~~~
t.string :note
# FKとして利用するカラムの型を明示的にstring, limit: 26として指定します
t.references :test, type: :string, limit: 26, foreign_key: true
end
end
end
自分は参照先の型を変更することを忘れていて、結構ハマりました。。。
学んだこと
以下の学びがありました!
- Railsのコールバック定義
-
concern
の使い方 -
migration
ファイルの設定