【Rails】stale? で同じリクエストでリソースが未更新のときは304を返す
概要
この記事では Rails の stale?
メソッドを使って、前回リクエストと同じでリソースが更新されていない場合に status code 304 Not Modified
を返す方法について解説します。
また、前提知識である以下についても解説していきます。
- status code 304ってなに?
- ETag ってなに?
- If-None-Match ってなに?
stale?
を使うことで、不要な処理を行うことなく、アプリケーションのパフォーマンスを向上させることができます。
先にまとめ
今回書いた内容をまとめると以下です。
-
status code 304
... クライアントが持っているキャッシュが最新であることを示す。 -
ETag
... 「リソースのバージョン」を一意に識別するために使用される識別子。 -
If-None-Match
... リクエストヘッダーの項目の1つで、以前に取得した ETag を送信する。 -
stale?
メソッド ... リクエストで送られてきたリソースの ETag が現在のリソースの ETag と一致するかどうかを確認するメソッド。
最終的なコード
class Api::V1::ArticlesController < ApplicationController
def index
last_updated_at = Article.maximum(:updated_at)
condition = params[:condition]
etag = "#{last_updated_at}|#{condition}"
return unless stale?(etag: etag)
articles = Article.where(title: condition)
render json: { data: articles }
end
end
status code 304 とは
status code 304 Not Modified
は、クライアントが持っているキャッシュが最新であることを示すために使用されます。
この status code によって、サーバーがリソース(HTML, JSON, 画像など)本体を送信することなく、クライアントにキャッシュの再利用を許可します。
ETag と If-None-Match について
ETag
ETag(エンティティタグ)は、「リソースのバージョン」を一意に識別するために使用される識別子です。
試しに Postman を使って、以下のコードの API にリクエストしてみます。
class Api::V1::ArticlesController < ApplicationController
def index
articles = Article.all
render json: { data: articles }
end
end
エンドポイントはこちらです。
GET /api/v1/articles
すると、レスポンスヘッダーに ETag
という項目が存在し、ハッシュ値入っている事がわかります。
このハッシュ値がリソースのバージョンを表しています。
If-None-Match
If-None-Match
はリクエストヘッダーの項目の1つです。
クライアント側はこのヘッダーを使用して、以前に取得した ETag を送信します。
サーバー側はそれを現在の ETag と比較して、リソースが変更されたかどうかを判定し、変更されていれば新しいリソースを提供し、されていなければ status code 304 Not Modified
だけを返すことで、不要な処理(前回のリソースを取得するなど)を避けることができます。
Rails で ETag のバージョンを判定する方法
さて、ここからが本題で、Railsでは、stale?
メソッドを使ってリクエストで送られてきたリソースの ETag が現在のリソースの ETag と一致するかどうかを確認できます。
例えば以下のコードの場合、リクエストヘッダーの If-None-Match
で送られてきた ETag と、現在のリソースの ETag("a"
)を比較します。
class Api::V1::ArticlesController < ApplicationController
def index
puts "1"
return unless stale?(etag: "a")
puts "2"
articles = Article.all
render json: { data: articles }
end
end
もしも違った場合(If-None-Match
で送られてきた ETag が古い場合)、
return unless stale?
("a") 以降の処理が行われるため、2
が出力されます。
では、Postman を使って、今度は If-None-Match に ETag を指定してリクエストしてみます。
ログを確認すると、現在のリソースと違うため、2
が出力され、それ以降の処理も行われています。
Started GET "/api/v1/articles" for 172.18.0.1 at 2024-05-04 04:22:21 +0000
Processing by Api::V1::ArticlesController#index as */*
1
2
Article Load (3.1ms) SELECT `articles`.* FROM `articles`
↳ app/controllers/api/v1/articles_controller.rb:10:in `index'
Completed 200 OK in 96ms (Views: 73.2ms | ActiveRecord: 5.2ms | Allocations: 4370)
さらに再度レスポンスヘッダーを確認すると、ETag に新しいハッシュ値が入っています。
これが現在のリソースのハッシュ値になります。
次にこのハッシュ値を If-None-Match
に入れてリクエストしてみます。
すると、 リクエストされた ETag が最新と判定され、2
が出力されず、それ以降の処理が行われずに、304 の status code が返ることが分かります。
Started GET "/api/v1/articles" for 172.18.0.1 at 2024-05-04 04:31:25 +0000
Processing by Api::V1::ArticlesController#index as */*
1
Completed 304 Not Modified in 3ms (ActiveRecord: 0.0ms | Allocations: 168)
前回リクエストと、リソースの更新状況から判定する
上記を踏まえて、以下のコードを見てみます。
class Api::V1::ArticlesController < ApplicationController
def index
last_updated_at = Article.maximum(:updated_at)
condition = params[:condition]
etag = "#{last_updated_at}|#{condition}"
return unless stale?(etag: etag)
articles = Article.where(title: condition)
render json: { data: articles }
end
end
こちらのコードでは ETag を、「全 Article の最終更新日時と、クエリパラメータの condition
」 から生成しています。
これで、Article が更新されるか、クエリパラメータの condition が変更されるかしない限りは、サーバは 304を返すことになり、2回目以降のリクエストでは無駄な処理を回避することができます。
具体的には、Article が更新されずに、以下のリクエストのときはずっと304が返ります。
/api/v1/articles?condition=condition1
ちなみに stale?
メソッドには last_modified
を引数に取ることができ、
リソースの更新日時を直接指定することもできます。
class Api::V1::ArticlesController < ApplicationController
def index
last_updated_at = Article.maximum(:updated_at)
condition = params[:condition]
return unless stale?(etag: condition, last_modified: last_updated_at)
articles = Article.all
render json: { data: articles }
end
end
Discussion