base64エンコード形式の文字列データをページに埋め込む

概要

S3のプライベートバケットに保管されている画像を表示したい。
S3からオブジェクトを取得し、それをbase64エンコードしたものをData URLを使用してブラウザに埋め込み、画像を表示することを行なったので それをメモとして残します。

Data URL とは

Data URL は data: スキームが先頭についたURLで、データをインラインで文書に埋め込むことができます。
スキーム(data:)、MIMEタイプ、base64トークン、データ自体の4つの部品で構成されます。

data:[<mediatype>][;base64],<data>

データが文字の場合は、そのまま指定することができます。
文字以外であれば、base64 を指定し、 base64エンコードしたバイナリーデータを指定します。
Data URIスキームに対応したブラウザが、base64エンコードされたデータをデコードし、それを展開してくれるそう。

例としてPNGの画像をbase64エンコードしたData URIは以下のようになります。

image_tag "data:image/png;base64, #{image_data}"

image_dataの部分は、base64エンコードされたデータそのものが入ります。

base64エンコードとは

base64は、データを64種類の印字可能な英数字のみを用いて、マルチバイト文字やバイナリデータを扱うためのエンコード方式です。
具体的には、A、…、Z、a、…、z、0、…、9、+、/ でデータが変換されています。

base64エンコード方式は、ASCIIテキストしか扱えないメディア上で保存や送信を行う際に、データを変換するために使用されてます。
base64が定められた経緯として、かつて電子メールを送信する際にSMTPプロトコルではASCIIで表現される英数字しか送信することができなかったが、画像などのテキスト以外のデータを扱うためにASCIIへの変換方法が定められました。

Data URL で画像を表示することについての注意点

  • ブラウザによってURLの文字数に制限が設けられています。
    データ URL - URI | MDN
  • base64エンコードして埋め込むと、当然ですが imgタグでsrcに画像パスを指定しするより、ソースコード自体も長くなります。そのため埋め込む画像の分だけページサイズも大きくなります。データ転送量が増加するため、画像を埋め込んだファイルのダウンロードに時間がかかる可能性があります。

実際のコード

s3 = Aws::S3::Resource.new(client: s3_client)
object = s3.bucket(Settings.s3.bucket_name).object(prefix: "uploads/image")
image_data = Base64.encode64(object.get.body.read)

object.get.body.readの部分について、 getメソッドでオブジェクトを取得し、bodyメソッドでオブジェクトの中身を参照します。以下のようにバイト型データを扱うストリームとなっています。

pry > object.get.body.class
=> StringIO

文字列として扱うためにはストリームから read して文字列型に変換する必要があります。

pry > object.get.body.read.class
=> String

ビュー側での記述です。

image_tag "data:image/jpeg;base64, #{image_data}"

ちなみに、object.get.body.read で文字列にした後そのままdataURLに埋め込むことができるのかと思い、 base64エンコードせずにビュー側に返すと Invalid byte sequence in UTF-8とエラーになってしまいました。
おそらく、dataURLが解釈できない文字列が含まれているため、別途追加処理(エスケープ?)が必要かもしれないと思います。

参考にしたURL

データ URL - URI | MDN
Base64 - Wikipedia
Base64についてまとめてみた - iimon TECH BLOG

浮動小数点の演算について

背景

商品価格に対し割引率をかけて最終価格を出す際に計算結果にずれが生じていたため、それについて調べた内容をまとめます。
たとえば以下のような、数値リテラル(Floatクラス)で計算した場合、計算結果が微妙にずれてしまっています。

pry(main)> 0.3 - 0.2  
=> 0.09999999999999998

いわゆる丸め誤差というもの。

プログラミングでの演算

PCは10進数ではなく、2進数で計算を行います。
たとえば、10進数で0.2は2進数で表すと0.00110011...のような循環小数になります。
演算を行う際、循環小数は桁の途中で切り捨てられることになります。
そのため、小数点の演算では上記のような誤差が発生することがあります。

