結論
既存レコードのバリデーションが通る場合
User.where(token: nil).find_each(&:regenerate_token)
既存レコードのバリデーションが通らない場合
User.where(token: nil).find_each do |user|
user.token = User.generate_unique_secure_token
user.save(validate: false)
end
蛇足
背景
Ruby on Rails で動いていたプロジェクトがフロントだけ Next.js 等に移行することになって API サーバーとして動かすことになることってよくあるじゃないですか。で、安直にやるとRails に API トークンを発行させて認証っぽいことを実現したりすることってよくあるじゃないですか。
そんな時に Ruby on Rails 5 から追加された has_secure_token
という素晴らしい ActiveRecord のメソッドがあって、データベースに api_token
列を追加しておけば、モデル側はこんな感じで1行追加するだけで勝手に新規作成されたユーザーに api_token
を保存してくれます。
class User < ApplicationRecord
has_secure_token :api_token
end
has_secure_token
コードリーディング
has_secure_token
の実装は実にシンプルです。本体のコードを読みに行くことに抵抗感のある方は是非読みに行ってみてください。記事執筆時点ではたった数行のシンプルなコードです。
https://github.com/rails/rails/blob/master/activerecord/lib/active_record/secure_token.rb
def has_secure_token(attribute = :token, length: MINIMUM_TOKEN_LENGTH)
if length < MINIMUM_TOKEN_LENGTH
raise MinimumLengthError, "Token requires a minimum length of #{MINIMUM_TOKEN_LENGTH} characters."
end
# Load securerandom only when has_secure_token is used.
require "active_support/core_ext/securerandom"
define_method("regenerate_#{attribute}") { update! attribute => self.class.generate_unique_secure_token(length: length) }
before_create { send("#{attribute}=", self.class.generate_unique_secure_token(length: length)) unless send("#{attribute}?") }
end
肝はメソッド最後の2行です。軽く読んでみましょう。has_secure_token
には :api_token
を与えたものとして読んでいきます。引数を見て分かるようにデフォルトは :token
です。
regenerate_api_token
メソッドの定義
さて、この define_method
では regenerate_api_token
のようなメソッドを新たに定義しています。メソッドの実装内容は、.generate_unique_secure_token
なる SecureRandom.base58
を返すメソッドでトークンを生成して、update!
するというものです。
define_method("regenerate_#{attribute}") { update! attribute => self.class.generate_unique_secure_token(length: length) }
before_create
コールバックで値の代入
最後の行は、 before_create
コールバックで先ほどと同様に .generate_unique_secure_token
なる SecureRandom.base58
を返すメソッドでトークンを生成して、 api_token
に代入してくれています。
before_create { send("#{attribute}=", self.class.generate_unique_secure_token(length: length)) unless send("#{attribute}?") }
とてもシンプルですね。
本題
さて、ようやくここからが本題ですが、前述の通り has_secure_token
が api_token
にセットしてくれるのは before_create
コールバックなので、元々動いていたシステムで後から has_seruce_token
をコールしても元々存在していたユーザーの api_token
は空のままです。ちょっとしたスクリプトを流して全レコードの api_token
を埋めることにしましょう。ここで先ほど define_method
されていた regenerate_api_token
を使うのが良さそうです。
User.where(api_token: nil).find_each(&:regenerate_api_token)
しかし、ここでお粗末なシステムを運用していたツケが回ってきます。regenerate_api_token
は先ほど実装を読んだ通り update!
を使って api_token
を更新するので、バリデーションが通っていないレコードに対してコールすると例外を投げます。バリデーションが通らないようなレコードがシステムに存在しているのが問題なのは百も承知ですが現実は残酷なもので、今更データを綺麗にするような余裕はありません。バリデーション無効の状態でなんちゃって regenerate_api_token
を実現しましょう。ここで、先ほど軽くコードリーディングしてきた経験が活きてきます。regenerate_api_token
はトークンの生成に self.class.generate_unique_secure_token
というメソッドを使っていることが分かったので、それに倣って一旦代入して save(validate: false)
すれば ok です。
User.where(api_token: nil).find_each do |user|
user.api_token = User.generate_unique_secure_token
user.save(validate: false)
end
以上。