Rails8お試し(Authentication GeneratorとSolidXのみ)
この辺りの新機能や改修を一通り試してみたい。せっかくRedis等のミドルウェアが不要になっているのでDockerも使わずホスト環境でそのまま触ってみる。
インストール
とりあえずインストールしてスタートページを表示させる。
code:sh
mkdir rails-sandbox
cd rails8-sandbox
brew upgrade ruby-build
rbenv install 3.3.6
rbenv local 3.3.6
rbenv rehash
code:sh
gem install rails -v 8.0.0
rails _8.0.0_ new .
code:sh
bin/dev
認証機能を作成する
今回から追加されたRails提供の基本の認証機能を実装してみる。まずはジェネレータを実行し生成されるファイルを確認してみる。
code:sh
$ bin/rails generate authentication
$ git status
On branch main
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: Gemfile
modified: Gemfile.lock
modified: app/controllers/application_controller.rb
modified: config/routes.rb
Untracked files:
(use "git add <file>..." to include in what will be committed)
app/channels/
app/controllers/concerns/authentication.rb
app/controllers/passwords_controller.rb
app/controllers/sessions_controller.rb
app/mailers/passwords_mailer.rb
app/models/current.rb
app/models/session.rb
app/models/user.rb
app/views/passwords/
app/views/passwords_mailer/
app/views/sessions/
db/migrate/
test/fixtures/users.yml
test/mailers/previews/
test/models/user_test.rb
code:rb
class Current < ActiveSupport::CurrentAttributes
attribute :session
delegate :user, to: :session, allow_nil: true
end
app/controllers/concerns/authentication.rbに具体的な認証処理が書かれている。
code:rb
module Authentication
extend ActiveSupport::Concern
included do
before_action :require_authentication
helper_method :authenticated?
end
class_methods do
def allow_unauthenticated_access(**options)
skip_before_action :require_authentication, **options
end
end
private
def authenticated?
resume_session
end
def require_authentication
resume_session || request_authentication
end
def resume_session
Current.session ||= find_session_by_cookie
end
def find_session_by_cookie
end
def request_authentication
redirect_to new_session_path
end
def after_authentication_url
session.delete(:return_to_after_authenticating) || root_url
end
def start_new_session_for(user)
user.sessions.create!(user_agent: request.user_agent, ip_address: request.remote_ip).tap do |session|
Current.session = session
cookies.signed.permanent:session_id = { value: session.id, httponly: true, same_site: :lax } end
end
def terminate_session
Current.session.destroy
cookies.delete(:session_id)
end
end
なんでメソッドごとに2行改行と1行改行があるのか謎だけど処理としてはシンプル。cookieからsession idを取り出して有効なら先ほどのCurrent.sessionに保持させて使い回す感じ。もしsession idがない場合はサインインページへリダイレクトされるだけ。あとはpasswords_controller.rbとsessions_controller.rb、それに対応するviewやroutes.rbも勝手に作成されている。従来のようにDevise入れてゴニョゴニョやるよりデフォルトが提供してる分スッキリしてる印象で良い。
余談だけどpasswords_controller.rbにUser.find_by_password_reset_token!という呼び出しがあり、このメソッドはどこで定義されてるのか調べたらhas_secure_passwordのクラスでdefine_methodされていた。#{attribute}_reset_tokenという感じで動的生成されており、つまりデフォルトはpasswordだけどhoge_reset_tokenという名前にもできるということっぽい。 code:sh
$ bin/rails db:migrate
https://scrapbox.io/files/672f6248e0b11443e93a49d7.png
で、じゃあ新規ユーザー作成するか〜と思ってリンク探したらなんとない!新規作成はサポートしてないらしい...。ここまでやってくれるなら新規作成までやってよくない?と思ったが仕方ないのでbin/rails consoleでUse.create(email_address:, password:)してHomeのcontrollerとviewを作りrootの設定をした。そんであとはさっきのログインページからログインすればhomeにリダイレクトされることを確認した(ちなみにcannot load such file -- bcryptのエラーが出たんだけどserverを再起動するだけで直るので焦るな。)
デフォルトだとAuthenticationのconcern内でrequire_authenticationが実行されており、ApplicationControllerでそれがincludeされているのでこのApplicationControllerを継承したページは全部ログイン必須になってた。やはり認証機能の最低限だけは適当に提供しとくけど、あとはご自由にどうぞという割り切りで作られてるな。
SolidXを試す
SolidQueue
SolidCache自体はRailsのデフォルトで入っているので追加のgemなどはいらない。設定ファイルがconfig/cache.ymlにあるのでそこにキャッシュのname_spaceやmax_sizeを設定していく模様。詳しいOptionはここにリストされている。基本はActiveJobのバックエンドとしてシームレスに利用できるようになっているから利用者としてはあまり意識しなくて良い(はず)。今までセッション管理のためだけにElastiCacheを使ってたりした部分がRDSに統合できるようなって嬉しい〜というのがおすすめポイントらしい。何かと依存するミドルウェアが多いと環境構築とか面倒だからありがたい改善ではある。 とはいえ設定がデフォルトでされているのは本番環境用の話であり、それ以外の開発環境などでは別途自分で設定が必要だった。まずconfig/development.ymlに下記を追加。
code:yml
config.active_job.queue_adapter = :solid_queue
config.solid_queue.connects_to = { database: { writing: :queue } }
そんでconfig/database.ymlのdevelopmentを下記に変更。SolidQueue用に別DBを作成する必要があるのか...。パフォーマンス考えたらそりゃそう。
code:yml
development:
primary:
<<: *default
database: storage/development.sqlite3
queue:
<<: *default
database: storage/development_queue.sqlite3
migrations_paths: db/queue_migrate
そしてbin/rails db:drop && bin/rails db:prepareを実行する。これでstorage/にdevelopment.sqlite3とdevelopment_queue.sqlite3が作成される。あとはbin/jobを実行するとSolidQueueが起動する。
ひとまず適当に↓のようなJobを作成してRailsコンソールを立ち上げてSampleJob.perform_laterを実行。
code:rb
class SampleJob < ApplicationJob
queue_as :default
def perform(*args)
Rails.logger.info "SampleJob is running"
sleep 5
Rails.logger.info "SampleJob is done"
end
end
するとlogs/development.logに出力されるはず。実行できとる。
QueueのDBの方がどうなってるのかも気になるのでActiveJobのダッシュボードを提供してくれるmission-control-jobsを入れてroute.rbにmount MissionControl::Jobs::Engine, at: "/jobs"を設定。すると/jobsというパスが生えるのでアクセスするとこんな感じになって実行結果がモニターできたりする(sidekiq/webみたいなUIの自動更新がないのでやや不便。issueは出来てた。)。 https://scrapbox.io/files/673032bed021c24e38e9f72c.png
データ用のDBとQueue用のDBは分けることを推奨されているが一つのDBで全て実行してもいいらしい。でもデータ用とは別にDBが必要なら、普通クラウドのRDB費用はクラウド全体の費用に占める結構な割合になりがちだし、それじゃあElastiCacheみたいなものを使うのと費用面ではあんま変わんないよな〜と思ったりした。 SolidCache
SolidCacheはRailsのキャッシュ機構のバックエンドを今までのmemcachedやRedisからRDBを使うようにできる機能。SolidQueueと同様にconfig/cache.ymlという設定ファイルがあったり、config/database.ymlにキャッシュ用のデータベースを作成する設定ができたりと同じインターフェースになっている。 今回は一番わかりやすそうなフラグメントキャッシュ(viewの部分キャッシュ)で試す。とりあえず開発環境だと有効になってないので下記のコマンドで有効にする。
code:sh
rails dev:cache
次にdevelopment.ymlでcache storeをSolidCacheに変更。
code:yml
# config.cache_store = :memory_store <- コメントアウト
config.cache_store = :solid_cache_store
ほんでdatabase.ymlにqueueの時と同じくcacheの設定を記述する。
code:yml
development:
...
cache:
<<: *default
database: storage/development_cache.sqlite3
migrations_paths: db/cache_migrate
キャッシュの更新についても確認しやすいようにexpireする時間を短くしておく。cache.ymlに下記を追加。失効時間を30秒に設定。
code:yml
development:
database: cache
store_options:
expiry_batch_size: 1
max_age: <%= 30.seconds.to_i %>
フラグメントキャッシュが動いてるのを確認するために下記をview/home/index.htmlに追加
code:erb
<% cache("home_index_time") do %>
<p>
<%= Time.now %>
</p>
<% end %>
これで何度かアクセスすると時間がキャッシュされた時間から表示が更新されなくなる。サーバーログにはこんな感じでsolid_cache_entriesテーブルからデータが取得されていることがわかった。
code:txt
SolidCache::Entry Load (0.8ms) SELECT "solid_cache_entries"."key", "solid_cache_entries"."value" FROM "solid_cache_entries" WHERE "solid_cache_entries"."key_hash" IN (-398009230309464708) /*action='index',application='Rails8Sandbox',controller='home'*/
↳ app/views/home/index.html.erb:3
Read fragment views/home/index:61461968e53d2da16b5bba2b509e5932/home_index_time (1.8ms)
次にどうやってキャッシュが削除されるのかを確認する。これは少し独特なので注意が必要。というのも次回書き込み時にキャッシュに失効期限のものがあれば削除されるというアルゴリズムになっている。なので例えばcache.ymlにmax_ageというキャッシュの有効期限を設定していても、次に書き込みがある新規のキャッシュ追加更新処理がなければそのキャッシュは失効していたとしても更新されない。先ほどのviewの場合、例えば下のように新規のキャッシュ処理を追加した時に初めて表示されるTime.nowの時間が更新される仕組み。
code:erb
<% cache("home_index_time") do %>
<p>
<%= Time.now %>
</p>
<% end %>
<% cache("home_index_time2") do %>
<p>削除用のキャッシュテスト</p>
<% end %>
これがrenderingされるときに初めて下記のログが表示された。
code:txt
SolidCache::Entry Upsert (0.4ms) INSERT INTO "solid_cache_entries" ("key","value","key_hash","byte_size","created_at") VALUES (x'646576656c6f706d656e743a76696577732f686f6d652f696e6465783a30666163373539643361373034356238643734363231313065616430346164612f686f6d655f696e6465785f74696d65', x'001102000000000000f0bf0a0000000408492200063a06454620203c703e0a20202020323032342d31312d31302031363a35393a3337202b303930300a20203c2f703e0a', 2662525179818617242, 285, STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')) ON CONFLICT ("key_hash") DO UPDATE SET "key"=excluded."key","value"=excluded."value","byte_size"=excluded."byte_size" RETURNING "id" /*action='index',application='Rails8Sandbox',controller='home'*/
↳ app/views/home/index.html.erb:3
Write fragment views/home/index:0fac759d3a7045b8d7462110ead04ada/home_index_time (1.2ms)
次の書き込み時とか関係なく失効時間になったら自動で更新されて欲しい場合はどうすんだ...。
ちなみにデフォルトだと書き込み処理の際に失効対象のキャッシュを探し、もしあればそのスレッド中で削除処理が行われる。だから例えば削除対象のキャッシュが多い場合はそのプロセスだけ重くなってしまうはず。というわけで削除処理自体をバックグラウンドジョブに逃がすオプションがある。expiry_method: :jobとcache.ymlで設定しておけば有効になる。その他詳しいアルゴリズムについてはREADME/cache-expiryに書いてるので確認しておくとよさそう。 SolidCable
SolidCacheはActionCableのバックエンドをRDBにするための機能。他のSolidX同様にRailsの依存からRedisを剥がすモチベーションで使われる模様。 まずはconfig/database.ymlとconfig/cable.ymlに設定を追加する。
code:yml
development:
...
cable:
<<: *default
database: storage/development_cable.sqlite3
migrations_paths: db/cable_migrate
code:yml
development:
adapter: solid_cable
connects_to:
database:
writing: cable
polling_interval: 0.1.seconds
message_retention: 1.day
そしてbin/rails db:drop && bin/rails db:prepareでdbの作成。
code:sh
bin/rails generate channel Chat
https://scrapbox.io/files/673087e13ee1fd8b4d132211.png
あとはメッセージを送ってログを確認すると↓のようにSolidCacheが使われているのがわかるはず。
code:txt
broadcast
ChatChannel#speak({"message"=>"hi!"})
ActionCable Broadcasting to chat_channel: {:message=>"hi!", :sent_at=>"19:12"} SolidCable::Message Insert (0.3ms) INSERT INTO "solid_cable_messages" ("created_at","channel","payload","channel_hash") VALUES ('2024-11-10 10:12:07.253561', x'636861745f6368616e6e656c', x'7b226d657373616765223a22686921222c2273656e745f6174223a2231393a3132227d', 5736154075277867913) ON CONFLICT DO NOTHING RETURNING "id" /*application='Rails8Sandbox'*/
↳ app/channels/chat_channel.rb:12:in `speak'