こちらはIEEE(米国電子電気技術者協会)により規格化された、IEEE754という浮動小数点数形式に準拠していることによる挙動とのこと。
さまざまなプログラミング言語がIEEE 754の規格に準拠しているため、
Rubyだけでなく他の言語においても上記の影響を受けることに注意。

Rubyでどうするか

そのため正確な小数点の計算をするには、ほかの数値クラスを使う必要があります。

Rationalクラス

有理数を扱うクラスです。
小数点を2進数に表す際に無限小数となりそれを一定の桁数で丸る処理を行うため、誤差が発生してしまっていました。
整数/整数 という形で整数の計算をした後で小数に戻すことをしてあげます。(整数は2進数で表すことができるため)

pry(main)> 0.3r - 0.2r
=> (1/10)
pry(main)> (0.3r - 0.2r).to_f
=> 0.1
pry(main)> Rational("0.3") - Rational("0.2")
=> (1/10)
pry(main)> (Rational("0.3") - Rational("0.2")).to_f.to_s
=> "0.1"

BigDecimalクラス

可変長浮動小数点計算クラスです。
Rationalに比べて計算速度が顕著に低下しますが、こちらも正確な小数の計算が可能です。

pry(main)> BigDecimal("0.3") - BigDecimal("0.2")
=> 0.1e0
# to_sを使うと10進数の文字列に変換してくれる
pry(main)> (BigDecimal("0.3") - BigDecimal("0.2")).to_s("F")
=> "0.1"

参考URL

研鑽Rubyプログラミング ― 実践的なコードのための原則とトレードオフ – 技術書出版と販売のラムダノート
IEEE 754 - Wikipedia
class Float (Ruby 3.3 リファレンスマニュアル)
浮動小数点って何? #Ruby - Qiita

ActiveRecordで関連モデルをロードする

背景

あるモデルのキャッシュを作成したく、その際に関連モデルも同時にキャッシュを作成しておきたい。 関連モデルを読み込むために preload、includes、eager_load、joins のどのメソッドを使用したら良いか迷ったためまとめとして自分用にかく。

実行環境

ruby 2.3.4
Rails 5.1.7

各メソッドの挙動を確認

railsコンソール上でのクエリの実行確認方法として、to_sqlメソッドはすべてのクエリが出力されないため、explainメソッドを使う。

preload

関連データを別のクエリで取得してキャッシュする。

pry(main)> User.preload(:tasks).explain; nil 
  User Load (45.8ms)  SELECT `users`.* FROM `users`
  Task Load (115.9ms)  SELECT `tasks`.* FROM `tasks` WHERE `tasks`.`user_id` IN (1, 2, 3)
=> nil

なお、usersテーブルのレコードが大量にある場合、IN句が大きくなるため注意が必要。

特定の条件に合致するデータだけを取得してキャッシュしたい場合、 上記のとおり常に2つのクエリが発行されるためwhere条件でpostsを絞ることはできない。

pry(main)> User.preload(:tasks).where(tasks: {id: 1}).explain; nil
  User Load (2.4ms)  SELECT `users`.* FROM `users` WHERE `tasks`.`id` = 1
