6
0

More than 5 years have passed since last update.

Rails5.2.3+MySQL+Ridgepoleでテーブルの主キーにUUIDを使う

Posted at

概要

主キーにUUIDを使用したいという場面は前からよくあるようで、ここではその是非には触れず、今回自分が採用した方法について残しておきます。

# ローカルでの実行
> ruby -v
ruby 2.6.3p62 (2019-04-16 revision 67580) [x86_64-darwin18]

> mysql -uroot
# 〜省略〜
Server version: 5.7.20 Homebrew
# 〜省略〜

> rails -v
Rails 5.2.3

> ridgepole -v
ridgepole 0.7.7

tl;dr

schemaファイル

create_table "hoges", id: :string, limit: 36, force: :cascade, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8" do |t|
  t.integer "foo", comment: "foo!!"
  t.string "bar", comment: "bar!!"
end

モデル

app/models/hoge.rb
class Hoge < ApplicationRecord
  include PrimaryKeyUuidAutoGeneratable
end
concerns/primary_key_uuid_auto_generatable.rb
module PrimaryKeyUuidAutoGeneratable
  extend ActiveSupport::Concern

  included do
    before_create :generate_uuid
  end

  def generate_uuid
    return if self.id.present?

    self.id = SecureRandom.uuid
  end
end

他の方法?

activeuuid

RailsとUUIDでググるとactiveuuidが散見されるのだけど、bundle installすると全てのrailsコマンドが壊れる。

NoMethodError: undefined method `alias_method_chain' for ActiveRecord::ConnectionAdapters::Column:Class

なんじゃあそりゃあと思ったら、どうやらRails5でdeprecatedになったメソッドのようだ。
まじかい、最近メンテされてないんかなと思ってよく見たら

Latest commit 4f8e0ef on 2 Sep 2015

😭

Rails5から導入された、idの型がデフォルトでUUIDになる設定

github.com/rails/rails/pull/21762

ぽすぐれだったら良いかもだけど、MySQLでこれできてもUUID型が存在しないので意味がない...

Ridgepoleでの書き方

最初は

create_table "other_services_posts", id: :false, force: :cascade, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8" do |t|
  t.string "id", limit: 36, null: false, primary_key: true, comment: "主キー"
  # 〜省略〜
end

と書いていて、applyもできるしこれでいいやと思っていたら、2度目のapply実行時に下記エラー。

Apply `db/Schemafile`
[WARNING] Primary key definition of `hoges` differ but `allow_pk_change` option is false
  from: {:id=>:string, :limit=>36}
    to: {:id=>false}

[ERROR] Mysql2::Error: Duplicate column name 'id': ALTER TABLE `hoges` ADD `id` varchar(36) NOT NULL PRIMARY KEY AFTER `id`

えー
どうもcreate_tableid: falseが2回目以降に矛盾を起こすらしい。

下記の様にcreate_table側の設定に書いたら2回目も問題なく実行されました。

create_table "hoges", id: :string, limit: 36, force: :cascade, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8" do |t|
  # 〜省略〜
end

レコード作成時のデフォルト値

MySQLにはデフォルト値に関数を指定できないので、ぽすぐれのようにDB側でUUIDのデフォルト値を持たせることができません。
ので、Rails側で値を入れる必要があります。

concerns/primary_key_uuid_auto_generatable.rb
module PrimaryKeyUuidAutoGeneratable
  extend ActiveSupport::Concern

  included do
    before_create :generate_uuid
  end

  def generate_uuid
    # 既に値が入っている場合は何もしない
    return if self.id.present?

    # ここでは作成したUUIDが既に使用されているか否かのチェックをしない、理由は以下。
    # - 毎回レコードの作成時にSQLが余分に発行されるのを避けたい
    # - UUIDの性質上その重複をアプリケーションレイヤーで調べる必要はない
    # - 仮に重複したとしてもDBの主キー制約によって保存はできない
    self.id = SecureRandom.uuid
  end
end

limit: 36

この設定が正しく動作しているかどうかについては、こう言う感じに細工すると確認できます。

id_generator.rb
  self.id = SecureRandom.uuid + "1"
> Hoge.create
   (0.2ms)  BEGIN
  Hoge Create (0.5ms)  INSERT INTO `hoges` (`id`, `created_at`, `updated_at`) VALUES ('e92a6b37-9bec-4452-9205-163e3dad98111', '2019-05-02 13:31:56', '2019-05-02 13:31:56')
   (0.2ms)  ROLLBACK
ActiveRecord::ValueTooLong: Mysql2::Error: Data too long for column 'id' at row 1: INSERT INTO `hoges` (`id`, `created_at`, `updated_at`) VALUES ('e92a6b37-9bec-4452-9205-163e3dad98111', '2019-05-02 13:31:56', '2019-05-02 13:31:56')
Caused by Mysql2::Error: Data too long for column 'id' at row 1

1文字足しただけでちゃんとエラーになっています。

参考

6
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
0