- Ruby / Rails関連
週刊Railsウォッチ: Hotwireをアプリ構築で学ぶ、Active RecordのDurationとPostgreSQL intervalデータ型ほか(20220613前編)
こんにちは、hachi8833です。とうとう値上がりしました。
参考: Apple、Macの既存モデルを値上げ - 14インチProで35,000円アップ | マイナビニュース
つっつきボイス:「旧モデルも上がっちゃいましたね」「円安は仕方ないけど、それでもスペックの割にMacは高いな〜」「仕事で必要なら買うしかないですけどね」
「そういえばM1チップの中心人物Jeff Wilcoxさんが少し前にAppleからIntelに転職してましたね↓」
参考: 「Apple M1」開発責任者がIntelに転職 - PC Watch
「それで思い出しましたけど、ジム・ケラーというAMDのx86互換CPUのIntel独占をひっくり返した偉人は、会社を移るたびにその会社のCPUが進化する感ありますね」
🔗Rails: 先週の改修(Rails公式ニュースより)
🔗 Active Storage: トランザクション内でattach
の複数回実行が失敗するのを修正
- PR: Fix #41661 attaching multiple times within transaction by santib · Pull Request #42300 · rails/rails
- トランザクション内で
attach
を複数実行するとファイルが正しくアップロードされない問題を修正以下の例では、設定されたサービスへのアップロードは最後のファイル以外すべて失敗する。
ActiveRecord::Base.transaction do
user.attachments.attach({
content_type: "text/plain",
filename: "dummy.txt",
io: ::StringIO.new("dummy"),
})
user.attachments.attach({
content_type: "text/plain",
filename: "dummy2.txt",
io: ::StringIO.new("dummy2"),
})
end
assert_equal 2, user.attachments.count
assert user.attachments.first.service.exist?(user.attachments.first.key) # Fails
アップロード待ちのサブチェンジをトラッキングし、トランザクションがコミットされた時点でアップロードすることでこの問題に対処した。
修正: #41661
Santiago Bartesaghi, Bruno Vezoli, Juan Roig, Abhay Nikam
同Changelogより
つっつきボイス:「Active Storageのバグ修正」「1トランザクション内でattach
を複数回実行したら最後以外失敗してたとは」「ありそうな話」「pending_uploads
で変更を追いかけるようにしたようですね↓」
# #Lactivestorage/lib/active_storage/attached/changes/create_many.rb
module ActiveStorage
class Attached::Changes::CreateMany # :nodoc:
- attr_reader :name, :record, :attachables
+ attr_reader :name, :record, :attachables, :pending_uploads
- def initialize(name, record, attachables)
+ def initialize(name, record, attachables, pending_uploads: [])
@name, @record, @attachables = name, record, Array(attachables)
blobs.each(&:identify_without_saving)
@pending_uploads = Array(pending_uploads) + subchanges_without_blobs
attachments
end
def attachments
@attachments ||= subchanges.collect(&:attachment)
end
def blobs
@blobs ||= subchanges.collect(&:blob)
end
def upload
- subchanges.each(&:upload)
+ pending_uploads.each(&:upload)
end
...
+ def subchanges_without_blobs
+ subchanges.reject { |subchange| subchange.attachable.is_a?(ActiveStorage::Blob) }
+ end
🔗 MemCacheStore
とRedisCacheStore
のコネクションプーリングがデフォルトでオンになる
- キャッシュストアのコネクションプール設定で
:pool_size
オプションと:pool_timeout
オプションを非推奨化
pool: true
にするとコネクションプールがデフォルトでオンになる
config.cache_store = :redis_cache_store, pool: true
または
:pool
オプションに個別のオプションを渡す。
config.cache_store = :redis_cache_store, pool: { size: 10, timeout: 2 }
fatkodima
同Changelogより
つっつきボイス:「#45111でコネクションプールの:pool_size
オプションと:pool_timeout
オプションが非推奨化されたことに伴う改修だそうです」「connection_pool gemがデフォルトで追加されている: コネクションプールを使っていなければいいけど、既に使っている場合はコンフィグパラメータを変えないといけなくなりそうので影響がありますね」
参考: §2.2.1 コネクションプールのオプション -- Rails のキャッシュ機構 - Railsガイド
「ところで、この種のキャッシュストアはいろいろありますけど、それぞれ設定や呼び出し方が違っているうえに複雑なんですよね」「そうそう、変数がserver
だったりservers
だったり」「覚えられなくて毎回ググってます」「しかもRailsのバージョンが変わるとちょくちょく変わったりしますし、動かさないと効いているかどうかもわからないし」「動かしてもなかなかわからなかったりしますけどね」「ヘッダーの変更やキャッシュストアに投げられる命令の変更などを調べて実際に動作を確かめるようにしてます」
参考: §2キャッシュストア -- Rails のキャッシュ機構 - Railsガイド
🔗 暗号化済み属性のバリデーションのN+1クエリ問題を解消
- PR: Remove N+1 validation for encrypted attributes by alexandreruban · Pull Request #45290 · rails/rails
つっつきボイス:「バリデーションのN+1クエリ問題の修正、なるほど」「each
で回していたのをinclude?(attribute)
に変えてActive Recordにやってもらうように変えていますね」
# activerecord/lib/active_record/encryption/extended_deterministic_uniqueness_validator.rb#L10
module EncryptedUniquenessValidator
def validate_each(record, attribute, value)
super(record, attribute, value)
klass = record.class
- klass.deterministic_encrypted_attributes&.each do |attribute_name|
- encrypted_type = klass.type_for_attribute(attribute_name)
+ if klass.deterministic_encrypted_attributes&.include?(attribute)
+ encrypted_type = klass.type_for_attribute(attribute)
[ encrypted_type, *encrypted_type.previous_types ].each do |type|
encrypted_value = type.serialize(value)
ActiveRecord::Encryption.without_encryption do
super(record, attribute, encrypted_value)
end
end
end
end
end
🔗 ActiveSupport::Notifications::Fanout
を高速化
(略)
後方互換性
ActiveSupport::Notifications
は古くからよく使われているインターフェイスで、これまで多くの機能や柔軟性がここに集約されてきたが、その大半は既に誰にも使われなくなっているのではないかと思う。しかしどれが使われていないかを具体的に知るのは難しいので、削除するなら少なくとも非推奨化サイクルが必要だろう。
そのため、この改修は互換性を完全に維持することを目的とする。含まれる改修には、Fanout
に代わる別の通知の実装を使えるようにする機能と、あらゆる種類のリスナーが受け取れるシグネチャ、Instrumenter
とFanout
自身で使われるインターフェイス(たまに問題を起こすstart
やfinish
も含む)がある。
安全性
これまでも#44167や#21873などで、特にイベントをsubscribeするときとunsubscribeするときに"timestacks"が不正になる問題があった。これはトピックが動いているときにsubscribe/unsubscribeしたときのissue。
以前の実装では、スレッドローカルなスタック内でリスナーごとに個別のタイムスタンプやイベントオブジェクトを記録していた。これは、開始するリスナーと終了するリスナーは根本的に同一でなければならないということになる。
イベントのstart
に使ったリスナーをfinish
に渡し(Instrumenter
のfinish_with_state
)、start
とfinish
が同じセットになるようにすることでこのissueが回避される。
今回のコミットではこのissueをさらに回避する。個別の時刻をスタックにプッシュする代わりに、Handle
という単一のオブジェクトをイベントのスタックにプッシュするようにした。Handle
オブジェクトは(開始時刻に記録された)すべてのサブスクライバと、それらに関連付けられたすべてのデータを保持する。すなわち、start
とfinish
がインターリーブしない限りこれらも安全ということになる。Instrumenter#instrument
が使われれば、スレッドローカルの利用を完全に回避できるようになる。
今回のコミットでは、build_handle
もパブリックインターフェイスとして公開している。build_handle
は、いつどんな順序でもstart
とfinish
を呼び出せるHandle
オブジェクトを返す。
build_handle
の公開についてひとつ気になるのは、既存の"イベント化された"リスナー(start
とfinish
を受け取る)側での準備がまだ整っていない(内部でスレッドローカルなスタックが維持されているなど)可能性があることだ。
(略)
同PRより抜粋
参考: ActiveSupport::Notifications::Fanout
つっつきボイス:「ActiveSupport::Notifications
のFanout
というpub/sub周りを高速化したらしい」「後方互換性を維持しつつ、内部実装をかなり書き直している感じ」「ベンチマークはだいぶ良くなってますね」
# 同PRより: mainブランチ
timed 66.739k (± 2.5%) i/s - 338.800k in 5.079883s
timed_monotonic 138.265k (± 0.6%) i/s - 699.261k in 5.057575s
event_object 48.650k (± 0.2%) i/s - 244.250k in 5.020614s
evented 366.559k (± 1.0%) i/s - 1.851M in 5.049727s
unsubscribed 3.696M (± 0.5%) i/s - 18.497M in 5.005335s
# 同PRより: 改修のブランチ
timed 259.031k (± 0.6%) i/s - 1.302M in 5.025612s
timed_monotonic 327.439k (± 1.7%) i/s - 1.665M in 5.086815s
event_object 228.991k (± 0.3%) i/s - 1.164M in 5.083539s
evented 296.057k (± 0.3%) i/s - 1.501M in 5.070315s
unsubscribed 3.670M (± 0.3%) i/s - 18.376M in 5.007095s
「時刻ベースだった実装をイベントベースの実装に変えているように見える」「前にも登場したProcess::CLOCK_MONOTONIC
を使ってますね(ウォッチ20211108)」「これで時刻の精度が数桁はアップするはず👍」
# activesupport/lib/active_support/notifications/fanout.rb#L84
- def publish(name, *args)
- iterate_guarding_exceptions(listeners_for(name)) { |s| s.publish(name, *args) }
+ class MonotonicTimedGroup < BaseTimeGroup # :nodoc:
+ private
+ def now
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
+ end
end
参考: Process::CLOCK_MONOTONIC
(Ruby 3.1 リファレンスマニュアル)
「ところでファンインやファンアウトって電気回路の用語ですけど、こういうところにも使うんですね」「ソフトウェアでもpub/subやチャネルやバスといった概念で、電気回路のアナロジー的にファンインやファンアウトという言葉が使われることはあります」「そういえばAND回路は2入力1出力だから、ファンインが2でファンアウトが1、みたいに言いますね」
参考: ファンアウト/ファンインとは - コトバンク
参考: fan-out、fan-inパターン【Go】 - 技術向上
🔗 count_records
で新規レコードがない場合の挙動を修正
# activerecord/lib/active_record/associations/has_many_association.rb#L84
def count_records
count = if reflection.has_cached_counter?
owner.read_attribute(reflection.counter_cache_column).to_i
else
scope.count(:all)
end
- # If there's nothing in the database and @target has no new records
- # we are certain the current target is an empty array. This is a
- # documented side-effect of the method that may avoid an extra SELECT.
- loaded! if count == 0
+ # If there's nothing in the database, @target should only contain new
+ # records or be an empty array. This is a documented side-effect of
+ # the method that may avoid an extra SELECT.
+ if count == 0
+ target.select!(&:new_record?)
+ loaded!
+ end
[association_scope.limit_value, count].compact.min
end
つっつきボイス:「スコープ付き関連付けの場合に、以下の1回目のassert_equal
がパスするのに2回目がパスしていなかったのか↓」「修正量は少ないけど、突き止めるまでが大変そう」「踏むとつらいバグ」
# 同PRより
class Member < ActiveRecord::Base
has_many :memberships, -> { where(favorite: true) }
end
# 同PRより
member = Member.create!
membership = member.memberships.create!(favorite: true)
membership.update!(favorite: false)
assert_equal 0, member.memberships.size
assert_equal 0, member.memberships.size
# The first assertion passes and the second fails with:
# Expected: 0
# Actual: 1
🔗Rails
🔗 Active RecordのDuration
をクエリで使う(Ruby Weeklyより)
以下はサンプルコードです↓。
つっつきボイス:「なるほど、PostgreSQLのinterval
というデータ型を使うことで、期間をintegerではなくActiveSupport::Duration
で扱えるようになる↓」「PostgreSQLでサポートされているものに近いデータ型がちょうどRailsでもサポートされているので、結果的にこういうことができるようになった感じですね」
# 同記事より
class CreateSteps < ActiveRecord::Migration[7.0]
def change
create_table :steps do |t|
t.interval :duration
...
end
end
end
# 同記事より
step = Step.new(duration: 10.minutes)
step.duration
# => 10.minutes
参考: PostgreSQL 13 ドキュメント 8.5. 日付/時刻データ型
参考: Rails API ActiveSupport::Duration
「期間を足したり引いたり大小比較したりという操作を頻繁に使うなら、演算をDB側で行うことでパフォーマンスもよくなりそう👍」
# 同記事
class Recipe < ApplicationRecord
has_many :steps
def self.with_duration_less_than, -> (duration){
joins(:steps)
.group(:id)
.having("SUM(steps.duration) <= ?", duration.iso8601)
}
end
Recipe.with_duration_less_than(60.minutes)
# => [#<Recipe>, #<Recipe>]
「ところで、コード中のISO 8601をぐぐってみたら期間の表現方法も規格に含まれていた↓」「ほんとだ」「P3Y6M4DT12H30M5S
みたいなフォーマットは、Duration
オブジェクトとして扱う分には気にしなくていいけど、SQLログを読んでデバッグするときのことを想像すると読みづらそうかな...」
たとえば、P3Y6M4DT12H30M5Sは、「3年、6か月、4日、12時間、30分、5秒」という継続時間を表現している。
Wikipediaより
🔗 Rails 7のquery_log_tags
“Rails 7の新機能「query_log_tags」でSQLクエリに自動でコメントを追加する” https://t.co/mrwMN9OT9z
— Watson (@watson1978) June 7, 2022
つっつきボイス:「query_log_tags
を使うとクエリログに手軽にコメントを付けられる: development環境とかで便利そう👍」
参考: Rails API ActiveRecord::QueryLogs
🔗 RailsConf 2022のキーノートスピーチ(Ruby Weeklyより)
My slides from my @railsconf keynote are available on @speakerdeck now https://t.co/LfATwsebea. Thanks to everyone who attended. It was lovely to see you all in person again. ❤️ #RailsConf2022
— Eileen M. Uchitelle (@eileencodes) May 24, 2022
つっつきボイス:「タイトルはThe Success of Rails、発表者の@eileencodesさんはRailsコアチームの方です」「技術的な話よりもRailsの現在の活動や今後の展望がメインという感じのキーノートスピーチらしい発表👍」「オープンなDiscordサーバーがもうすぐ登場するらしい↓」
🔗 条件付きGETリクエストでRailsアプリを高速化(Ruby Weeklyより)
# 同記事より
def index
@user = User.find(params[:user_id])
if stale?(etag: @user.updated_at)
@recipes = @user.recipes
else
# head 304
end
end
つっつきボイス:「クックパッドの英語ブログです」「HTTPの条件付きリクエストか、似たような趣旨の記事を以前もウォッチで扱った覚えがありますね」
参考: HTTP 条件付きリクエスト - HTTP | MDN
「記事にあるETagとIf-Match
やIf-None-Match
などのヘッダーは、一般にはキャッシュを失効させるときなんかに使います」「なるほど」「HTTP条件付きリクエストと効果的に組み合わせられれば、リクエスト数がものすごく多いWebアプリのパフォーマンスを改善できるというのはわかる: MDNと合わせて読んでおくとよさそう👍」
参考: ETag - HTTP | MDN
参考: If-None-Match - HTTP | MDN
「そうそう、以前扱ったのはちょうどMDNにも載っている『楽観的ロックで更新が失われる問題を避ける』でしたね(ウォッチ20211102)」「そういえばそうでした」
🔗 フォーラムアプリを作りながらHotwireを学ぶ
Learn Hotwire by Building a Forum https://t.co/VXkQi52Aw6
— nuncapops (@nuncapops) April 9, 2022
つっつきボイス:「Hotwireを学ぶコースが無料になったと書かれている」「目次を見るとかなり充実してそう👍」
🔗 Hotwireよもやま
「何度か話しましたけど、Hotwireは割と筋がいいと思っています」「ちょうど以下のツィートを見かけました↓」「お〜、GitHubもHotwireを使っているんですね」「GitHubのHTMLソースを自分でも見てみると、たしかにdata-turbo-*
がありました」「GitHubがHotwireを使っているなら、Hotwireを導入するうえでアピールになりそう👍」
this is their source code pic.twitter.com/F8fPXPNBYf
— Adrien Poly (@adrienpoly) June 8, 2022
「そういえば、古参のRails開発会社である万葉さんも、ちょうどHotwireを推す宣言をしていましたね↓」「お〜!」
「万葉は Hotwire を推していきます!」https://t.co/Majc9gaSaN
Ruby on Rails 7 から標準となったHotwireを、万葉では全社をあげて推していきます。その背景や取り組みについてご紹介します。#万葉note #Hotwire
— 株式会社万葉 (@everyleaf) June 8, 2022
前編は以上です。
バックナンバー(2022年度第2四半期)
週刊Railsウォッチ: Shopifyのlanguage server ruby-lsp、PostgreSQL 15 Beta 1リリースほか(20220607後編)
- 20220606前編 BasecampのHotwireページネーション、Query Object、Lograge gemほか
- 20220531 Railsコミュニティアンケート結果発表、書籍『Sustainable Web Development with Ruby on Rails』ほか
- 20220524後編 Railsコアチームとコミッターに新メンバー、ruby-buildでのRust YJITサポートほか
- 20220523前編 Hotwireの用途解説記事、RubyKaigi 2022プロポーザル募集開始ほか
- 20220517後編 rubygemsに「scoped gems」の提案、RSpecのブロック構文ほか
- 20220516前編 Active Modelで属性のパターンマッチをサポート、猫でもわかるHotwire入門ほか
- 20220511後編 Ruby 3.2.0devにRust版YJITがマージ、Docker Compose V2ほか
- 20220510前編 Active RecordにPromiseと非同期集計メソッドがマージ、climate_control gemほか
- 20220419後編 RubyのGCコンパクション改修、jemalloc、ReDoSの自動検出修正ほか
- 20220418前編 RailsConf 2022が5月17〜19日開催、認可機能解説記事ほか
- 20220412後編 HashieでRubyのハッシュを強化、最近のRubyコア解説記事ほ
- 20220411前編 Turbo Railsチュートリアル、Active Recordの「Leaky Abstraction」を削減ほか
- 20220406後編 RBS関連記事、Ruby formatterプロジェクト、Google Cloud Runほか
- 20220404前編 Ruby 3.2.0 Preview 1リリース、Rails向けDocker環境ジェネレータ、scientist gemほか
今週の主なニュースソース
ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。
週刊Railsウォッチについて
TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)