10
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【Rails】ModelのPKをULIDに変更する

Last updated at Posted at 2021-12-18

やりたいこと

モデルのPKをULIDに変更し、モデル生成時に自動でULIDがPKとして設定されるようにしたい。

なぜULIDをPKに利用しようと思ったか

Railsは初期値としてPKがbigint auto_incrementで設定されています。
そのため、editshowなどの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ファイルの設定

参考

10
7
3

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?