Rails の UUIDプライマリキーを試す

RailsでUUIDプライマリキーを試してみた。

UUIDプライマリキーとは、オートインクリメントされる連番の整数ではなく、
`077cacaa-3913-11e9-aacc-0242ac1c0002`の様なランダムな36文字のUUIDのをIDとして、プライマリキーとして使うことである。

## 検証環境

- Ruby 2.6.1
- Rails 5.2.2
- PostgreSQL 10.7
- MySQL 5.7.25 と 8.0.15

データベースのバージョンは Amazon Auroraで現状使えるバージョンに近しいものを選択した。

## 検証モデル

次のようなプロジェクトごとにタスクを登録し、それぞれのタスクにメンバーをアサインするモデルを考える。

<img width="225" alt="model.png" src="https://qiita-image-store.s3.amazonaws.com/0/32533/ab12304e-1a62-fc38-385f-3663a97f8b05.png">

次のような関連を持っている。

- `Project` と `Task` は 一対多
- `Task` と `Member` は 多対多(タスクには複数人をアサイン可能) 

## PostgreSQLの場合

PostgreSQLはUUID型があるのでプライマリキーの型をUUIDにする。

### マイグレーションファイル

各モデルともIDはUUIDとする。
最初のテーブル`projects`を作成する前に`enable_extension 'pgcrypto'`を実行してPostgreSQLの拡張を有効化する。

```ruby:db/migrate/xxxxxxxxxxxxxx_create_projects.rb
class CreateProjects < ActiveRecord::Migration[5.2]
  def change
    enable_extension 'pgcrypto' unless extension_enabled?('pgcrypto')
    create_table :projects, id: :uuid, comment: 'プロジェクト' do |t|
      t.string :name, null: false, comment: 'プロジェクト名'
      t.text :description, comment: '概要'
      t.date :start_on, comment: '開始日'
      t.date :end_on, comment: '終了日'

      t.timestamps
    end
  end
end
```

続く`tasks`テーブルでは、`projects`テーブルに`t.references`で関連を定義する。
関連先のIDの型を反映するのかと思ったのだが、`bigint`となり、エラーとなるので `type: :uuid`でわざわざ指定している。

```ruby:db/migrate/xxxxxxxxxxxxxx_create_tasks.rb
class CreateTasks < ActiveRecord::Migration[5.2]
  def change
    create_table :tasks, id: :uuid, command: 'タスク' do |t|
      t.references :project, type: :uuid, foreign_key: true
      t.string :name, null: false, command: 'タスク名'
      t.text :description, comment: '概要'
      t.integer :status, default: 0, comment: 'ステータス'
      t.date :start_on, comment: '開始日'
      t.date :end_on, comment: '終了日'

      t.timestamps
    end
  end
end
```

最後の`members`テーブルは`tasks`と多対多の関係としたいので、関連テーブル`members_tasks`も追加する。
`has_and_belongs_to_many`でリレーションを作るので、`members_tasks`は`id: false`としている。

```ruby:db/migrate/xxxxxxxxxxxxxx_create_member.rb
class CreateMembers < ActiveRecord::Migration[5.2]
  def change
    create_table :members, id: :uuid, comment: 'メンバー' do |t|
      t.string :name, null: false
      t.text :description

      t.timestamps
    end

    create_table :members_tasks, id: false, comment: 'タスクとメンバーの関連テーブル' do |t|
      t.references :member, type: :uuid, foreign_key: true
      t.references :task, type: :uuid, foreign_key: true

      t.timestamps
    end
  end
end
```

これらの結果、生成されるテーブル群は次の通り。

```sql
                               Table "public.projects"
   Column    |            Type             | Collation | Nullable |      Default      
-------------+-----------------------------+-----------+----------+-------------------
 id          | uuid                        |           | not null | gen_random_uuid()
 name        | character varying           |           | not null | 
 description | text                        |           |          | 
 start_on    | date                        |           |          | 
 end_on      | date                        |           |          | 
 created_at  | timestamp without time zone |           | not null | 
 updated_at  | timestamp without time zone |           | not null | 
Indexes:
    "projects_pkey" PRIMARY KEY, btree (id)
Referenced by:
    TABLE "tasks" CONSTRAINT "fk_rails_02e851e3b7" FOREIGN KEY (project_id) REFERENCES projects(id)

                                 Table "public.tasks"
   Column    |            Type             | Collation | Nullable |      Default      
-------------+-----------------------------+-----------+----------+-------------------
 id          | uuid                        |           | not null | gen_random_uuid()
 project_id  | uuid                        |           |          | 
 name        | character varying           |           | not null | 
 description | text                        |           |          | 
 status      | integer                     |           |          | 0
 start_on    | date                        |           |          | 
 end_on      | date                        |           |          | 
 created_at  | timestamp without time zone |           | not null | 
 updated_at  | timestamp without time zone |           | not null | 
Indexes:
    "tasks_pkey" PRIMARY KEY, btree (id)
    "index_tasks_on_project_id" btree (project_id)
Foreign-key constraints:
    "fk_rails_02e851e3b7" FOREIGN KEY (project_id) REFERENCES projects(id)
Referenced by:
    TABLE "members_tasks" CONSTRAINT "fk_rails_61255d7478" FOREIGN KEY (task_id) REFERENCES tasks(id)

                                Table "public.members"
   Column    |            Type             | Collation | Nullable |      Default      
-------------+-----------------------------+-----------+----------+-------------------
 id          | uuid                        |           | not null | gen_random_uuid()
 name        | character varying           |           | not null | 
 description | text                        |           |          | 
 created_at  | timestamp without time zone |           | not null | 
 updated_at  | timestamp without time zone |           | not null | 
Indexes:
    "members_pkey" PRIMARY KEY, btree (id)
Referenced by:
    TABLE "members_tasks" CONSTRAINT "fk_rails_7c97f456df" FOREIGN KEY (member_id) REFERENCES members(id)

                       Table "public.members_tasks"
   Column   |            Type             | Collation | Nullable | Default 
------------+-----------------------------+-----------+----------+---------
 member_id  | uuid                        |           |          | 
 task_id    | uuid                        |           |          | 
 created_at | timestamp without time zone |           | not null | 
 updated_at | timestamp without time zone |           | not null | 
Indexes:
    "index_members_tasks_on_member_id" btree (member_id)
    "index_members_tasks_on_task_id" btree (task_id)
Foreign-key constraints:
    "fk_rails_61255d7478" FOREIGN KEY (task_id) REFERENCES tasks(id)
    "fk_rails_7c97f456df" FOREIGN KEY (member_id) REFERENCES members(id)
```