ActiveRecord::StatementInvalid: Mysql2::Error: Unknown column 'tasks.id' in 'where clause': SELECT `users`.* FROM `users` WHERE `tasks`.`id` = 1
from /root/.rbenv/versions/2.3.4/lib/ruby/gems/2.3.0/gems/mysql2-0.5.5/lib/mysql2/client.rb:151:in `_query'

Userデータなら絞ることができる。

pry(main)> User.preload(:tasks).where(users: {id: 1}).explain; nil    
  User Load (1.4ms)  SELECT `users`.* FROM `users` WHERE `users`.`id` = 1
  Task Load (1.1ms)  SELECT `tasks`.* FROM `tasks` WHERE `tasks`.`user_id` = 1
=> nil

includes

includesは、preloadと同様に関連データを別のクエリでロードする。

pry(main)> User.includes(:tasks).explain; nil                               
  User Load (29.5ms)  SELECT `users`.* FROM `users`
  Task Load (100.3ms)  SELECT `tasks`.* FROM `tasks` WHERE `tasks`.`user_id` IN (1, 2, 3)
=> nil

先ほどのpreloadでは関連モデルをwhereで絞ることができなかった。 includesでは、以下のとおりLEFT OUTER JOINで指定された条件も適用され、結果を取得することができる。

pry(main)> User.includes(:tasks).where(tasks: {id: 1}).explain; nil
  SQL (5.7ms)  SELECT `users`.`id` AS t0_r0, `users`.`created_at` AS t0_r1, `users`.`updated_at` AS t0_r2, `tasks`.`id` AS t1_r0, `tasks`.`user_id` AS t1_r1, `tasks`.`provider` AS t1_r2, `tasks`.`uid` AS t1_r3, `tasks`.`created_at` AS t1_r4, `tasks`.`updated_at` AS t1_r5 FROM `users` LEFT OUTER JOIN `tasks` ON `tasks`.`user_id` = `users`.`id` WHERE `tasks`.`id` = 1
=> nil

その際にincludesは2 つのクエリから1 つのクエリに変更されている。
なおusersの条件絞り込みでは2つのクエリが生成されている。

pry(main)> User.includes(:tasks).where(users: {id: 1}).explain; nil  
  User Load (1.2ms)  SELECT `users`.* FROM `users` WHERE `users`.`id` = 1
  Task Load (0.8ms)  SELECT `tasks`.* FROM `tasks` WHERE `tasks`.`user_id` = 1
=> nil

referencesを使用することでwhere句で条件絞りこみをしなくても、LEFT OUTER JOINすることができる。

pry(main)> User.includes(:tasks).references(:tasks).explain; nil
  SQL (192.1ms)  SELECT `users`.`id` AS t0_r0, `users`.`created_at` AS t0_r1, `users`.`updated_at` AS t0_r2, `tasks`.`id` AS t1_r0, `tasks`.`user_id` AS t1_r1, `tasks`.`provider` AS t1_r2, `tasks`.`uid` AS t1_r3, `tasks`.`created_at` AS t1_r4, `tasks`.`updated_at` AS t1_r5 FROM `users` LEFT OUTER JOIN `tasks` ON `tasks`.`user_id` = `users`.`id`
=> nil

eager_load

eager_loadは、LEFT OUTER JOINを使用して単一のクエリですべての関連モデルをロードする。

pry(main)> User.eager_load(:tasks).explain; nil 
  SQL (200.1ms)  SELECT `users`.`id` AS t0_r0, `users`.`created_at` AS t0_r1, `users`.`updated_at` AS t0_r2, `tasks`.`id` AS t1_r0, `tasks`.`user_id` AS t1_r1, `tasks`.`provider` AS t1_r2, `tasks`.`uid` AS t1_r3, `tasks`.`created_at` AS t1_r4, `tasks`.`updated_at` AS t1_r5 FROM `users` LEFT OUTER JOIN `tasks` ON `tasks`.`user_id` = `users`.`id`
=> nil

これは、includesがwhere句またはorder句でtasksテーブルのデータを扱う際に単一のクエリになるのと、まったく同じ。

pry(main)> User.eager_load(:tasks).where(tasks: {id: 1}).explain; nil
  SQL (4.3ms)  SELECT `users`.`id` AS t0_r0, `users`.`created_at` AS t0_r1, `users`.`updated_at` AS t0_r2, `tasks`.`id` AS t1_r0, `tasks`.`user_id` AS t1_r1, `tasks`.`provider` AS t1_r2, `tasks`.`uid` AS t1_r3, `tasks`.`created_at` AS t1_r4, `tasks`.`updated_at` AS t1_r5 FROM `users` LEFT OUTER JOIN `tasks` ON `tasks`.`user_id` = `users`.`id` WHERE `tasks`.`id` = 1
=> nil

joins

joinsは、内部結合(INNER JOIN)を使用して関連モデルのデータを取得する。ただしtasksテーブルのデータはSELECTされていない。

pry(main)> User.joins(:tasks).explain; nil  
  User Load (75.9ms)  SELECT `users`.* FROM `users` INNER JOIN `tasks` ON `tasks`.`user_id` = `users`.`id`
=> nil

tasksのデータを扱いたい場合は、selectメソッドを使う必要がある。

pry(main)> User.joins(:tasks).select('users.*, tasks.id').explain; nil
  User Load (72.0ms)  SELECT users.*, tasks.id FROM `users` INNER JOIN `tasks` ON `tasks`.`user_id` = `users`.`id`
=> nil

参考にした記事

Preload, Eagerload, Includes and Joins - BigBinary Blog

RSpec で ActiveModel::Errors のモックを作成する

ActiveRecord::RecordInvalid が発生するかどうかをテストするために ActiveModel::Errors のモックを作りたい。

実行環境

ruby 2.6.6
Rails 6.0.3.6

ActiveRecord::RecordInvalid が発生するかどうかのテスト

subjectが実行された際 ActiveRecord::RecordInvalid が発生するかどうかのテストコードを実装します。
なお、subjectは省略していますが、subjectで定義しているものには、user.save! の処理が含まれています。

# Userのインスタンスに対してsave!メソッド実行時にActiveRecord::RecordInvalidがraiseされるようにスタブ化
before { allow_any_instance_of(User).to receive(:save!).and_raise(ActiveRecord::RecordInvalid) }


it { expect { subject }.to raise_error(ActiveRecord::RecordInvalid) }

Model にerrors や class を定義するスタブを作成

つぎに user.save! に失敗した場合、以下のようにエラーメッセージをログに出力するコードが書かれているとします。

Rails.logger.error e.message

その場合に、subjectを実行し ActiveRecord::RecordInvalid が発生した時に、エラーログが出力されるかどうかのテストコードを実装します。

まずテストの前処理としてスタブの定義を行います。

let(:errored_model) do
  instance_double(
    User,
    errors: instance_double('errors', full_messages: ['nameを入力してください'], messages: { name: ['を入力してください'] }),
    class: class_double(User, i18n_scope: :activerecord),
  )
end
before do
  # Userのインスタンスに対してsave!メソッドが実行された時にActiveRecord::RecordInvalidがraiseされるようにスタブ化
  allow_any_instance_of(User).to receive(:save!).and_raise(ActiveRecord::RecordInvalid, errored_model)
  # Rails.loggerのオブジェクトに対してerrorメソッドを呼び出せるようにする
  allow(Rails.logger).to receive(:error)
end

and_raise について

rspec-mocks/lib/rspec/mocks/message_expectation.rb at main · rspec/rspec-mocks · GitHub
こちらにあるように and_raise 内で、raise メソッドが呼び出されているため、raise ActiveRecord::RecordInvalid, userとするように、
and_raise の引数に ActiveRecord::RecordInvalid, user を渡してあげるとよさそうです。

instance_double の errors や class について

User の InstanceDouble で errors を指定していないと以下のようなエラーが発生します。

RSpec::Mocks::MockExpectationError: #<InstanceDouble(User) (anonymous)> received unexpected message :errors with (no args)
from /app/.bundle/ruby/2.6.0/gems/rspec-support-3.10.2/lib/rspec/support.rb:102:in `block in <module:Support>'

