Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

has_secure_token を後から追加した時に token を埋める

結論

既存レコードのバリデーションが通る場合

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_tokenapi_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

以上。

OgiharaRyo
ソフトウェアエンジニア / 個人事業主 / Ruby on Rails / dvorak配列 / Ergodox EZ / リモートワーカー / ヘビーゲーマー
https://ogihara-ryo.github.io
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away