activerecord

Databases on Rails.

Create a Topic

Topics

Active Recordのgenerates_token_for:DBカラム不要で一時的なトークンを扱う機能

118 views Post
wakairo @wakairo

generates_token_forとは

generates_token_forは、特定の目的を持つトークンを生成し、そのトークンからレコードを検索・検証するための機能です。 Rails 7.1で標準機能として導入されました。

特長

  • DBへのデータ保存不要:トークンを保存するためのデータベースのカラムやテーブルを追加せずに利用できます。
  • 目的別に分離:目的(purpose)ごとにトークンを分離できるため、別目的で生成されたトークンを使い回すことができません。
  • 失効条件を設定可能:有効期限とモデルのデータ変化による失効条件を柔軟に設定できます。
  • 改ざん耐性:署名付きのトークンのため、改ざん耐性があります。

使いどころ:一時的に有効なURLの送付

以下のような「一時的で目的が限定されたトークン付きURL」を送付する場面に適しています。

  • メールアドレス確認リンク
  • 招待リンク
  • マジックリンク(パスワードなしログイン)
  • 一時アクセスリンク

なお、APIの認証トークンのような永続的な用途には向きません。 Railsで永続的なトークンを扱いたい場合は、has_secure_tokenなどの利用を検討してください。

注意:パスワードリセットには専用APIあり

Rails 8.0以降でhas_secure_passwordを利用している場合、パスワードリセット用トークンを扱う専用APIが自動的に提供されます。そのため、パスワードリセットの用途に限っては、自分でgenerates_token_forを定義する必要はありません。(詳しくは後述の補足を参照してください)

使い方(メールアドレス確認の例)

ここでは「メールアドレス変更時の確認リンク」を題材に、基本的な使い方を説明します。

基本の流れ(定義・生成・検証)

まず、モデルにgenerates_token_forを用いて、トークンの用途名(ここでは:email_verification)を定義します。 なお、用途名が異なっていれば複数定義することも可能です。

class User < ApplicationRecord
  generates_token_for :email_verification
end

トークンを生成する際は、対象のインスタンスに対してgenerate_token_forを呼び出します。

注意:モデルで定義する際は複数形のgenerates_token_forですが、 インスタンスから呼び出す際は単数形のgenerate_token_forになる点に注意してください。

user = User.first # 対象のレコードを取得(ここでは`first`で代替)
token = user.generate_token_for(:email_verification)
# => "BAhJIi..." (生成されたトークン文字列)

生成したトークンは、例えば以下のようにURLパラメータに載せてメールなどで送付するのが典型的な流れです。

# 例:メイラー等でのURL生成
email_verifications_url(token: token)
# => "https://example.com/email_verifications?token=BAhJIi..."

受け取ったトークンからレコードを検索・検証するには、クラスメソッドのfind_by_token_forを使います。 有効なトークンならUserインスタンスが返り、無効なトークンや期限切れのトークンの場合はnilが返ります。

user = User.find_by_token_for(:email_verification, token)

有効期限による失効

定義時にexpires_inオプションを付与することで、トークンに有効期限(例:15分間)を設けることができます。

class User < ApplicationRecord
  generates_token_for :email_verification, expires_in: 15.minutes
end

モデルのデータ変化による失効

generates_token_forは、「一度状態が変わったら、古いトークンを使わせない」挙動を簡単に実装できます。 例えば、新しいメールアドレス確認用に生成されたトークンを、メールアドレスの変更が完了した後に無効化すれば、万が一流出しても悪用を防げます。

無効化の仕組みは「定義時のブロックの戻り値をトークンに埋め込み、検証時にその値が変わっていれば無効とみなす」というものです。

class User < ApplicationRecord
  generates_token_for :email_verification, expires_in: 15.minutes do
    # トークン生成時と検証時で`email`の値が異なれば(=メールアドレスが変更完了していれば)、
    # 有効期限内であっても無効になる
    email
  end
end
注意:ブロックの戻り値に機密情報を含めない

ブロック内で返した値は、改ざん耐性はあるものの、暗号化されるわけではなく、デコードすれば読める形でトークンに埋め込まれます。 そのため、ブロックの戻り値には機密情報を含めてはなりません。

補足:パスワードリセット専用API

Rails 8.0以降では、モデルでhas_secure_passwordを利用している場合、パスワードリセットトークン用の以下のメソッドがデフォルトで追加されます。

  • password_reset_token:トークン生成(インスタンスメソッド)
  • find_by_password_reset_token:トークンからの検索と検証(クラスメソッド)
  • find_by_password_reset_token!:同上(無効時に例外を発生させるバージョン)

Rails 8.1で追加されたトークン有効期限の機能

さらにRails 8.1以降では、トークンの有効期限を柔軟に扱うための機能が追加されています。