ほかにも i18n_scope メソッドが定義されていないと以下のようなエラーが発生します。

NoMethodError: undefined method `i18n_scope' for RSpec::Mocks::InstanceVerifyingDouble:Class
from /app/.bundle/ruby/2.6.0/gems/activerecord-6.0.3.6/lib/active_record/validations.rb:22:in `initialize'

内部的に ActiveRecord において i18n_scope メソッドを呼び出しているようでした。
rails/activerecord/lib/active_record/translation.rb at main · rails/rails · GitHub

最終的なテストコード

let(:errored_model) do
  instance_double(
    User,
    errors: instance_double('errors', full_messages: ['nameを入力してください'], messages: { name: ['を入力してください'] }),
    class: class_double(User, i18n_scope: :activerecord),
  )
end
before do
  allow_any_instance_of(User).to receive(:save!).and_raise(ActiveRecord::RecordInvalid, errored_model)
  allow(Rails.logger).to receive(:error)
end

# 実際にRails.logger.errorが呼び出され、エラーメッセージが引数として渡されることをモックで確認する
it 'エラーログが出力されること' do
  subject
  expect(Rails.logger).to have_received(:error).with("バリデーションに失敗しました: nameを入力してください")
end

モック/スタブ実装のためのメソッド

上記で使用したメソッドを整理します。

