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

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

### なぜULIDをPKに利用しようと思ったか
Railsは初期値としてPKが`bigint auto_increment`で設定されています。
そのため、`edit`や`show`などのPKを含むルーティングを行う際、PKがURIに表示されており、`rails generate model xxx`で生成したものをそのまま利用すると、別レコードのIDが推測できる状態になってしまいます。

```url
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](https://github.com/rafaelsales/ulid)（原文抜粋）
> 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](https://github.com/rafaelsales/ulid) gemを利用します。
ULIDの生成だけをgemに依存するため、RailsのモデルのPKをULIDにするロジックは自身で実装する必要があります。

### 1. Gemfileにgemを追加
RailsでULIDを生成するため、[ulid](https://github.com/rafaelsales/ulid) gemを追加します。

```ruby
# Gemfile
gem 'ulid'
```
`bundle install`でモジュールを取得します。

```bash
$ bundle install
```

このgemを入れることによって、`ULID.generate`という関数を利用して、ULIDが生成できるようになります。

#### 2. ULID生成ロジックを作成する
ULIDを生成するロジックは全モデルで共通の処理になるため`concern`として定義します。
今回は`ulid_pk.rb`ファイルを作成し、その中で定義を行いました。

```ruby
# 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`クラスでモジュールの読み込みを行います。

```ruby
# 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`に変更し、プライマリーキーとして明示的に定義すること

```ruby
# 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に反映させれば設定完了です。

```bash
$ rails db:migrate
== 20211218xxxxxx Tests: migrating ================================
-- create_table(:tests, {:id=>false})
   -> 0.0021s
== 20211218xxxxxx Tests: migrated (0.0022s) =======================
```

### 6. うまく生成できるか確認
Rails Consoleでロジックが正しく動作しているか確認します。

```irb
$ 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の設定も変更が必要です。

```ruby
# 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`ファイルの設定

## 参考
* [【Rails】ModelのPrimary keyをULIDにする【ActiveRecord】](https://qiita.com/shin1rok/items/93e159718b8174241956)
* [How to use ULID as primary key Rails](https://dev.to/renzodiaz/how-to-use-ulid-as-primary-key-rails-5hf6)
* [Active Record コールバック](https://railsguides.jp/active_record_callbacks.html)
* [【Rails・MySQL】MySQLのデータ型とRailsのマイグレーションファイルのデータ定義の対応まとめ](https://qiita.com/vermilionfog/items/816fa7de1d0213979929)