has_secure_passwordの呼び出し時にreset_tokenオプションを通して有効期限を変更できるようになりました。

class User < ApplicationRecord
  has_secure_password reset_token: { expires_in: 1.hour }
end

また、以下のメソッドが追加されました。

  • password_reset_token_expires_in:設定されている有効期限の取得(インスタンスメソッド)

参考情報

0
Raw
https://www.techtips.page/en/comments/1117

Active Recordのallが持つ条件を引き継ぐ性質と動的クエリ構築への応用

233 views Post
wakairo @wakairo

RailsのActive Recordのallメソッドは、 その名前から「テーブルに存在する全レコードを取得するメソッド」と理解されがちです。 しかし実際には「その時点までに積み上げられた条件に該当するものを全て取得する」という実務上とても重要な性質を持っています。

本記事では、このallの性質の基本と、動的にデータベースクエリを組み立てる際の実践的な応用テクニックについて解説します。

allは「構築済みの条件」を引き継ぐ

データベースにPenモデルのレコードが3件保存されているとします。 以下のようにPenクラスに対して直接allを呼び出すと、テーブルに存在する全てのレコードが取得されます。

test(dev):000> Pen.all
  Pen Load (1.2ms)  SELECT "pens".* FROM "pens" /* loading for pp */ LIMIT 11 
=>
[#<Pen:0x00007cdf641ba6f0
  id: 1,
  color: "red",
  price: 80,
  created_at: "2026-02-20 05:52:53.460900000 +0000",
  updated_at: "2026-02-20 05:52:53.460900000 +0000">,
 #<Pen:0x00007cdf66b23208
  id: 2,
  color: "blue",
  price: 80,
  created_at: "2026-02-20 05:53:03.457025000 +0000",
  updated_at: "2026-02-20 05:53:03.457025000 +0000">,
 #<Pen:0x00007cdf66b230c8
  id: 3,
  color: "red",
  price: 150,
  created_at: "2026-02-20 05:53:10.752331000 +0000",
  updated_at: "2026-02-20 05:53:10.752331000 +0000">]

では、別の条件に続けてallを呼び出すとどうなるでしょうか。 以下の実行例を見ると分かるように、allは「全て」ではなく、「それまでの条件を満たすもの全て」を返します。 ここではcolor: "red"を満たすレコードだけが取得されています。

test(dev):000> Pen.where(color: "red").all
  Pen Load (0.1ms)  SELECT "pens".* FROM "pens" WHERE "pens"."color" = 'red' /* loading for pp */ LIMIT 11 
=>
[#<Pen:0x00007cdf66b20648
  id: 1,
  color: "red",
  price: 80,
  created_at: "2026-02-20 05:52:53.460900000 +0000",
  updated_at: "2026-02-20 05:52:53.460900000 +0000">,
 #<Pen:0x00007cdf66b20508
  id: 3,
  color: "red",
  price: 150,
  created_at: "2026-02-20 05:53:10.752331000 +0000",
  updated_at: "2026-02-20 05:53:10.752331000 +0000">]

このように、allは単に全件取得する機能だけでなく、 手前に条件がある場合には「そこまでに構築されたクエリ条件(Relationオブジェクト)を引き継ぎ、そのまま返す」という機能を担う側面があります。

応用例:動的クエリ構築においてallを起点にしてメソッドチェーン可能にする

この「そこまでに構築された条件を引き継ぐ」というallの性質は、 「URLのクエリパラメータが存在する場合だけ条件を追加する」といった動的なデータベースクエリ構築において役立ちます。

例えば、クエリパラメータを処理するfilter_byというスコープを定義することを考えてみましょう。 このスコープは、以下のように「1年以内に作成されたレコード」という前提条件(where)のあとにメソッドチェーンで呼び出される想定です。

test(dev):000> Pen.where(created_at: 1.year.ago..).filter_by(color: "red", max_price: 100)
  Pen Load (0.4ms)  SELECT "pens".* FROM "pens" WHERE "pens"."created_at" >= '2025-02-21 05:50:22.982591' AND "pens"."color" = 'red' AND "pens"."price" <= 100 /* loading for pp */ LIMIT 11 
=>
[#<Pen:0x000072d2c03393d8
  id: 1,
  color: "red",
  price: 80,
  created_at: "2026-02-20 05:52:53.460900000 +0000",
  updated_at: "2026-02-20 05:52:53.460900000 +0000">]

この挙動を実現するfilter_byスコープの実装は以下のようになります。 最初にallを呼び出して現在のRelation(既に構築済みのwhere条件など)を変数pensに格納し、 そのpensに対してパラメータの有無をチェックしながら条件を継ぎ足しています。

