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で現状使えるバージョンに近しいものを選択した。
検証モデル
次のようなプロジェクトごとにタスクを登録し、それぞれのタスクにメンバーをアサインするモデルを考える。
次のような関連を持っている。
-
Project
とTask
は 一対多 -
Task
とMember
は 多対多(タスクには複数人をアサイン可能)
PostgreSQLの場合
PostgreSQLはUUID型があるのでプライマリキーの型をUUIDにする。
マイグレーションファイル
各モデルともIDはUUIDとする。
最初のテーブルprojects
を作成する前にenable_extension 'pgcrypto'
を実行してPostgreSQLの拡張を有効化する。
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
でわざわざ指定している。
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
としている。
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をプライマリキーにしているからといって変わる部分はなく、
通常通り、リレーションを定義していけば良い。
class Project < ApplicationRecord
has_many :tasks
end
class Task < ApplicationRecord
belongs_to :project
has_and_belongs_to_many :members
enum status: { waiting: 0, doing: 1, done: 2, cancel: -1 }
end
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
型なんて存在しないので、id
をstring
にして、UUIDを格納する。
UUIDは36文字なのでlimit: 36
を設定するために一旦、id: false
として、primary_key: true
となるカラムを定義している。
varchar(255)
になるのを気にしないなら、create_table
にid: :string
を指定してもいいかと思う。
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
を指定している。
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
も同様。
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)
モデルクラス
class Project < ApplicationRecord
include IdGenerator
has_many :tasks
end
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
class Member < ApplicationRecord
include IdGenerator
has_and_belongs_to_many :tasks
end
各モデルクラスの実装はPostgreSQLの場合とほぼ変わらない。
include IdGenerator
を除いては。
何をインクルードしているかというと次のモジュール。
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
動作確認
Task
、Member
も同様に実装して動作を確認してみた。
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で保存されている。
先程のサンプルコードも実行してみると次の様にエラーとなる。
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で連番を晒していると、順にクロールしやすい
- 整数型の最大値以上のレコード数が見込まれるケース
- まあ、そんなにたくさんのレコードが1テーブルに入ってしまうこと自体を避けるべきかと思うが1。
参考
- Active Record と PostgreSQL - Rails ガイド
- DockerでRails5.2.1とMySQL8を動かすgitプロジェクトを作る方法 : 試行錯誤な日々
- Railsでuuidを取り扱う時のTips - Qiita
- 日々の覚書: MySQL 8.0.13でカラム定義のDEFAULTに関数が指定できるようになった
-
たとえ古いレコードを削除してもIDの使い回しはちょっと気持ち悪いしなぁ ↩