Railsでプライマリーキーを文字列で扱う

# この記事の目的
Railsのマイグレーションでは各テーブルのプライマリーキーにデフォルトで`id`が追加される仕様になってる。

何も指定しなくても`id`が`BIGINT型`で付与されて自動でインクリメントされるのは非常に便利だけど、アプリケーションの構成によっては文字列で登録したい時がある。

例えばQiitaでも各記事は`8f8f1d1e5333da561fd8`みたいな文字の羅列がIDとして付与されてる(たぶん)。こんな感じで`id`を文字列(`VARCHAR型`)として扱う場合の手順を説明します。

尚、今回使用してるDBは**PostgreSQL**、Railsのバージョンは**Rails5**です。

# 結論
下記の手順で実現できます。modelで付与するidのロジックはお好みで。

- `create_table`メソッドのオプションに`id: :string`を指定する。
- 子テーブルの外部キーは`add_foreign_key`と`add_index`で指定する。
-`before_create`とかでModelでidを初期化する処理をフックする。

# それじゃやってみよう
`Hoge`テーブルと`Foo`テーブルが親子関係にある場合、`Hoge`テーブルのIDを文字列にしたいぜ！って内容で説明します。

## `Hoge`テーブルのマイグレーション
`id`の型ってオプションで変えられないかなーと思い[APIドキュメント]()を参照してみました。

> A Symbol can be used to specify the type of the generated primary key column.

あ、もうこれで解決ですやん。。。。

```ruby
class CreateHoges < ActiveRecord::Migration[5.1]
  def change
    create_table :hoges, id: :string do |t|
      t.string :name, null: false

      t.timestamps
    end
  end
end
```

## `Foo`テーブルのマイグレーション
親テーブルの定義があまりに簡単だったんで、子テーブルも`t.belongs_to :hoge, null: false, foreign_key: true`で普通に書いてマイグレーションしてみる。

```
rails aborted!
StandardError: An error has occurred, this and all later migrations canceled:
PG::DatatypeMismatch: ERROR:  foreign key constraint "fk_rails_484f6f4b4a" cannot be implemented
DETAIL:  Key columns "hoge_id" and "id" are of incompatible types: bigint and character varying.
: CREATE TABLE "foos" ("id" bigserial primary key, "hoge_id" bigint NOT NULL, "name" character varying NOT NULL, "created_at" timestamp NOT NULL, "updated_at" timestamp NOT NULL, CONSTRAINT "fk_rails_484f6f4b4a"
FOREIGN KEY ("hoge_id")
  REFERENCES "hoges" ("id")
)
```

#### なんだとこのやろう
とりあえず**穏やかな気持ちで**エラーメッセージを見てみよう。
`DETAIL:  Key columns "hoge_id" and "id" are of incompatible types: bigint and character varying.`
型が違うって言われてる。`belongs_to`は`BIGINT`型でカラムを作ろうとするんでしょうね。
めんどくさいけど、外部キーはカスタムで定義しましょう。これでいけます。

```ruby
class CreateFoos < ActiveRecord::Migration[5.1]
  def change
    create_table :foos do |t|
      t.string :hoge_id, null: false
      t.string :name, null: false

      t.timestamps
    end
    add_foreign_key :foos, :hoges
    add_index  :foos, :hoge_id
  end
end
```

## モデルの定義
普通に定義すれば大丈夫ですが、idは文字列にしてあるので、**新しくレコードを追加した時に初期化する必要があります**。

下記のように`before_create`で新規作成処理をフックして、`identify`メソッドに初期化処理を記述するのが簡単だと思います。`id`に付与する文字列を作る処理はテキトーに変えてください。一意のKeyならなんでもいいなら下記のままでいいです。

```ruby
# 親テーブル
class Hoge < ApplicationRecord
  has_many :foos, dependent: :destroy

  before_create :identify

  private

    def identify(num = 16)
      self.id ||= SecureRandom.hex(num)
    end
end

# 子テーブル
class Foo < ApplicationRecord
  belongs_to :hoge, inverse_of: :foos
end
```

## 確認
スキーマの定義とモデルの挙動の簡単な動作確認をしてみる。それぞれ下記のようになってれば大丈夫。