class Pen < ApplicationRecord
  scope :filter_by, ->(params) do
    pens = all
    pens = pens.where(color: params[:color]) if params[:color].present?
    pens = pens.where(price: ..params[:max_price]) if params[:max_price].present?
    pens
  end
end

ここで重要になるのが、filter_byの中で最初にallを呼んで変数に入れている点です。
allは「何も条件がない状態」ではなく、 「このスコープが呼ばれた時点のRelationをそのまま受け取る」役割を果たしています。 そのため、Pen.where(created_at: …)のように手前で条件を付けていても、 それをリセットすることなくさらに条件を積み上げられます。 つまり、allを変数に入れることで、 手前の条件を維持した上でさらに条件を積み上げていくための「起点」を作ることができるのです。 なお、クエリパラメータが全て空で条件追加がない場合でも、allが返した元のRelationがpensに入っているため、 メソッドチェーンをそのまま継続できます。

(参考)個別スコープへの切り出しとそのタイミング

先ほどの例ではfilter_byの内部で変数に再代入しながら条件を追加していましたが、 もしcolorpriceによる絞り込みをアプリケーション内の他の場所で単独で利用したい場合は、 それぞれを個別のスコープとして切り出すことでコードをよりキレイに書くことができます。

Active Recordのスコープは、ブロックの評価結果がnilまたはfalseになった場合、 自動的に元のRelationをそのまま返す(条件を追加せずにスルーする)という便利な仕様になっています。 そのため、以下のようにメソッドチェーンでそのままつなぐだけで、条件分岐をスコープの中に自然にカプセル化できます。

class Pen < ApplicationRecord
  scope :with_color, ->(color) { where(color: color) if color.present? }
  scope :with_max_price, ->(max_price) { where(price: ..max_price) if max_price.present? }
  scope :filter_by, ->(params) do
    with_color(params[:color]).with_max_price(params[:max_price])
  end
end

なお、YAGNI原則の観点からは、 これらの個別スコープ(with_colorwith_max_price)が他の場所で「本当に必要になるまで」は、 無理に切り出さない方が良いでしょう。 つまり、最初から再利用性を過剰に意識してスコープを量産するよりは、 まずは前述のpens = allを使うアプローチで1箇所にまとめておき、 個別スコープが本当に必要になったタイミングで切り出す方が、 結果的には開発コストを減らせるはずです。

0
Raw
https://www.techtips.page/en/comments/1114

Active Recordマイグレーション:t.referencesとadd_referenceで外部キー制約を詳細設定する方法

289 views Post
wakairo @wakairo

要点

Active Recordのマイグレーションファイルにてt.referencesまたはadd_referenceを使う際、 foreign_key:オプションに対してtrueの代わりに ハッシュを渡すことで外部キー制約の詳細を設定 できます。

このハッシュで指定できるオプションはadd_foreign_keyのAPIドキュメントに記載されているオプションです。

具体例

t.references を使う場合(新規テーブル作成時)

create_table :books do |t|
  t.references :author, foreign_key: { on_delete: :cascade }
end

add_reference を使う場合(既存テーブルへの追加)

add_reference :books, :author, foreign_key: { on_delete: :cascade }

説明

Active Recordのマイグレーションファイルにてテーブル間に関連付けをする際には、 t.referencesまたはadd_referenceを使うと便利です。

この2つのメソッドは、呼び出すときにforeign_key:オプションを付けると、 外部キー制約も同時に付与することが出来ます。例えば、foreign_key: trueという記述をよく見かけます。

foreign_key: trueは外部キー制約をかけることだけの指定となりますが、 ここでのtrueの代わりにハッシュを渡すことで、外部キー制約の詳細が設定可能になります。

このハッシュの中に入れられるオプション、つまり外部キー制約の詳細設定項目は、add_foreign_keyに渡せるオプションと同じです。 このadd_foreign_keyに渡せるオプションの具体的な種類とそれぞれの意味については、 add_foreign_keyのAPIドキュメントをご覧ください。

0
Raw
https://www.techtips.page/en/comments/1108

Active Recordマイグレーションでのadd_foreign_keyとadd_referenceの違い

346 views Post
wakairo @wakairo

Railsガイドを一読しただけでは、 add_foreign_keyとadd_referenceが、それぞれどのようなもので、どう違うのかがいまいちよく分かりませんでした。 そこで、add_foreign_keyとadd_referenceによってdb/schema.rbがどのように変化するかを基に、 それぞれの機能について確認してみました。

add_foreign_key

add_foreign_keyは外部キー制約の追加だけを行います。よって、カラムやインデックスの追加は行いません。

外部キー制約とは、子テーブルが参照している親のIDが親テーブルに存在することをデータベースレベルで保証する制約です。

例えば、以下の記述をマイグレーションファイルに行ったとします。