### モデルクラス

モデルの実装は次の通り。
UUIDをプライマリキーにしているからといって変わる部分はなく、
通常通り、リレーションを定義していけば良い。

```ruby:app/models/project.rb
class Project < ApplicationRecord
  has_many :tasks
end
```

```ruby:app/models/task.rb
class Task < ApplicationRecord
  belongs_to :project
  has_and_belongs_to_many :members

  enum status: { waiting: 0, doing: 1, done: 2, cancel: -1 }
end
```

```ruby:app/models/member.rb
class Member < ApplicationRecord
  has_and_belongs_to_many :tasks
end
```

### 動作確認

次の様なサンプルコードで動作確認してみる。

```ruby:サンプルコード

def sample
  # 予め登録されていると思われるオブジェクト
  project = Project.create(name: 'プロジェクト①');
  member1 = Member.create(name: 'メンバー①')

  task1 = Task.new(name: 'タスク-A', members: [member1])
  # アサインされるメンバーと同時登録
  task2 = Task.new(name: 'タスク-B', members: [member1, Member.new(name: 'メンバー②')]) 

  project.tasks = [task1, task2] # 複数タスクの同時の追加

  project.save
  project.reload

  pp project
  pp project.tasks
  pp project.tasks.last.members
end
```

結果は次の通り。
コードの通り各オブジェクトが保存できている。

```ruby
irb(main):220:0> sample
#<Project:0x00005610d4c8c450
 id: "492e5a1f-4899-4f00-94be-02afb3f46be2",
 name: "プロジェクト①",
 description: nil,
 start_on: nil,
 end_on: nil,
 created_at: Tue, 26 Feb 2019 13:52:30 UTC +00:00,
 updated_at: Tue, 26 Feb 2019 13:52:30 UTC +00:00>
  Task Load (0.4ms)  SELECT "tasks".* FROM "tasks" WHERE "tasks"."project_id" = $1  [["project_id", "492e5a1f-4899-4f00-94be-02afb3f46be2"]]
[#<Task:0x00005610d4d4a6f8
  id: "7dcc7181-5286-4912-b25c-f2fcaf362eac",
  project_id: "492e5a1f-4899-4f00-94be-02afb3f46be2",
  name: "タスク-A",
  description: nil,
  status: "waiting",
  start_on: nil,
  end_on: nil,
  created_at: Tue, 26 Feb 2019 13:52:30 UTC +00:00,
  updated_at: Tue, 26 Feb 2019 13:52:30 UTC +00:00>,
 #<Task:0x00005610d4d49f28
  id: "e8325765-053b-40c1-a909-be679a5fe739",
  project_id: "492e5a1f-4899-4f00-94be-02afb3f46be2",
  name: "タスク-B",
  description: nil,
  status: "waiting",
  start_on: nil,
  end_on: nil,
  created_at: Tue, 26 Feb 2019 13:52:30 UTC +00:00,
  updated_at: Tue, 26 Feb 2019 13:52:30 UTC +00:00>]
  Member Load (0.4ms)  SELECT "members".* FROM "members" INNER JOIN "members_tasks" ON "members"."id" = "members_tasks"."member_id" WHERE "members_tasks"."task_id" = $1  [["task_id", "e8325765-053b-40c1-a909-be679a5fe739"]]
[#<Member:0x00005610d4cadd58
  id: "b63a0b87-657c-424a-93e1-c7c8c6e5e6f1",
  name: "メンバー①",
  description: nil,
  created_at: Tue, 26 Feb 2019 13:52:30 UTC +00:00,
  updated_at: Tue, 26 Feb 2019 13:52:30 UTC +00:00>,
 #<Member:0x00005610d4cadbc8
  id: "06555612-3a12-43a0-95d0-f0d18eff8e8f",
  name: "メンバー②",
  description: nil,
  created_at: Tue, 26 Feb 2019 13:52:30 UTC +00:00,
  updated_at: Tue, 26 Feb 2019 13:52:30 UTC +00:00>]
=> nil
```

