- Ruby / Rails関連
週刊Railsウォッチ: SorbetでRailsアプリの型シグネチャを書く、activerecord-cte gemとanycable-client gem(20210803前編)
こんにちは、hachi8833です。RubyKaigi Takeout 2021のチケット販売が開始されました。
RubyKaigi Takeout 2021 tickets are available now!
Speakers and Ruby committers are invited to the event. Please check your mailbox if you are a speaker or/and a Ruby committer. 🍔 https://t.co/BJ3aTFj0iE
— Akira Matsuda (@a_matsuda) July 29, 2021
🔗Rails: 先週の改修(Rails公式ニュースより)
今回は以下の公式更新情報から見繕いました。
- 更新情報: Performance and parallel testing improvements and more! | Riding Rails
- 更新情報: Active record improvements and much more | Riding Rails(残りは次回に)
🔗 Active SupportのNilClass#try
とNilClass#try!
がRuby 2.7以降で遅かった問題を修正
つっつきボイス:「以前#34068でRuby 2.5向けにNilClass#try
とNilClass#try!
を高速化したけど、Ruby 2.7以降だと逆に遅くなることがわかったので元に戻したんですね」
# activesupport/lib/active_support/core_ext/object/try.rb#L5
module ActiveSupport
module Tryable #:nodoc:
- def try(method_name = nil, *args, &block)
- if method_name.nil? && block_given?
+ def try(*args, &block)
+ if args.empty? && block_given?
if block.arity == 0
instance_eval(&block)
else
yield self
end
- elsif respond_to?(method_name)
- public_send(method_name, *args, &block)
+ elsif respond_to?(args.first)
+ public_send(*args, &block)
end
end
ruby2_keywords(:try)
- def try!(method_name = nil, *args, &block)
- if method_name.nil? && block_given?
+ def try!(*args, &block)
+ if args.empty? && block_given?
if block.arity == 0
instance_eval(&block)
else
yield self
end
else
- public_send(method_name, *args, &block)
+ public_send(*args, &block)
end
end
ruby2_keywords(:try!)
end
end
...
class NilClass
...
- def try(_method_name = nil, *)
+ def try(*)
nil
end
- def try!(_method_name = nil, *)
+ def try!(*)
nil
end
end
The approach in that PR is significantly faster in Ruby 2.5, but slower in Ruby 2.6, 2.7, and 3.0 from my testing https://t.co/RJZUSlTX86
— Jeremy Evans (@jeremyevans0) July 12, 2021
🔗 ハッシュ構文でorder
したときのeager_loading?
を修正
- PR: Fix `eager_loading?` when ordering with `Hash` syntax by intrip · Pull Request #42782 · rails/rails
ハッシュ構文で
order
したときのeager_loading?
を修正。外側テーブルをハッシュ構文で
order
したときにeager_loading?
が正しく動くようになった。
Post.includes(:comments).order({ "comments.label": :ASC }).eager_loading?
#=> true
Jacopo Beschi
同PRより
つっつきボイス:「eager_loading?
はeager loadingされているかどうかをtrue/falseで返すだけのメソッドなのに、これでエラーになったらびっくりする」「よくぞ見つけた感」「修正を見ると以前はString
しか想定されていなかったんですね↓」
# activerecord/lib/active_record/relation/query_methods.rb#L1550
def column_references(order_args)
- references = order_args.grep(String)
+ references = order_args.flat_map do |arg|
+ case arg
+ when String
+ arg
+ when Hash
+ arg.keys
+ end
+ end
references.map! { |arg| arg =~ /^\W?(\w+)\W?\./ && $1 }.compact!
references
end
- Rails API:
eager_loading?
--ActiveRecord::Relation
🔗 パラレルテストの最小数を設定可能になった
つっつきボイス:「test_parallelization_minimum_number_of_tests
コンフィグが追加されたんですね」「デフォルトの最小パラレルテスト数は50か」「環境によって使えるメモリ量も違うので、これはたしかにコンフィグ可能であって欲しい👍」
# Changelogより
config.active_support.test_parallelization_minimum_number_of_tests = 100
参考: 3 並列テスト -- Rails テスティングガイド - Railsガイド
🔗 データベースごとにスキーマダンプを無効にできるようになった
- PR: Add option to disable schema dumb per-database by eileencodes · Pull Request #42804 · rails/rails
つっつきボイス:「マルチプルデータベース向けの機能っぽいですね」「schema_dump: false
でデータベースごとにスキーマダンプをオフにできる」「細かいですけど見出しのdumbはdumpのつもりだったんでしょうね」
# 同Changelogより
# config/database.yml
production:
schema_dump: false
「ところでスキーマダンプをデータベースごとにオフにしたいシチュエーションって何だろう?🤔」「言われてみるとこの機能が欲しい理由がプルリクに書かれていませんね」
追いかけボイス:「もしかするとRailsのmigrationで管理していないDBが接続先にある場合に使いたいのかもしれませんね: 手動で作ったschema dumpを読み込ませたいといったケースがありえるのかも」
🔗 belongs_to
関連付けにトラッキング変更メソッドが2つ追加
belongs_to
関連付けが直前のsave
で新しいレコードを指しているかどうかと、次のsave
で新しいレコードを指しているかどうかを調べられるようになる。
post.category # => #<Category id: 1, name: "Ruby">
post.category = Category.second # => #<Category id: 2, name: "Programming">
post.category_changed? # => true
post.category_previously_changed? # => false
post.save!
post.category_changed? # => false
post.category_previously_changed? # => true
利用例: Hotwireで、ブログ記事のカテゴリが変更されたら直前のカテゴリから削除するようブロードキャストする。関連付けの直前のターゲットのアクセサが必要だが、これは別途追加可能。
同PRより
- Rails API:
ActiveModel::Dirty
つっつきボイス:「belongs_to関連付けに関連付け名_changed?
と関連付け名_previously_changed?
が生えてくるようになったみたい」「今まではbelongs_to関連付けにDirty
的な機能がなかったのか」「早くも記事が出ていました↓」「使おうと思ったことはなかったけど、あれば使うかも」
参考: Rails 7 adds change tracking methods for belongs_to associations | Saeloun Blog
🔗Rails
🔗 SorbetでRailsアプリの型シグネチャを書く方法(Ruby Weeklyより)
# 同記事より: Sorbetを使うRubyコードサンプル
# typed: true
class Foo
extend T::Sig
sig { params(num: Integer).returns(Integer) }
def self.double(num)
num * 2
end
end
Foo.double('bar') #=> Expected Integer but found String("bar") for argument num
T.reveal_type(Foo.double(10)) #=> Revealed type: Integer
つっつきボイス:「記事にあるTapiocaは、たしかSorbet用のRBI(Ruby interface)を生成するShopifyのgem↓」「sorbet-rails gemなども含めてSorbet関連ツールはひととおり揃ってきたようですね」
「このあたりを見極めるには、Sorbet環境の整ったRailsプロジェクトで数か月ぐらい開発を体験してから、自分でもスクラッチでいくつかアプリを作ってみたりする必要があるかも」「自分もそんな気がします」「Sorbetで開発が順調に回っているところを実際に体験してみないとなかなかわからなそう」「これができるとどのあたりが嬉しいんでしょうか?」「少数精鋭のプロジェクトだと実感しにくいですが、特に人数が多いプロジェクトがSorbetなどで型チェックがうまく回るようになれば、メンバーの出入りがあったときの安心感が違いますし、他にも嬉しいことがいろいろあると思います」「お〜」
🔗 activerecord-cte(Ruby Weeklyより)
つっつきボイス:「activerecord-cte、このgem名だけでおぉっという気持ちになりますね: .with
でCTE(Common Table Expression: 共通テーブル式)をActive Recordで書ける」「しかもMySQLとPostgreSQLのどちらでも使えるんですって」「お〜マジですか」
# 同リポジトリより
Post.with(
posts_with_comments: Post.where("comments_count > ?", 0),
posts_with_tags: Post.where("tags_count > ?", 0)
)
# 同リポジトリより
posts = Arel::Table.new(:posts)
top_posts = Arel::Table.new(:top_posts)
anchor_term = posts.project(posts[:id]).where(posts[:comments_count].gt(1))
recursive_term = posts.project(posts[:id]).join(top_posts).on(posts[:id].eq(top_posts[:id]))
Post.with(:recursive, top_posts: anchor_term.union(recursive_term)).from("top_posts AS posts")
# WITH RECURSIVE "popular_posts" AS (
# SELECT "posts"."id" FROM "posts" WHERE "posts"."comments_count" > 0 UNION SELECT "posts"."id" FROM "posts" INNER JOIN "popular_posts" ON "posts"."id" = "popular_posts"."id" ) SELECT "posts".* FROM popular_posts AS posts
-- 同リポジトリより
WITH posts_with_comments AS (
SELECT * FROM posts WHERE (comments_count > 0)
), posts_with_tags AS (
SELECT * FROM posts WHERE (tags_count > 0)
)
SELECT * FROM posts
- PostgreSQLドキュメント: 7.8. WITH問い合わせ(共通テーブル式)
- MySQL ドキュメント: 13.2.15 WITH (Common Table Expressions)
「recursive CTEもこうやって書けるのね↓」
# 同リポジトリより: recursive CTE
posts = Arel::Table.new(:posts)
top_posts = Arel::Table.new(:top_posts)
anchor_term = posts.project(posts[:id]).where(posts[:comments_count].gt(1))
recursive_term = posts.project(posts[:id]).join(top_posts).on(posts[:id].eq(top_posts[:id]))
Post.with(:recursive, top_posts: anchor_term.union(recursive_term)).from("top_posts AS posts")
# WITH RECURSIVE "popular_posts" AS (
# SELECT "posts"."id" FROM "posts" WHERE "posts"."comments_count" > 0 UNION SELECT "posts"."id" FROM "posts" INNER JOIN "popular_posts" ON "posts"."id" = "popular_posts"."id" ) SELECT "posts".* FROM popular_posts AS posts
参考: Understanding SQL Server Recursive CTE By Practical Examples
「複雑なクエリでCTEを使うのは考えものですが、ちょっとしたサブクエリなどでWITHを少しだけ使いたいときならこのgemを使うとよさそう👍」「おぉ」「CTEは複雑になったときにActive Recordが解釈できるかどうかが問題なんですが、CTE自体は通常のSQLでも多用される強力な機能なので、ちゃんと動くならActive RecordでもシンプルなCTEが標準でサポートされてもいいなという気持ちです」「ちゃんと動くならですね😆」
🔗 AnyCable用JavaScriptクライアント
同記事より
つっつきボイス:「Evil MartiansのVladimirさんがAnyCable↓用のJSクライアントも作ったそうです」「AnyCableはRailsのAction Cableの高速版的なgemでしたね」「元記事によると、このanycable-clientはAction CableとJSONプロトコルレベルで完全互換とある」「TypeScriptでクライアントを書けるのはよさそう👍」
🔗 SeleniumとCupriteとPlaywright
一旦Cupriteへの移行を諦めるので供養のために記事にしといた。 / はてなブログに投稿しました #はてなブログ
Selenium WebDriverからCupriteへの移行は難しい件 - patorashのブログhttps://t.co/XiLqizmXzr— パトラッシュ@エキスパート職 (@patorash) June 4, 2021
つっつきボイス:「ruby-jp Slackで見かけた上の記事にTechRachoの翻訳記事が引用されていたのでピックアップしました」「あ〜、こういう問題は実際にやってみないとわからないヤツでしょうね」「最初からCupriteで書いていたらもっとスムーズだったのかな?」「既に動いているE2Eテストはさんざん試行錯誤して書かれることが多そうなので、差し替えたときにデフォルトの挙動が細かく違ったりするのかも」
「これで思い出しましたけど、ちょっと前の銀座Railsの発表で、playwright-ruby-clientというgemを作った方がSeleniumやCupriteなどのWebドライバ周りも含めて解説していましたね」「お、後で探してみます」
参考: Playwright
以下のスライドとリポジトリです。なおplaywrightは「劇作家」という意味だそうです。
🔗 その他Rails
さっそくRubyMine 2021.2をインストールしてみた。RBS関連のアップデートが気になる〜。https://t.co/rVX4H4fhBG https://t.co/VZoo2a7sap
— Junichi Ito (伊藤淳一) (@jnchito) July 27, 2021
つっつきボイス:「RubyMineの新バージョンが出た」「今回もRBS周りが改良されているようですね」「手元のRubyMineは2021.1.3でした」
「ところで、最近自分のWindows環境でDocker for Windowsをアップデートしたらなぜか急に速くなったんですよ」「へ〜!」「まだ調べたわけではありませんがストレージアクセスが速くなった感じがする: これならボリュームマウントしてもいいかなと思ったけど環境構築で1日ぐらいつぶれそうなので、そのうちに試してみようかな」
参考: Windows に Docker Desktop をインストール — Docker-docs-ja 19.03 ドキュメント
前編は以上です。
バックナンバー(2021年度第3四半期)
週刊Railsウォッチ: ruby-gitでGit操作、最近のruby/debug、stdgems.org、Windows 365 Cloud PCほか(20210720後編)
- 20210719前編 GitHubによるdisable_joins解説、MemoWise gemでメモ化、RailsのDDoS攻撃対策ほか
- 20210713後編 ruby-spacyで自然言語処理、Ruby製x86-64アセンブラ、『タイムゾーン呪いの書』ほか
- 20210712前編 AR::Relation#destroy_allがバッチ分割に変更、Active Record暗号化解説、sidekiq-unique-jobsほか
- 20210706後編 GitHub CopilotのAI補完、Pure Ruby実装のRuby JIT rhizome、PostgreSQLのPG-Strom拡張ほか
- 20210705前編 DI的な書き方が必要なとき、脆弱性学習用アプリRailsGoat、brakemanは優秀ほか
今週の主なニュースソース
ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。
週刊Railsウォッチについて
TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)