概要
主キーに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
モデル
class Hoge < ApplicationRecord
include PrimaryKeyUuidAutoGeneratable
end
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_table
のid: 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側で値を入れる必要があります。
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
この設定が正しく動作しているかどうかについては、こう言う感じに細工すると確認できます。
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文字足しただけでちゃんとエラーになっています。
参考
-
- 最も参考になったのが@HeRoさんの記事でした、ありがとうございました!!