Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

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の使い回しはちょっと気持ち悪いしなぁ 

HeRo
エンジニア
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away