そもそも RSpec におけるスタブとモックの違い

スタブは、他のメソッドが実行されないようにして欲しい値を返させるものです。

つまり、テストで確認したい内容のために何か都合のいい値を返すように設定したものをスタブと言います。

モックは、指定のメソッドが実行されたかどうかを、引数や頻度を指定してチェックするものです。

instance_double

特定のクラスに対して double(身代わり)を作成します。
指定したクラスで定義されているインスタンスメソッドのみが、スタブとして許可されます。
ほとんどdoubleと同様だが、allowなどで定義するメソッドが、引数で指定したクラス(今回の User)のインスタンスメソッドとして存在していないとテストが失敗します。

class_double

実際のクラスの情報と紐付いて実際のクラスに実装されていないメソッドのスタブ化はできないようになります。
ほとんどinstance_doubleと同様だが、こちらはクラスメソッドに対して適用されます。

allow_any_instance_of

生成されたインスタンスすべてに対して、スタブを設定できます。

参考にした記事

techracho.bpsinc.jp

zenn.dev

コンテナ間通信でデータベースを共有する

異なるリポジトリのDBコンテナにデータを投入する方法をメモします。
例えば、サービス(言語)ごとにリポジトリを分けていて、DBを共有したいケースに活用できるかと思います。

前提として、AリポジトリをメインとしてDBコンテナを起動します。こちらにBリポジトリで構築した、appコンテナのデータを保存できるようにします。
やることは、コンテナのネットワークを利用して、コンテナ間通信を行います。

手順

まず、Aリポジトリの docker-compose.yml の内容を以下のようにします。(insert-mysqlは例です。)

  db:
   (他の項目については省略)
    networks:
      - default
      - insert-mysql

networks:
  insert-mysql:
    driver: bridge

上記の設定変更を反映します。

$ docker-compose up -d
Creating network "hogehoge_insert-mysql" with driver "bridge"

ここでネットワーク一覧に新しいネットワーク(hogehoge_insert-mysql)が表示されていることを確認します。

$ docker network list
NETWORK ID     NAME                       DRIVER    SCOPE
abcdef123456   hogehoge_default            bridge    local
123456abcdef   hogehoge_insert-mysql       bridge    local
aabbccddeeff   bridge                      bridge    local

また、現時点では上記の新しいネットワークを使用しているコンテナが、AリポジトリのDBコンテナしかないことを確認しておきます。
"Containers"の項目の部分です。

$ docker network inspect hogehoge_insert-mysql

次に、Bリポジトリの docker-compose.yml の編集を行います。

  app:
    (他の項目については省略)
    networks:
      - default
      - hogehoge_insert-mysql
networks:
  hogehoge_insert-mysql:
    external: true

先ほどと同様に設定の変更を反映します。

$ docker-compose up -d

再度hogehoge_insert-mysqlのネットワークを使用しているコンテナの確認をします。
"Containers"の項目に、Bリポジトリのappコンテナが追加されていることが確認できると思います。

$ docker network inspect hogehoge_insert-mysql

これでリポジトリに跨ってDBの共有ができるはずです。

もしコンテナ起動に失敗する場合

既存のコンテナが影響している可能性があるので、一旦全部のネットワークを削除(docker network rm)とdocker-compose downをすると良いかと思います。

