16
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Railsでプライマリーキーを文字列で扱う

Last updated at Posted at 2019-02-25

この記事の目的

Railsのマイグレーションでは各テーブルのプライマリーキーにデフォルトでidが追加される仕様になってる。

何も指定しなくてもidBIGINT型で付与されて自動でインクリメントされるのは非常に便利だけど、アプリケーションの構成によっては文字列で登録したい時がある。

例えばQiitaでも各記事は8f8f1d1e5333da561fd8みたいな文字の羅列がIDとして付与されてる(たぶん)。こんな感じでidを文字列(VARCHAR型)として扱う場合の手順を説明します。

尚、今回使用してるDBはPostgreSQL、RailsのバージョンはRails5です。

結論

下記の手順で実現できます。modelで付与するidのロジックはお好みで。

  • create_tableメソッドのオプションにid: :stringを指定する。
  • 子テーブルの外部キーはadd_foreign_keyadd_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_toBIGINT型でカラムを作ろうとするんでしょうね。
めんどくさいけど、外部キーはカスタムで定義しましょう。これでいけます。

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便利ですね〜。

16
9
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
16
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?