#### スキーマの確認

```sql
pappen_devel=> \d hoges
                           Table "public.hoges"
   Column   |            Type             | Collation | Nullable | Default 
------------+-----------------------------+-----------+----------+---------
 id         | character varying           |           | not null | 
 name       | character varying           |           | not null | 
 created_at | timestamp without time zone |           | not null | 
 updated_at | timestamp without time zone |           | not null | 
Indexes:
    "hoges_pkey" PRIMARY KEY, btree (id)

pappen_devel=> \d foos
                                        Table "public.foos"
   Column   |            Type             | Collation | Nullable |             Default              
------------+-----------------------------+-----------+----------+----------------------------------
 id         | bigint                      |           | not null | nextval('foos_id_seq'::regclass)
 hoge_id    | character varying           |           | not null | 
 name       | character varying           |           | not null | 
 created_at | timestamp without time zone |           | not null | 
 updated_at | timestamp without time zone |           | not null | 
Indexes:
    "foos_pkey" PRIMARY KEY, btree (id)
    "index_foos_on_hoge_id" btree (hoge_id)
Foreign-key constraints:
    "fk_rails_484f6f4b4a" FOREIGN KEY (hoge_id) REFERENCES hoges(id)
```

#### モデルの確認

```ruby
pry(main)> Hoge.create(name: :hoge)
   (0.3ms)  BEGIN
  SQL (1.6ms)  INSERT INTO "hoges" ("id", "name", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id"  [["id", "6bc2018b3b66025e72f1c568fc7c6618"], ["name", "hoge"], ["created_at", "2019-02-25 13:35:05.115762"], ["updated_at", "2019-02-25 13:35:05.115762"]]
   (2.3ms)  COMMIT
=> #<Hoge:0x00007ff7070891d8
 id: "6bc2018b3b66025e72f1c568fc7c6618",
 name: "hoge",
 created_at: Mon, 25 Feb 2019 13:35:05 JST +09:00,
 updated_at: Mon, 25 Feb 2019 13:35:05 JST +09:00>
pry(main)> Foo.create(hoge_id: "6bc2018b3b66025e72f1c568fc7c6618", name: :foo)
   (0.3ms)  BEGIN
  Hoge Load (0.4ms)  SELECT  "hoges".* FROM "hoges" WHERE "hoges"."id" = $1 LIMIT $2  [["id", "6bc2018b3b66025e72f1c568fc7c6618"], ["LIMIT", 1]]
  SQL (3.7ms)  INSERT INTO "foos" ("hoge_id", "name", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id"  [["hoge_id", "6bc2018b3b66025e72f1c568fc7c6618"], ["name", "foo"], ["created_at", "2019-02-25 13:36:05.747563"], ["updated_at", "2019-02-25 13:36:05.747563"]]
   (0.4ms)  COMMIT
=> #<Foo:0x00007ff705fb98d0
 id: 1,
 hoge_id: "6bc2018b3b66025e72f1c568fc7c6618",
 name: "foo",
 created_at: Mon, 25 Feb 2019 13:36:05 JST +09:00,
 updated_at: Mon, 25 Feb 2019 13:36:05 JST +09:00>
pry(main)> Hoge.first.foos
  Hoge Load (0.4ms)  SELECT  "hoges".* FROM "hoges" ORDER BY "hoges"."id" ASC LIMIT $1  [["LIMIT", 1]]
  Foo Load (0.5ms)  SELECT "foos".* FROM "foos" WHERE "foos"."hoge_id" = $1  [["hoge_id", "6bc2018b3b66025e72f1c568fc7c6618"]]
=> [#<Foo:0x00007ff70113e228
  id: 1,
  hoge_id: "6bc2018b3b66025e72f1c568fc7c6618",
  name: "foo",
  created_at: Mon, 25 Feb 2019 13:36:05 JST +09:00,
  updated_at: Mon, 25 Feb 2019 13:36:05 JST +09:00>]
```

# まとめ
意外と簡単にプライマリーキーを文字列として扱う事ができました。
`belongs_to`でできねえとか若干の罠はありましたが。
Rails便利ですね〜。