参考

はじめdocker network create hoge_networkで手動で新しくネットワークを作成しようとしたのですが、docker-compose.yml に設定することにより、自動作成したかったので、上記の方法にしました。
https://tech-blog.rakus.co.jp/entry/20181211/docker/postgresql/go

Rails のコネクションプールについて( Puma の Worker 数も変えてみた)

Railsのコネクションプール経由で行っているDB接続について、少し調べたのでメモを残します。

実行環境

Rails 6.1.3.1

Connection Pool とは

コネクションプールとは、Railsの処理がデータベースにアクセスするたびにコネクション接続と切断を行って負荷が高くなったり、パフォーマンスが低下するのを防ぐために、予め決められた上限数を考慮してデータベースとの間に作っておく接続のグループのことです。

tech-book.precena.co.jp

データベースに接続した状態を、メモリ上にあらかじめ確保(キャッシュ)しておき、データベースにアクセスするタイミングで、Poolから利用可能な接続を再利用します。
毎回接続して切断するより、DB接続というコストの高い処理をあらかじめ行うことで、DB接続にかかる時間を短縮でき、効率よく処理を行うことができるというメリットがあるとのこと。

Connection pool の設定について

Connection pool は、database.yml で設定します。

default: &default
  adapter: mysql2
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>

pool はアプリがデータベースに対して保持できる接続の最大数です。
上記でデフォルトの 5 を設定しています。

また、ActiveRecord はスレッドごとに個別のデータベース接続を使用します。
そのため、puma.rb において、スレッドはデフォルトのまま 5 として pool と同じ値になるようにしています。(スレッド数 = pool 数)

max_threads_count = ENV.fetch('RAILS_MAX_THREADS', 5)
min_threads_count = ENV.fetch('RAILS_MIN_THREADS') { max_threads_count }

さて、接続を使用した後、自動的に解放されますが、使用可能な数を超える接続を使用しようとすると、Active Record によってブロックされ、残りのリクエストはプールの接続の待ち状態になってしまいます。

動作確認

接続待ちの状態になることを実際に見てみたいと思います。
Promise.all() を用いて 10 リクエストを並列実行されるようにしてみます。

Promise.all(
    [...new Array(10)].map((_, i) => {
        api.get(`http://localhost:3000/home/${index + 1}`)
    })
)

Rails側では、リクエストを受け付け、DB処理を行います。
わかりやすいように、処理時間を5秒ほど伸ばします。

class HomeController < ApplicationController
    def index
        @user = User.find params[:id]
        sleep 5
    end
end

先ほど並列実行数(pool)として 5 を設定しているので、10 リクエストのうち残りの 5 つのリクエストが待機されるはずです。

同時接続数 5 の 10 リクエスト実行結果

画像から最後の 5 リクエストは遅れて返ってきていることがわかります。

次に、pool 数を変更してみます。変更されれば単純に実行時間にも影響が出てきます。
pool 数を 3 にしてみます。

default: &default
  adapter: mysql2
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 3 } %>

同時接続数 3 の 10 リクエスト実行結果
10リクエストの中で 3 ずつ並列処理されていることがわかります。

なお、処理時間が長いなどで接続を取得できない場合は、アプリケーションからの接続がタイムアウトになり、例外が発生する可能性があります。

could not obtain a connection from the pool within 5.000 seconds (waited 5.001 seconds); all pooled connections were in use excluded from capture: No host specified, no public_key specified, no project_id specified
Completed 500 Internal Server Error in 5017ms (Views: 5.4ms | ActiveRecord: 0.0ms | Allocations: 158211)

Puma の Worker 数を変更する

Worker 数 ≒ (子)プロセス数と捉え、スレッド数 = pool 数 は 3 でそのままとし、その状態で Worker 数を変更してみます。
puma.rb でコメントアウトされていた以下の記述をアンコメントし有効化します。

workers ENV.fetch("WEB_CONCURRENCY") { 2 }

