もふもふ技術部

IT技術系mofmofメディア

モデルのIDにUUIDを使って玄人感を出す

バージョン

Ruby 2.6.3 Rails 6.0.3 PostgreSQL 12.2

参考URL: ( Choose UUIDs for model IDs in Rails - Andy Croll)

UUIDとは何か

UUIDというのはuniversally unique identifierの略です。 128ビットの数値で16進数で表されることが多いです。

550e8400-e29b-41d4-a716-446655440000

のように表現されます。 詳細はUUID - Wikipedia を参照いただくとより理解が深まりますが、要は被らない一意の文字列であるということです。

今回はRailsを使用したアプリケーションにおけるUUIDの使用を紹介していきます。 Railsではデフォルトでidには1から始まる整数であるidがレコードの一意性を担保するカラムとして用意されており、

User.find(1)

のようにすることでidが1のユーザーを取得することができるようになっています。 今回はこの部分をUUIDにしていく方法をご紹介します。

なぜUUIDを使うのか

かっこいいからです。

Railsの標準として提供している整数値であるidを用いた場合、同じidを入れようとして何らかのエラーが起こってしまう場合があります。 また、整数値である以上人間がデータを推測することが容易になってしまい、様々なリスクが生じる可能性があります。

https://example.co.jp/users/123

例えば自分がログインした場合のユーザーページがこのようなURLになっているとします。このURLを見ることで自分が123の識別子を持つユーザーであることが分かりますね。

また、(普通は制御をかけていますが)123の部分を別の数字に置き換えることで(本来見えてはいけないはずの)他のユーザーのページを見ることができるかも知れません。

問題なのはこういったことができるかどうか、ではなくユーザーにそのような思考を抱かせる推測の容易性にあります。 UUIDを使うことでこのような問題を回避し、

https://example.co.jp/users/550e8400-e29b-41d4-a716-446655440000

このようなurlを利用者に提供することができるようになります。 こういったURLであれば推測は非常に難しくなりますし、サービスを使用するユーザーも安心して使用できるという心理的なメリットも提供することができます。

注意すべきポイント

そんなにいいことづくめであるのならUUIDをデフォルトにすれば良いのではないか?という意見もあるかと思います。 確かにメリットもありますが、例えばRailsで使用できるActiveRecordfirst,lastというスコープを使用する場合は注意しなくてはいけません。

User.first

このシンプルなコードで発行されるクエリは

SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT $1

ですが、idの値は 550e8400-e29b-41d4-a716-446655440000このようになっています。これはつまり、新規作成したUserを

User.last

で取得できるとは限らないということです。

もし既に稼働しているプロジェクトにUUIDを採用しようとしている場合はIDによるorder,first, lastのような制御を行っていないかを十分に確認するようにしてください。

アプリにUUIDを適用してみる

以上を踏まえてUUIDを適用したtableを作成していきましょう。 今回はUserモデルを作成し、そのIDをUUIDで登録できるようにしていきます。 今回はPostgreSQLのUUID拡張サポートを利用します。 また、記述量を減らすためにRails5.0以降で利用できるデフォルトのキーをuuidにする設定を行います。

拡張機能を有効化する

rails g migration enable_extension_for_uuid

を実行して、EnableExtensionForUuid用のmigrationを作成します。 中身は下記のようにしましょう。

# frozen_string_literal: true

class EnableExtensionForUuid < ActiveRecord::Migration[6.0]
  def change
    enable_extension 'pgcrypto' unless extension_enabled?('pgcrypto')
  end
end

デフォルトのキーをuuidにする設定を行う。

config/initializers/generators.rbファイルを作成します。中身を下記のようにしましょう。

# frozen_string_literal: true

Rails.application.config.generators do |g|
  g.orm :active_record, primary_key_type: :uuid
end

この 設定でmodel作成時にidがuuidであるというオプションがmigrationファイルに自動で付与されるようになります。なくても都度記載すれば動作するので、以降説明する記述を追加して実行しても構いません。

モデルの作成

Userモデルを作成しましょう。特別な操作をする必要はありません。何もないのは味気ないのでnameカラムを作っておきます。

$ rails g model user name:string

この時点で作成されるmigrationはこのようになっています。

class CreateUsers < ActiveRecord::Migration[6.0]
  def change
    create_table :users, id: :uuid do |t|
      t.string :name

      t.timestamps
    end
  end
end

注目するべきはcreate_table :usersの後にあるid: :uuidです。この記述があることで、idがuuidとして設定されます。config/initializers/generators.rbの記述によってmigrationの際に自動でuuidの設定が上記のように記載されますが、自分で都度書いていただいても大丈夫です。

migrate実行後のschemaファイルは

schema.rb

ActiveRecord::Schema.define(version: 2020_06_24_101030) do

  # These are extensions that must be enabled in order to support this database
  enable_extension "pgcrypto"
  enable_extension "plpgsql"

  create_table "users", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
    t.string "name"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
  end
end

このようになっています。ここで注目する部分は id: :uuid, default: -> { "gen_random_uuid()" }です。 この記述で一意のuuidが生成されます。

この状態でコンソールなどからUser.createしてみましょう。

#<User id: "e96939eb-7a1b-46c9-b5f6-38c79fe8a4e7", name: nil, created_at: "2020-06-24 10:26:10", updated_at: "2020-06-24 10:26:10"> 

こんな感じでidの部分がuuidとして生成されれば成功です!

おまけ:関連を付けたい場合

関連を作成したい場合です。先ほどUserモデルを作成したので、Userモデルに紐づくPostモデルを作成し、関連を付けましょう。

$ rails g model post title:string user:references

PostモデルはタイトルとUserとの関連を持ちます。 通常の整数のインクリメント方式の場合はこのままmigrateしてしまえば問題ないのですが、今回は関連に使用するのがuuidになるので少し修正が必要です。

# frozen_string_literal: true

class CreatePosts < ActiveRecord::Migration[6.0]
  def change
    create_table :posts, id: :uuid do |t|
      t.string :title
      t.references :user, null: false, foreign_key: true

      t.timestamps
    end
  end
end

migrationファイルの関連の記述部分に

type: :uuid

を記載しましょう。

# frozen_string_literal: true

class CreatePosts < ActiveRecord::Migration[6.0]
  def change
    create_table :posts, id: :uuid do |t|
      t.string :title
      t.references :user, type: :uuid, null: false, foreign_key: true

      t.timestamps
    end
  end
end

このような形式になります。 この状態でmigration を行いましょう。成功すればschemaファイルは下記のようになります。

ActiveRecord::Schema.define(version: 2020_06_24_104946) do

  # These are extensions that must be enabled in order to support this database
  enable_extension "pgcrypto"
  enable_extension "plpgsql"

  create_table "posts", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
    t.string "title"
    t.uuid "user_id", null: false
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
    t.index ["user_id"], name: "index_posts_on_user_id"
  end

  create_table "users", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
    t.string "name"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
  end

  add_foreign_key "posts", "users"
end

関連のidの部分が t.uuidになっています。PostgreSQLのuuid型の関連はこのようにする必要があるので、UUIDを使用する場合はこの部分も忘れないようにしましょう。

まとめ

Railsが提供するデフォルトのidシステムの代わりにUUIDを利用する方法をご紹介しました。 ちょっとした注意点を考慮すればほとんどコストもかからず、サービスの利用者に安全性を提供することが可能になりますので、新規プロジェクトを進める場合は採用を検討してみてはいかがでしょうか?