<details>
<summary> SQLも含む結果 </summary>
<div>

```ruby
irb(main):220:0> sample
   (0.7ms)  BEGIN
  Project Create (0.7ms)  INSERT INTO "projects" ("name", "created_at", "updated_at") VALUES ($1, $2, $3) RETURNING "id"  [["name", "プロジェクト①"], ["created_at", "2019-02-26 13:52:30.849742"], ["updated_at", "2019-02-26 13:52:30.849742"]]
   (1.8ms)  COMMIT
   (0.4ms)  BEGIN
  Member Create (2.2ms)  INSERT INTO "members" ("name", "created_at", "updated_at") VALUES ($1, $2, $3) RETURNING "id"  [["name", "メンバー①"], ["created_at", "2019-02-26 13:52:30.856421"], ["updated_at", "2019-02-26 13:52:30.856421"]]
   (1.1ms)  COMMIT
  Task Load (0.5ms)  SELECT "tasks".* FROM "tasks" WHERE "tasks"."project_id" = $1  [["project_id", "492e5a1f-4899-4f00-94be-02afb3f46be2"]]
   (0.3ms)  BEGIN
  Task Create (0.6ms)  INSERT INTO "tasks" ("project_id", "name", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id"  [["project_id", "492e5a1f-4899-4f00-94be-02afb3f46be2"], ["name", "タスク-A"], ["created_at", "2019-02-26 13:52:30.870659"], ["updated_at", "2019-02-26 13:52:30.870659"]]
  Task::HABTM_Members Create (0.5ms)  INSERT INTO "members_tasks" ("member_id", "task_id", "created_at", "updated_at") VALUES ($1, $2, $3, $4)  [["member_id", "b63a0b87-657c-424a-93e1-c7c8c6e5e6f1"], ["task_id", "7dcc7181-5286-4912-b25c-f2fcaf362eac"], ["created_at", "2019-02-26 13:52:30.872595"], ["updated_at", "2019-02-26 13:52:30.872595"]]
  Task Create (0.5ms)  INSERT INTO "tasks" ("project_id", "name", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id"  [["project_id", "492e5a1f-4899-4f00-94be-02afb3f46be2"], ["name", "タスク-B"], ["created_at", "2019-02-26 13:52:30.874447"], ["updated_at", "2019-02-26 13:52:30.874447"]]
  Task::HABTM_Members Create (0.5ms)  INSERT INTO "members_tasks" ("member_id", "task_id", "created_at", "updated_at") VALUES ($1, $2, $3, $4)  [["member_id", "b63a0b87-657c-424a-93e1-c7c8c6e5e6f1"], ["task_id", "e8325765-053b-40c1-a909-be679a5fe739"], ["created_at", "2019-02-26 13:52:30.876188"], ["updated_at", "2019-02-26 13:52:30.876188"]]
  Member Create (0.4ms)  INSERT INTO "members" ("name", "created_at", "updated_at") VALUES ($1, $2, $3) RETURNING "id"  [["name", "メンバー②"], ["created_at", "2019-02-26 13:52:30.877893"], ["updated_at", "2019-02-26 13:52:30.877893"]]
  Task::HABTM_Members Create (0.4ms)  INSERT INTO "members_tasks" ("member_id", "task_id", "created_at", "updated_at") VALUES ($1, $2, $3, $4)  [["member_id", "06555612-3a12-43a0-95d0-f0d18eff8e8f"], ["task_id", "e8325765-053b-40c1-a909-be679a5fe739"], ["created_at", "2019-02-26 13:52:30.879402"], ["updated_at", "2019-02-26 13:52:30.879402"]]
   (0.9ms)  COMMIT
   (0.4ms)  BEGIN
   (0.4ms)  COMMIT
  Project Load (1.2ms)  SELECT  "projects".* FROM "projects" WHERE "projects"."id" = $1 LIMIT $2  [["id", "492e5a1f-4899-4f00-94be-02afb3f46be2"], ["LIMIT", 1]]
#<Project:0x00005610d4c8c450
 id: "492e5a1f-4899-4f00-94be-02afb3f46be2",
 name: "プロジェクト①",
 description: nil,
 start_on: nil,
 end_on: nil,
 created_at: Tue, 26 Feb 2019 13:52:30 UTC +00:00,
 updated_at: Tue, 26 Feb 2019 13:52:30 UTC +00:00>
  Task Load (0.4ms)  SELECT "tasks".* FROM "tasks" WHERE "tasks"."project_id" = $1  [["project_id", "492e5a1f-4899-4f00-94be-02afb3f46be2"]]
[#<Task:0x00005610d4d4a6f8
  id: "7dcc7181-5286-4912-b25c-f2fcaf362eac",
  project_id: "492e5a1f-4899-4f00-94be-02afb3f46be2",
  name: "タスク-A",
  description: nil,
  status: "waiting",
  start_on: nil,
  end_on: nil,
  created_at: Tue, 26 Feb 2019 13:52:30 UTC +00:00,
  updated_at: Tue, 26 Feb 2019 13:52:30 UTC +00:00>,
 #<Task:0x00005610d4d49f28
  id: "e8325765-053b-40c1-a909-be679a5fe739",
  project_id: "492e5a1f-4899-4f00-94be-02afb3f46be2",
  name: "タスク-B",
  description: nil,
  status: "waiting",
  start_on: nil,
  end_on: nil,
  created_at: Tue, 26 Feb 2019 13:52:30 UTC +00:00,
  updated_at: Tue, 26 Feb 2019 13:52:30 UTC +00:00>]
  Member Load (0.4ms)  SELECT "members".* FROM "members" INNER JOIN "members_tasks" ON "members"."id" = "members_tasks"."member_id" WHERE "members_tasks"."task_id" = $1  [["task_id", "e8325765-053b-40c1-a909-be679a5fe739"]]
[#<Member:0x00005610d4cadd58
  id: "b63a0b87-657c-424a-93e1-c7c8c6e5e6f1",
  name: "メンバー①",
  description: nil,
  created_at: Tue, 26 Feb 2019 13:52:30 UTC +00:00,
  updated_at: Tue, 26 Feb 2019 13:52:30 UTC +00:00>,
 #<Member:0x00005610d4cadbc8
  id: "06555612-3a12-43a0-95d0-f0d18eff8e8f",
  name: "メンバー②",
  description: nil,
  created_at: Tue, 26 Feb 2019 13:52:30 UTC +00:00,
  updated_at: Tue, 26 Feb 2019 13:52:30 UTC +00:00>]
=> nil
```
</div>
</details>