すると 同時並列数は 3(スレッド数) × 2(worker数) = 6 に増えていることがわかります!

同時接続数 3 の 10 リクエスト実行結果( 2 worker)

参考URL

railsguides.jp

devcenter.heroku.com

qiita.com

RailsとMySQLとでタイムゾーンが異なる場合の検索の仕方

RailsとMySQLなどのDBとでタイムゾーンが異なる場合に、データ抽出や検索の際にミスが起こりそうなので自戒の意味を込めてメモします。
なおRailsコンソールで検索する方法について記載しています。

実行環境

ruby 2.6.6
Rails 6.0.3.6

まずはそれぞれにおけるタイムゾーンを確認します。

mysql でのタイムゾーン確認

UTCとなっています。

mysql> show variables like "%time_zone";
+------------------+--------+
| Variable_name    | Value  |
+------------------+--------+
| system_time_zone | UTC    |
| time_zone        | SYSTEM |
+------------------+--------+
2 rows in set (0.00 sec)

mysql> select NOW();
+---------------------+
| NOW()               |
+---------------------+
| 2023-12-17 14:52:01 |
+---------------------+
1 row in set (0.00 sec)

Rails でのタイムゾーン確認

日本時間で検索しています。

pry(main)> Time.zone
=> #<ActiveSupport::TimeZone:0x000055555c991608 @name="Tokyo", @tzinfo=#<TZInfo::DataTimezone: Asia/Tokyo>, @utc_offset=nil>

Rubyコンテナサーバーのタイムゾーン

UTCとなっています。

pry(main)> Time.now
=> "2023-12-17T15:00:03.228+00:00"

Railsコンソールでの検索方法

いくつか方法があるのでそれぞれの違いを見ます。
なお現在時刻は日本時間で2023-12-18 00:00であるとします。

Time(RailsはTimeWithZone)クラスを使用する

Time.zone.now や TIme.zone.now.ago など TimeWithZone クラスのメソッドを使って検索します。

pry(main)> User.where("updated_at > ?", Time.zone.now).to_sql
=> "SELECT `users`.* FROM `users` WHERE (updated_at > '2023-12-17 15:00:47.921823')"

日本時間がUTC(9時間前)に変換されてデータベース検索を行なっていることがわかります。

日時の文字列で指定する

直接日時の文字列を指定して検索します。

pry(main)> User.where("updated_at > ?", '2023-12-18 00:00').to_sql
=> "SELECT `users`.* FROM `users` WHERE (updated_at > '2023-12-18 00:00')"

指定した時間からUTCには変換されていないことがわかります。
なので、UTCに変換してから指定しないと、日本時間に変換されると勘違いしていたら、期待した検索結果が得られないので注意が必要です。

Integerに変換する

Timeクラス(RailsならTimeWithZoneクラス)のオブジェクトを to_i でIntegerに変換したものを指定します。

# Time.zone.now → integer
pry(main)> User.where("updated_at > ?", Time.zone.now.to_i).to_sql
=> "SELECT `users`.* FROM `users` WHERE (updated_at > 1702825200)"

# Time.zone.parse → integer
pry(main)> User.where("updated_at > ?", Time.zone.parse("2023-12-18 00:00:00").to_i).to_sql
=> "SELECT `users`.* FROM `users` WHERE (updated_at > 1702825200)"

# 日本時間 2023-12-18 00:00 です
pry(main)> Time.zone.at(1702825200)
=> Mon, 18 Dec 2023 00:00:00 JST +09:00

Integerに変換しUnixTImeとして扱うことができるようになります。

pry(main)> User.where("updated_at > ?", Time.parse("2023-12-17 15:00:00").to_i).to_sql
=> "SELECT `users`.* FROM `users` WHERE (updated_at > 1702825200)"

上記のように、Time.parse でUTC時間のままTimeオブジェクトを to_i した時と同じ1702825200で検索されています。 そのため、タイムゾーンを気にすることなく検索することができます。

参考ページ

docs.ruby-lang.org