この記事の目的
Railsのマイグレーションでは各テーブルのプライマリーキーにデフォルトでid
が追加される仕様になってる。
何も指定しなくてもid
がBIGINT型
で付与されて自動でインクリメントされるのは非常に便利だけど、アプリケーションの構成によっては文字列で登録したい時がある。
例えばQiitaでも各記事は8f8f1d1e5333da561fd8
みたいな文字の羅列がIDとして付与されてる(たぶん)。こんな感じでid
を文字列(VARCHAR型
)として扱う場合の手順を説明します。
尚、今回使用してるDBはPostgreSQL、RailsのバージョンはRails5です。
結論
下記の手順で実現できます。modelで付与するidのロジックはお好みで。
-
create_table
メソッドのオプションにid: :string
を指定する。 - 子テーブルの外部キーは
add_foreign_key
とadd_index
で指定する。 -before_create
とかでModelでidを初期化する処理をフックする。
それじゃやってみよう
Hoge
テーブルとFoo
テーブルが親子関係にある場合、Hoge
テーブルのIDを文字列にしたいぜ!って内容で説明します。
Hoge
テーブルのマイグレーション
id
の型ってオプションで変えられないかなーと思いAPIドキュメントを参照してみました。
A Symbol can be used to specify the type of the generated primary key column.
あ、もうこれで解決ですやん。。。。
class CreateHoges < ActiveRecord::Migration[5.1]
def change
create_table :hoges, id: :string do |t|
t.string :name, null: false
t.timestamps
end
end
end
Foo
テーブルのマイグレーション
親テーブルの定義があまりに簡単だったんで、子テーブルもt.belongs_to :hoge, null: false, foreign_key: true
で普通に書いてマイグレーションしてみる。
rails aborted!
StandardError: An error has occurred, this and all later migrations canceled:
PG::DatatypeMismatch: ERROR: foreign key constraint "fk_rails_484f6f4b4a" cannot be implemented
DETAIL: Key columns "hoge_id" and "id" are of incompatible types: bigint and character varying.
: CREATE TABLE "foos" ("id" bigserial primary key, "hoge_id" bigint NOT NULL, "name" character varying NOT NULL, "created_at" timestamp NOT NULL, "updated_at" timestamp NOT NULL, CONSTRAINT "fk_rails_484f6f4b4a"
FOREIGN KEY ("hoge_id")
REFERENCES "hoges" ("id")
)
なんだとこのやろう
とりあえず穏やかな気持ちでエラーメッセージを見てみよう。
DETAIL: Key columns "hoge_id" and "id" are of incompatible types: bigint and character varying.
型が違うって言われてる。belongs_to
はBIGINT
型でカラムを作ろうとするんでしょうね。
めんどくさいけど、外部キーはカスタムで定義しましょう。これでいけます。
class CreateFoos < ActiveRecord::Migration[5.1]
def change
create_table :foos do |t|
t.string :hoge_id, null: false
t.string :name, null: false
t.timestamps
end
add_foreign_key :foos, :hoges
add_index :foos, :hoge_id
end
end
モデルの定義
普通に定義すれば大丈夫ですが、idは文字列にしてあるので、新しくレコードを追加した時に初期化する必要があります。
下記のようにbefore_create
で新規作成処理をフックして、identify
メソッドに初期化処理を記述するのが簡単だと思います。id
に付与する文字列を作る処理はテキトーに変えてください。一意のKeyならなんでもいいなら下記のままでいいです。
# 親テーブル
class Hoge < ApplicationRecord
has_many :foos, dependent: :destroy
before_create :identify
private
def identify(num = 16)
self.id ||= SecureRandom.hex(num)
end
end
# 子テーブル
class Foo < ApplicationRecord
belongs_to :hoge, inverse_of: :foos
end
確認
スキーマの定義とモデルの挙動の簡単な動作確認をしてみる。それぞれ下記のようになってれば大丈夫。
スキーマの確認
pappen_devel=> \d hoges
Table "public.hoges"
Column | Type | Collation | Nullable | Default
------------+-----------------------------+-----------+----------+---------
id | character varying | | not null |
name | character varying | | not null |
created_at | timestamp without time zone | | not null |
updated_at | timestamp without time zone | | not null |
Indexes:
"hoges_pkey" PRIMARY KEY, btree (id)
pappen_devel=> \d foos
Table "public.foos"
Column | Type | Collation | Nullable | Default
------------+-----------------------------+-----------+----------+----------------------------------
id | bigint | | not null | nextval('foos_id_seq'::regclass)
hoge_id | character varying | | not null |
name | character varying | | not null |
created_at | timestamp without time zone | | not null |
updated_at | timestamp without time zone | | not null |
Indexes:
"foos_pkey" PRIMARY KEY, btree (id)
"index_foos_on_hoge_id" btree (hoge_id)
Foreign-key constraints:
"fk_rails_484f6f4b4a" FOREIGN KEY (hoge_id) REFERENCES hoges(id)
モデルの確認
pry(main)> Hoge.create(name: :hoge)
(0.3ms) BEGIN
SQL (1.6ms) INSERT INTO "hoges" ("id", "name", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id" [["id", "6bc2018b3b66025e72f1c568fc7c6618"], ["name", "hoge"], ["created_at", "2019-02-25 13:35:05.115762"], ["updated_at", "2019-02-25 13:35:05.115762"]]
(2.3ms) COMMIT
=> #<Hoge:0x00007ff7070891d8
id: "6bc2018b3b66025e72f1c568fc7c6618",
name: "hoge",
created_at: Mon, 25 Feb 2019 13:35:05 JST +09:00,
updated_at: Mon, 25 Feb 2019 13:35:05 JST +09:00>
pry(main)> Foo.create(hoge_id: "6bc2018b3b66025e72f1c568fc7c6618", name: :foo)
(0.3ms) BEGIN
Hoge Load (0.4ms) SELECT "hoges".* FROM "hoges" WHERE "hoges"."id" = $1 LIMIT $2 [["id", "6bc2018b3b66025e72f1c568fc7c6618"], ["LIMIT", 1]]
SQL (3.7ms) INSERT INTO "foos" ("hoge_id", "name", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id" [["hoge_id", "6bc2018b3b66025e72f1c568fc7c6618"], ["name", "foo"], ["created_at", "2019-02-25 13:36:05.747563"], ["updated_at", "2019-02-25 13:36:05.747563"]]
(0.4ms) COMMIT
=> #<Foo:0x00007ff705fb98d0
id: 1,
hoge_id: "6bc2018b3b66025e72f1c568fc7c6618",
name: "foo",
created_at: Mon, 25 Feb 2019 13:36:05 JST +09:00,
updated_at: Mon, 25 Feb 2019 13:36:05 JST +09:00>
pry(main)> Hoge.first.foos
Hoge Load (0.4ms) SELECT "hoges".* FROM "hoges" ORDER BY "hoges"."id" ASC LIMIT $1 [["LIMIT", 1]]
Foo Load (0.5ms) SELECT "foos".* FROM "foos" WHERE "foos"."hoge_id" = $1 [["hoge_id", "6bc2018b3b66025e72f1c568fc7c6618"]]
=> [#<Foo:0x00007ff70113e228
id: 1,
hoge_id: "6bc2018b3b66025e72f1c568fc7c6618",
name: "foo",
created_at: Mon, 25 Feb 2019 13:36:05 JST +09:00,
updated_at: Mon, 25 Feb 2019 13:36:05 JST +09:00>]
まとめ
意外と簡単にプライマリーキーを文字列として扱う事ができました。
belongs_to
でできねえとか若干の罠はありましたが。
Rails便利ですね〜。