Rails
MySQL
ActiveRecord
UUID

Railsでuuidを取り扱う時のTips

Railsでidを uuid を取り扱う時に苦労したので、Tipsとして残しておきます。
そもそもuuidってなんなの!?というのは、適度にググってください。

開発環境

  • Ruby: 2.3.1
  • Rails: 4.2.6
  • MySQL: 5.7.16

Rails4系でuuidを使いたい場合は activeuuid というGemを入れると便利です。

gem 'activeuuid'

Rails5からは、データベースマイグレーションの生成時にuuidをデフォルトの主キーに設定できる機能が追加されたので、そちらを利用してください。
https://railsguides.jp/5_0_release_notes.html

問題点

上記の方法でuuidを使っていると、実際のDB内にはバイナリ形式で保存されており、データベースからデータを取得したい場合にちょっと面倒な部分があります。

モデル検索

こんなモデルがあったとします。

app/models/book.rb
# == Schema Information
#
# Table name: books
#
#  id              :uuid(16)         not null, primary key
#  created_at      :datetime
#  updated_at      :datetime
#

class Book < ApplicationRecord
end

find

何も意識しなくても、発行されたSQLを見ると、ActiveRecordがいい感じにしてくれる事に気がつきます。

Book.find('d84e1955-b5c5-4b0f-b75f-1ab2db7359e2')
# Book Load (1.5ms)  SELECT  `books`.* FROM `books` WHERE `books`.`id` = x'd84e1955b5c54b0fb75f1ab2db7359e2' LIMIT 1
=> #<Book:0x007ff372ed5b30>

where

find で上手く行ったからといって、whereではそうは行きません。

Book.where('id = ?', 'd84e1955-b5c5-4b0f-b75f-1ab2db7359e2')
# Book Load (1.6ms)  SELECT `books`.* FROM `books` WHERE (id = 'd84e1955-b5c5-4b0f-b75f-1ab2db7359e2')
=> []

これは、こうする必要があります。

Book.where('id = x?', 'd84e1955b5c54b0fb75f1ab2db7359e2')
# Book Load (0.5ms)  SELECT `books`.* FROM `books` WHERE (id = x'd84e1955b5c54b0fb75f1ab2db7359e2')
=> [#<Book:0x007ff36d15d480>]

MySQLでは x をつけることで16進数として検索をかける事ができます。

ユースケースドリブン

さて、uuidを文字列として扱っていると、- が入った形式で取り扱う事になります。しかしながら、上記でもあるように、whereで検索したい場合 (primaryなのにそんなことあるのかというのは置いといて) 、エンドユーザーに対して、ハイフンは消してから検索してくださいというのはちょっと不便かも知れません。
その場合、次のように検索します。

uuid = 'd84e1955-b5c5-4b0f-b75f-1ab2db7359e2'
condition = "
  CONCAT(
    SUBSTR(HEX(books.id), 1, 8),
    '-',
    SUBSTR(HEX(books.id), 9, 4),
    '-',
    SUBSTR(HEX(books.id), 13, 4),
    '-',
    SUBSTR(HEX(books.id), 17, 4),
    '-',
    SUBSTR(HEX(books.id), 21)
  ) = ?
"
Book.where(condition, uuid)
=> [#<Book:0x007ff36ecc32b0>]

なぜ、こんな面倒な事が起きたかというと、サービス運用をしているとカスタマーサポートでユーザー情報を取り扱う事が多々あり、お問い合わせメールにそのユーザーの識別子を付与したいというケースがあります。識別子というと、primary keyであるuuidをそのまま渡せば楽ですが、安全性の観点からMD5でハッシュ化した文字列を付与するケースがあります。
その場合、MD5でハッシュ化されてしまった文字列を使って、ユーザー情報を引っ張ってくるのはどうしたものかと四苦八苦しました。
その解決策が以下の通りです。

uuid = 'd84e1955-b5c5-4b0f-b75f-1ab2db7359e2'
hashed_uuid == Digest::MD5.hexdigest(uuid)
condition = "
  MD5(
    CONCAT(
      SUBSTR(HEX(books.id), 1, 8),
      '-',
      SUBSTR(HEX(books.id), 9, 4),
      '-',
      SUBSTR(HEX(books.id), 13, 4),
      '-',
      SUBSTR(HEX(books.id), 17, 4),
      '-',
      SUBSTR(HEX(books.id), 21)
    )
  ) = ?
"
Book.where(condition, hashed_uuid)
=> [#<Book:0x007ff36e218068>]

MySQLでは、MD5 という関数があるので、これを利用します。
これで、データベース内に存在するバイナリ形式のuuidを16進数に直して、ハイフンがある形に連結し、それをMD5で検索をかけるという事が実現できます。

他にもいい方法ありましたら、是非教えてください!