MySQL
PostgreSQL
UUID
Rails5

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で現状使えるバージョンに近しいものを選択した。


検証モデル

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

model.png

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



  • ProjectTask は 一対多


  • TaskMember は 多対多(タスクには複数人をアサイン可能)


PostgreSQLの場合

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


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

各モデルともIDはUUIDとする。

最初のテーブルprojectsを作成する前にenable_extension 'pgcrypto'を実行してPostgreSQLの拡張を有効化する。


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でわざわざ指定している。


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_tasksid: falseとしている。


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


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

                               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をプライマリキーにしているからといって変わる部分はなく、

通常通り、リレーションを定義していけば良い。


app/models/project.rb

class Project < ApplicationRecord

has_many :tasks
end


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



app/models/member.rb

class Member < ApplicationRecord

has_and_belongs_to_many :tasks
end


動作確認

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


サンプルコード


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


結果は次の通り。

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

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



SQLも含む結果

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






MySQLの場合

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

activeuuidなどを使う方法もあるが、ここではgemには頼らず、文字列としてUUIDをプライマリキーにしてみる。


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

最初はprojectsテーブルから。

MySQLにはuuid型なんて存在しないので、idstringにして、UUIDを格納する。

UUIDは36文字なのでlimit: 36を設定するために一旦、id: falseとして、primary_key: trueとなるカラムを定義している。

varchar(255)になるのを気にしないなら、create_tableid: :stringを指定してもいいかと思う。


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

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


membersmembers_tasksも同様。


ruby

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


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

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)


モデルクラス


app/models/project.rb

class Project < ApplicationRecord

include IdGenerator

has_many :tasks
end



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



app/models/member.rb

class Member < ApplicationRecord

include IdGenerator

has_and_belongs_to_many :tasks
end


各モデルクラスの実装はPostgreSQLの場合とほぼ変わらない。

include IdGenerator を除いては。

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


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を設定していた。

MySQL5.7では、関数をカラムのデフォルト値にできないので、Ruby側で作ってIDに設定する。

このモジュールをインクルードするとbefore_createフックでそれを行う。


動作確認

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

次の様に、コードの通り登録されている。

ただ、reload後のproject.tasksの順序が異なっている。

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



SQLも含む結果

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






では、MySQL 8.1.15 なら?

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

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

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

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()関数が設定されている。

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側では行わないようにした。

class Project < ApplicationRecord

end


動作確認

TaskMemberも同様に実装して動作を確認してみた。

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"]

Projectcreateしてみると、確かに登録されているのだが、id: "0"となっている。

しかし、DBにはちゃんとUUIDで保存されている。

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

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側で生成される値をsavecreate後のオブジェクトに反映する方法はないのだろうか…。


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

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


  • データサイズが大きくなる

  • リソースのIDとしてパスにUUIDを使った場合にURLが長くなる


    • 階層的なリソースの場合、辛い長さになると思う



  • 調査等で直接SQLを叩く場合に面倒くさい

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


  • データの登録数を推測されたくないケース


    • ECサイトなどで購入IDなどに連番を使うと購入件数などが推測されてしまう。それを避けたい場合。



  • サイトをクロールされたくないケース


    • URLで連番を晒していると、順にクロールしやすい



  • 整数型の最大値以上のレコード数が見込まれるケース


    • まあ、そんなにたくさんのレコードが1テーブルに入ってしまうこと自体を避けるべきかと思うが1




参考





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