## MySQLの場合

今度はMySQLでUUIDプライマリキーを試してみる。

[activeuuid](https://github.com/jashmenn/activeuuid)などを使う方法もあるが、ここではgemには頼らず、文字列としてUUIDをプライマリキーにしてみる。

### マイグレーションファイル

最初は`projects`テーブルから。
MySQLには`uuid`型なんて存在しないので、`id`を`string`にして、UUIDを格納する。
UUIDは36文字なので`limit: 36`を設定するために一旦、`id: false`として、`primary_key: true`となるカラムを定義している。
`varchar(255)`になるのを気にしないなら、`create_table`に`id: :string`を指定してもいいかと思う。

```ruby:ruby:db/migrate/xxxxxxxxxxxxxx_create_projects.rb
class CreateProjects < ActiveRecord::Migration[5.2]
  def change
    create_table :projects, id: false, comment: 'プロジェクト' do |t|
      t.string :id, limit: 36, null: false, primary_key: true, comment: 'プライマリキー'
      t.string :name, null: false, comment: 'プロジェクト名'
      t.text :description, comment: '概要'
      t.date :start_on, comment: '開始日'
      t.date :end_on, comment: '終了日'

      t.timestamps
    end
  end
end
```

続いて `tasks`テーブルもIDの定義については`projects`と同様。
`projects`を参照する`references`については、指定しない場合、`bigint`
型で作ろうとして、型の不一致のエラーが発生するため、`type: :string`を指定している。

```ruby:ruby:db/migrate/xxxxxxxxxxxxxx_create_tasks.rb
class CreateTasks < ActiveRecord::Migration[5.2]
  def change
    create_table :tasks, id: false, command: 'タスク' do |t|
      t.string :id, limit: 36, null: false, primary_key: true, comment: 'プライマリキー'
      t.references :project, type: :string, foreign_key: true
      t.string :name, null: false, command: 'タスク名'
      t.text :description, comment: '概要'
      t.integer :status, default: 0, comment: 'ステータス'
      t.date :start_on, comment: '開始日'
      t.date :end_on, comment: '終了日'

      t.timestamps
    end
  end
end
```

`members`と`members_tasks`も同様。

```ruby:ruby:db/migrate/xxxxxxxxxxxxxx_create_members.rb
class CreateMembers < ActiveRecord::Migration[5.2]
  def change
    create_table :members, id: false, comment: 'メンバー' do |t|
      t.string :id, limit: 36, null: false, primary_key: true, comment: 'プライマリキー'
      t.string :name, null: false
      t.text :description

      t.timestamps
    end

    create_table :members_tasks, id: false, comment: 'タスクとメンバーの関連テーブル' do |t|
      t.references :member, type: :string, foreign_key: true
      t.references :task, type: :string, foreign_key: true

      t.timestamps
    end
  end
end
```

これらのマイグレーションから生成されるテーブルは次の通り。

```sql
mysql> desc projects;
+-------------+--------------+------+-----+---------+-------+
| Field       | Type         | Null | Key | Default | Extra |
+-------------+--------------+------+-----+---------+-------+
| id          | varchar(36)  | NO   | PRI | NULL    |       |
| name        | varchar(255) | NO   |     | NULL    |       |
| description | text         | YES  |     | NULL    |       |
| start_on    | date         | YES  |     | NULL    |       |
| end_on      | date         | YES  |     | NULL    |       |
| created_at  | datetime     | NO   |     | NULL    |       |
| updated_at  | datetime     | NO   |     | NULL    |       |
+-------------+--------------+------+-----+---------+-------+
7 rows in set (0.00 sec)

mysql> desc tasks;
+-------------+--------------+------+-----+---------+-------+
| Field       | Type         | Null | Key | Default | Extra |
+-------------+--------------+------+-----+---------+-------+
| id          | varchar(36)  | NO   | PRI | NULL    |       |
| project_id  | varchar(255) | YES  | MUL | NULL    |       |
| name        | varchar(255) | NO   |     | NULL    |       |
| description | text         | YES  |     | NULL    |       |
| status      | int(11)      | YES  |     | 0       |       |
| start_on    | date         | YES  |     | NULL    |       |
| end_on      | date         | YES  |     | NULL    |       |
| created_at  | datetime     | NO   |     | NULL    |       |
| updated_at  | datetime     | NO   |     | NULL    |       |
+-------------+--------------+------+-----+---------+-------+
9 rows in set (0.00 sec)

mysql> desc members;
+-------------+--------------+------+-----+---------+-------+
| Field       | Type         | Null | Key | Default | Extra |
+-------------+--------------+------+-----+---------+-------+
| id          | varchar(36)  | NO   | PRI | NULL    |       |
| name        | varchar(255) | NO   |     | NULL    |       |
| description | text         | YES  |     | NULL    |       |
| created_at  | datetime     | NO   |     | NULL    |       |
| updated_at  | datetime     | NO   |     | NULL    |       |
+-------------+--------------+------+-----+---------+-------+
5 rows in set (0.00 sec)

mysql> desc members_tasks;
+------------+--------------+------+-----+---------+-------+
| Field      | Type         | Null | Key | Default | Extra |
+------------+--------------+------+-----+---------+-------+
| member_id  | varchar(255) | YES  | MUL | NULL    |       |
| task_id    | varchar(255) | YES  | MUL | NULL    |       |
| created_at | datetime     | NO   |     | NULL    |       |
| updated_at | datetime     | NO   |     | NULL    |       |
+------------+--------------+------+-----+---------+-------+
4 rows in set (0.00 sec)
```

## モデルクラス

```ruby:app/models/project.rb
class Project < ApplicationRecord
  include IdGenerator

  has_many :tasks
end
```

```ruby:app/models/task.rb
class Task < ApplicationRecord
  include IdGenerator

  belongs_to :project
  has_and_belongs_to_many :members

  enum status: { waiting: 0, doing: 1, done: 2, cancel: -1 }
end
```

```ruby:app/models/member.rb
class Member < ApplicationRecord
  include IdGenerator

  has_and_belongs_to_many :tasks
end
```

各モデルクラスの実装はPostgreSQLの場合とほぼ変わらない。
`include IdGenerator` を除いては。

何をインクルードしているかというと次のモジュール。

```ruby:app/models/concerns/id_generator.rb
module IdGenerator
  def self.included(klass)
    klass.before_create :fill_id
  end

  def fill_id
    self.id = loop do
      uuid = SecureRandom.uuid
      break uuid unless self.class.exists?(id: uuid)
    end
  end
end
```

PostgreSQLではテーブル定義でIDのデフォルト値に`gen_random_uuid()`という関数が設定されてた。
なので、データベース側でIDにUUIDを設定していた。

MySQL５．７では、関数をカラムのデフォルト値にできないので、Ruby側で作ってIDに設定する。
このモジュールをインクルードすると`before_create`フックでそれを行う。

### 動作確認

先程のサンプルコードを同じく実行してみた。

次の様に、コードの通り登録されている。
ただ、`reload`後の`project.tasks`の順序が異なっている。

```ruby
irb(main):015:0> sample
#<Project:0x000056398a8b82a0
 id: "cb3cd510-b631-49df-bb5b-a8984b6496e0",
 name: "プロジェクト①",
 description: nil,
 start_on: nil,
 end_on: nil,
 created_at: Tue, 26 Feb 2019 13:53:46 UTC +00:00,
 updated_at: Tue, 26 Feb 2019 13:53:46 UTC +00:00>
  Task Load (0.5ms)  SELECT `tasks`.* FROM `tasks` WHERE `tasks`.`project_id` = 'cb3cd510-b631-49df-bb5b-a8984b6496e0'
[#<Task:0x0000563989984fe0
  id: "9b133316-1d82-4489-8eb2-e1db7611c842",
  project_id: "cb3cd510-b631-49df-bb5b-a8984b6496e0",
  name: "タスク-B",
  description: nil,
  status: "waiting",
  start_on: nil,
  end_on: nil,
  created_at: Tue, 26 Feb 2019 13:53:47 UTC +00:00,
  updated_at: Tue, 26 Feb 2019 13:53:47 UTC +00:00>,
 #<Task:0x0000563989984630
  id: "f6c996b1-6523-4bf8-8fc3-2d4aa5110c42",
  project_id: "cb3cd510-b631-49df-bb5b-a8984b6496e0",
  name: "タスク-A",
  description: nil,
  status: "waiting",
  start_on: nil,
  end_on: nil,
  created_at: Tue, 26 Feb 2019 13:53:47 UTC +00:00,
  updated_at: Tue, 26 Feb 2019 13:53:47 UTC +00:00>]
  Member Load (0.5ms)  SELECT `members`.* FROM `members` INNER JOIN `members_tasks` ON `members`.`id` = `members_tasks`.`member_id` WHERE `members_tasks`.`task_id` = 'f6c996b1-6523-4bf8-8fc3-2d4aa5110c42'
[#<Member:0x0000563989a28ed8
  id: "ba74e512-6924-42c4-8bd5-c69bdb0b837f",
  name: "メンバー①",
  description: nil,
  created_at: Tue, 26 Feb 2019 13:53:46 UTC +00:00,
  updated_at: Tue, 26 Feb 2019 13:53:46 UTC +00:00>]
=> nil
```

<details>
<summary> SQLも含む結果 </summary>
<div>

```ruby
irb(main):015:0> sample
   (0.6ms)  BEGIN
  Project Exists (0.6ms)  SELECT  1 AS one FROM `projects` WHERE `projects`.`id` = 'cb3cd510-b631-49df-bb5b-a8984b6496e0' LIMIT 1
  Project Create (0.7ms)  INSERT INTO `projects` (`id`, `name`, `created_at`, `updated_at`) VALUES ('cb3cd510-b631-49df-bb5b-a8984b6496e0', 'プロジェクト①', '2019-02-26 13:53:46', '2019-02-26 13:53:46')
   (2.4ms)  COMMIT
   (0.5ms)  BEGIN
  Member Exists (0.7ms)  SELECT  1 AS one FROM `members` WHERE `members`.`id` = 'ba74e512-6924-42c4-8bd5-c69bdb0b837f' LIMIT 1
  Member Create (0.5ms)  INSERT INTO `members` (`id`, `name`, `created_at`, `updated_at`) VALUES ('ba74e512-6924-42c4-8bd5-c69bdb0b837f', 'メンバー①', '2019-02-26 13:53:46', '2019-02-26 13:53:46')
   (4.0ms)  COMMIT
  Task Load (0.6ms)  SELECT `tasks`.* FROM `tasks` WHERE `tasks`.`project_id` = 'cb3cd510-b631-49df-bb5b-a8984b6496e0'
   (0.5ms)  BEGIN
  Task Exists (0.9ms)  SELECT  1 AS one FROM `tasks` WHERE `tasks`.`id` = 'f6c996b1-6523-4bf8-8fc3-2d4aa5110c42' LIMIT 1
  Task Create (0.7ms)  INSERT INTO `tasks` (`id`, `project_id`, `name`, `created_at`, `updated_at`) VALUES ('f6c996b1-6523-4bf8-8fc3-2d4aa5110c42', 'cb3cd510-b631-49df-bb5b-a8984b6496e0', 'タスク-A', '2019-02-26 13:53:47', '2019-02-26 13:53:47')
  Task::HABTM_Members Create (0.6ms)  INSERT INTO `members_tasks` (`member_id`, `task_id`, `created_at`, `updated_at`) VALUES ('ba74e512-6924-42c4-8bd5-c69bdb0b837f', 'f6c996b1-6523-4bf8-8fc3-2d4aa5110c42', '2019-02-26 13:53:47', '2019-02-26 13:53:47')
  Task Exists (1.0ms)  SELECT  1 AS one FROM `tasks` WHERE `tasks`.`id` = '9b133316-1d82-4489-8eb2-e1db7611c842' LIMIT 1
  Task Create (0.9ms)  INSERT INTO `tasks` (`id`, `project_id`, `name`, `created_at`, `updated_at`) VALUES ('9b133316-1d82-4489-8eb2-e1db7611c842', 'cb3cd510-b631-49df-bb5b-a8984b6496e0', 'タスク-B', '2019-02-26 13:53:47', '2019-02-26 13:53:47')
  Task::HABTM_Members Create (0.6ms)  INSERT INTO `members_tasks` (`member_id`, `task_id`, `created_at`, `updated_at`) VALUES ('ba74e512-6924-42c4-8bd5-c69bdb0b837f', '9b133316-1d82-4489-8eb2-e1db7611c842', '2019-02-26 13:53:47', '2019-02-26 13:53:47')
  Member Exists (0.5ms)  SELECT  1 AS one FROM `members` WHERE `members`.`id` = '3db54957-bcf9-44b9-b033-aaa15a79cb8c' LIMIT 1
  Member Create (0.7ms)  INSERT INTO `members` (`id`, `name`, `created_at`, `updated_at`) VALUES ('3db54957-bcf9-44b9-b033-aaa15a79cb8c', 'メンバー②', '2019-02-26 13:53:47', '2019-02-26 13:53:47')
  Task::HABTM_Members Create (0.6ms)  INSERT INTO `members_tasks` (`member_id`, `task_id`, `created_at`, `updated_at`) VALUES ('3db54957-bcf9-44b9-b033-aaa15a79cb8c', '9b133316-1d82-4489-8eb2-e1db7611c842', '2019-02-26 13:53:47', '2019-02-26 13:53:47')
   (1.0ms)  COMMIT
   (0.3ms)  BEGIN
   (0.2ms)  COMMIT
  Project Load (0.5ms)  SELECT  `projects`.* FROM `projects` WHERE `projects`.`id` = 'cb3cd510-b631-49df-bb5b-a8984b6496e0' LIMIT 1
#<Project:0x000056398a8b82a0
 id: "cb3cd510-b631-49df-bb5b-a8984b6496e0",
 name: "プロジェクト①",
 description: nil,
 start_on: nil,
 end_on: nil,
 created_at: Tue, 26 Feb 2019 13:53:46 UTC +00:00,
 updated_at: Tue, 26 Feb 2019 13:53:46 UTC +00:00>
  Task Load (0.5ms)  SELECT `tasks`.* FROM `tasks` WHERE `tasks`.`project_id` = 'cb3cd510-b631-49df-bb5b-a8984b6496e0'
[#<Task:0x0000563989984fe0
  id: "9b133316-1d82-4489-8eb2-e1db7611c842",
  project_id: "cb3cd510-b631-49df-bb5b-a8984b6496e0",
  name: "タスク-B",
  description: nil,
  status: "waiting",
  start_on: nil,
  end_on: nil,
  created_at: Tue, 26 Feb 2019 13:53:47 UTC +00:00,
  updated_at: Tue, 26 Feb 2019 13:53:47 UTC +00:00>,
 #<Task:0x0000563989984630
  id: "f6c996b1-6523-4bf8-8fc3-2d4aa5110c42",
  project_id: "cb3cd510-b631-49df-bb5b-a8984b6496e0",
  name: "タスク-A",
  description: nil,
  status: "waiting",
  start_on: nil,
  end_on: nil,
  created_at: Tue, 26 Feb 2019 13:53:47 UTC +00:00,
  updated_at: Tue, 26 Feb 2019 13:53:47 UTC +00:00>]
  Member Load (0.5ms)  SELECT `members`.* FROM `members` INNER JOIN `members_tasks` ON `members`.`id` = `members_tasks`.`member_id` WHERE `members_tasks`.`task_id` = 'f6c996b1-6523-4bf8-8fc3-2d4aa5110c42'
[#<Member:0x0000563989a28ed8
  id: "ba74e512-6924-42c4-8bd5-c69bdb0b837f",
  name: "メンバー①",
  description: nil,
  created_at: Tue, 26 Feb 2019 13:53:46 UTC +00:00,
  updated_at: Tue, 26 Feb 2019 13:53:46 UTC +00:00>]
=> nil
```
</div>
</details>

### では、MySQL 8.1.15 なら？

MySQLでもデフォルト値に関数を設定して、データベース側でUUIDを生成できれば、Ruby側でゴニョらなくてもいいのにと思っていたら、MySQL 8.0.13で、カラム定義のDEFAULTに関数が指定できるようになったようだ。

で、8系の最新バージョンのMySQL ８.0.15でも試してみた。

マイグレーションは次の通り。

```ruby
class CreateProjects < ActiveRecord::Migration[5.2]
  def change
    create_table :projects, id: false, comment: 'プロジェクト' do |t|
      t.string :id, limit: 36, null: false, primary_key: true, default: ->{"(uuid())"}, comment: 'プライマリキー'
      t.string :name, null: false, comment: 'プロジェクト名'
      t.text :description, comment: '概要'
      t.date :start_on, comment: '開始日'
      t.date :end_on, comment: '終了日'

      t.timestamps
    end
  end
end
```

`default: ->{"(uuid())"}`でカラムのデフォルトに`uuid()`関数を設定している。
これで作られるテーブルは次の通り。Extraが`DEFAULT_GENERATED`となっていてよくわからないが、`uuid()`関数が設定されている。

```sql
mysql> desc projects;
+-------------+--------------+------+-----+---------+-------------------+
| Field       | Type         | Null | Key | Default | Extra             |
+-------------+--------------+------+-----+---------+-------------------+
| id          | varchar(36)  | NO   | PRI | NULL    | DEFAULT_GENERATED |
| name        | varchar(255) | NO   |     | NULL    |                   |
| description | text         | YES  |     | NULL    |                   |
| start_on    | date         | YES  |     | NULL    |                   |
| end_on      | date         | YES  |     | NULL    |                   |
| created_at  | datetime     | NO   |     | NULL    |                   |
| updated_at  | datetime     | NO   |     | NULL    |                   |
+-------------+--------------+------+-----+---------+-------------------+
7 rows in set (0.00 sec)
```

モデルクラスの実装は次の様に`id`の生成をRuby側では行わないようにした。

```ruby
class Project < ApplicationRecord
end
```

## 動作確認

`Task`、`Member`も同様に実装して動作を確認してみた。

```ruby
irb(main):001:0> project = Project.create(name: 'プロジェクト')
   (0.8ms)  SET NAMES utf8mb4 COLLATE utf8mb4_general_ci,  @@SESSION.sql_mode = CONCAT(CONCAT(@@sql_mode, ',STRICT_ALL_TABLES'), ',NO_AUTO_VALUE_ON_ZERO'),  @@SESSION.sql_auto_is_null = 0, @@SESSION.wait_timeout = 2147483
   (0.5ms)  BEGIN
  Project Create (1.6ms)  INSERT INTO `projects` (`name`, `created_at`, `updated_at`) VALUES ('プロジェクト', '2019-02-25 15:35:50', '2019-02-25 15:35:50')
   (4.1ms)  COMMIT
=> #<Project id: "0", name: "プロジェクト", description: nil, start_on: nil, end_on: nil, created_at: "2019-02-25 15:35:50", updated_at: "2019-02-25 15:35:50">
irb(main):002:0> pp project
#<Project:0x00005644fe35f860
 id: "0",
 name: "プロジェクト",
 description: nil,
 start_on: nil,
 end_on: nil,
 created_at: Mon, 25 Feb 2019 15:35:50 UTC +00:00,
 updated_at: Mon, 25 Feb 2019 15:35:50 UTC +00:00>
=> #<Project id: "0", name: "プロジェクト", description: nil, start_on: nil, end_on: nil, created_at: "2019-02-25 15:35:50", updated_at: "2019-02-25 15:35:50">
irb(main):003:0> Project.all.ids
   (0.8ms)  SELECT `projects`.`id` FROM `projects`
=> ["077cacaa-3913-11e9-aacc-0242ac1c0002"]
```

`Project`を`create`してみると、確かに登録されているのだが、`id: "0"`となっている。
しかし、DBにはちゃんとUUIDで保存されている。

先程のサンプルコードも実行してみると次の様にエラーとなる。

```ruby
irb(main):016:0> sample
  Task Destroy (0.8ms)  DELETE FROM `tasks`
  Member Destroy (0.8ms)  DELETE FROM `members`
  Project Destroy (4.9ms)  DELETE FROM `projects`
   (0.4ms)  BEGIN
  Project Create (0.7ms)  INSERT INTO `projects` (`name`, `created_at`, `updated_at`) VALUES ('プロジェクト①', '2019-02-26 13:17:54', '2019-02-26 13:17:54')
   (3.2ms)  COMMIT
   (0.4ms)  BEGIN
  Member Create (0.8ms)  INSERT INTO `members` (`name`, `created_at`, `updated_at`) VALUES ('メンバー①', '2019-02-26 13:17:54', '2019-02-26 13:17:54')
  Member Load (0.5ms)  SELECT  `members`.* FROM `members` WHERE `members`.`id` = '0' LIMIT 1
   (1.2ms)  ROLLBACK
Traceback (most recent call last):
        2: from (irb):16
        1: from app/models/member.rb:2:in `block in <class:Member>'
ActiveRecord::RecordNotFound (Couldn't find Member with 'id'=0)
```

どうも`create`した後のインスタンスにIDがセットされない。
そのため、そのインスタンスをviewでそのまま使おうとするとおかしくなると思う。
上記のようなnewしただけのオブジェクトで関連させて、いっぺんに`save`するケースやパスヘルパー(`edit_project_path`など)の引数に使うなど。
そして、`id`がわからないので`reload`もできない。

この様な場合でも、`create`時にDB側で生成される値を`save`や`create`後のオブジェクトに反映する方法はないのだろうか…。

## UUIDプライマリキーの使いどころ

プライマリキーにUUIDを使うことのデメリットは次が考えられる

- データサイズが大きくなる
- リソースのIDとしてパスにUUIDを使った場合にURLが長くなる
    - 階層的なリソースの場合、辛い長さになると思う
- 調査等で直接SQLを叩く場合に面倒くさい

では、UUIDプライマリキーはどういうときに使うとよいのだろうか？

- データの登録数を推測されたくないケース
    - ECサイトなどで購入IDなどに連番を使うと購入件数などが推測されてしまう。それを避けたい場合。
- サイトをクロールされたくないケース
    - URLで連番を晒していると、順にクロールしやすい
- 整数型の最大値以上のレコード数が見込まれるケース
    - まあ、そんなにたくさんのレコードが１テーブルに入ってしまうこと自体を避けるべきかと思うが[^2]。

[^2]: たとえ古いレコードを削除してもIDの使い回しはちょっと気持ち悪いしなぁ

## 参考

- [Active Record と PostgreSQL \- Rails ガイド](https://railsguides.jp/active_record_postgresql.html#uuid%E4%B8%BB%E3%82%AD%E3%83%BC)
- [DockerでRails5\.2\.1とMySQL8を動かすgitプロジェクトを作る方法 : 試行錯誤な日々](http://asukiaaa.blogspot.com/2018/11/dockerrails521mysql8git.html)
- [Railsでuuidを取り扱う時のTips \- Qiita](https://qiita.com/okutaku/items/97c651132796b94aad23)
- [日々の覚書: MySQL 8\.0\.13でカラム定義のDEFAULTに関数が指定できるようになった](https://yoku0825.blogspot.com/2018/10/mysql-8013default.html)