add_foreign_key :products, :users

すると以下の記述がdb/schema.rbに追加されます。

add_foreign_key "products", "users"

この記述は、products(子テーブル)のuser_idカラムに登場するIDが、users(親テーブル)のidカラムに必ず存在するように制約をかけています。

add_reference

add_referenceは、テーブル間の関連付けに関する複数の設定を一度に行える便利メソッドです。

add_referenceの基本機能は、カラムとインデックスの追加です。 例えば、以下の記述をマイグレーションファイルに行ったとします。

add_reference :products, :user

すると以下のように、users(親テーブル)に関連付ける2つの記述、具体的にはuser_idのカラムとインデックスをproducts(子テーブル)に追加する2つの記述が、db/schema.rbに追加されます。

t.integer "user_id"
t.index ["user_id"], name: "index_products_on_user_id"

またadd_referenceは、foreign_key:trueオプションを追加することで、前述の2つに加えて外部キー制約も同時に設定できます。 例えば、以下の記述をマイグレーションファイルに行ったとします。

add_reference :products, :user, foreign_key:true

すると以下のように、前述の2つの記述に加えて、add_foreign_keyの記述がdb/schema.rbに追加されます。

t.integer "user_id"
t.index ["user_id"], name: "index_products_on_user_id"
add_foreign_key "products", "users"

(参考)add_referenceとt.referencesの機能は基本的に同じ

add_referenceとcreate_tableのブロックの中で呼び出すt.referencesは基本的に同じ機能を提供します(オプション以外の引数の部分で違いはありますが。) 実際にt.referencesへ渡せるオプションはadd_referenceと同じです。 また、t.referencesの実装add_referenceの実装のどちらもReferenceDefinition.newを内部で呼ぶ形になっています(v8.1.2で確認)。

0
Raw
https://www.techtips.page/en/comments/1107

ActiveRecord::Rollbackで例外を伝播させずにロールバック後の処理を継続する

399 views Post
wakairo @wakairo

Active RecordのTransactionブロック内で例外が投げられた場合、ActiveRecord::Rollback以外の例外はロールバックの後に再度投げられます。

したがって、処理失敗で例外を投げるメソッド(save!等)を使えば、基本的にその例外はロールバックの後Transactionブロックの外へ伝播します。 これをキャッチしなければ、RailsはHTTPのエラーレスポンスをブラウザ等のクライアントに返します。 つまり、「処理に失敗したら、ロールバックして、あとの処理は切り上げて、エラーレスポンスを返す」という挙動は簡単に実装できます。

一方で、「ロールバック後に例外を出さずに処理を継続する」場合には、ActiveRecord::Rollbackを活用できます。 以下は、Transactionの成否でリダイレクト先を変えるコード例です。

completed = false

ActiveRecord::Base.transaction do
  unless obj1.save && obj2.save
    raise ActiveRecord::Rollback
  end
  completed = true
end

if completed
  redirect_to completed_path
else
  redirect_to uncompleted_path
end
0
Raw
https://www.techtips.page/en/comments/1106

activerecordでは、firstを使った方が実装とSQLが揃って可読性が上がる

2177 views Post
takuma_tech Takuma @takuma_tech

こちらの記事によると、activerecordでlastを使った場合、指定したorderを逆にして"LIMIT 1"とするSQLが発行されるそうです。

一方で、firstを使った場合、指定したorderはそのままで"LIMIT 1"とするSQLとなるので、railsのコードとSQLの対応関係が分かりやすくなります。

したがって、lastよりもfirstを使った方が実装とSQLが揃って可読性が上がるということがこの記事で主張されていました。 ご参考まで。

0
Raw
https://www.techtips.page/en/comments/304
🔧1
💡1
❤️1

unscopeはscope系以外の条件も外す

3078 views Post
wakairo @wakairo
Last edited

RailsのActive Recordには、scopedefault_scopeという機能があり、SQLクエリの条件を指定してあらかじめ付けておくことが可能です。

unscopeunscopedは、これらのscope系で付けた条件を外すことが出来ます。
ただ、注意点として、scope系以外で付けた条件も外してしまいます

以下に例を示します。

irb(main):001:0> puts User.where(id: 1).all.to_sql
SELECT "users".* FROM "users" WHERE "users"."id" = 1
=> nil
irb(main):002:0> puts User.where(id: 1).unscope(:where).all.to_sql
SELECT "users".* FROM "users"
=> nil
irb(main):003:0> puts User.where(id: 1).unscoped.all.to_sql
SELECT "users".* FROM "users"
=> nil

unscopeunscopedが、直前のwhere句で指定した条件を外していることが確認できます。

確認した環境

  • Rails 7.0.4

参考

0
Raw
https://www.techtips.page/en/comments/23