- Ruby / Rails関連
週刊Railsウォッチ(20210531前編)RailsConf 2021の動画が公開、GraphQLのN+1を自動回避、Ruby 3のJITとRailsほか
こんにちは、hachi8833です。
🔗Rails: 先週の改修(Rails公式ニュースより)
今回は以下のコミットリストのChangelogを中心に見繕いました。
🔗 ActiveModel::Type.lookup
の委譲を...
に変更
# activemodel/lib/active_model/type.rb#L33
- def lookup(*args, **kwargs) # :nodoc:
- registry.lookup(*args, **kwargs)
+ def lookup(...) # :nodoc:
+ registry.lookup(...)
end
つっつきボイス:「引数の*args, **kwargs
をトリプルドット構文...
に変えてバグを修正したのね」「...
構文ってありましたね」「空ハッシュを渡すとargs={}
になるはずがargs=nil
になっていたのか」
変更しない場合、以下のような新しいテストが失敗する。
Failure:
ActiveModel::TypeTest#test_registering_a_new_type [test/cases/type_test.rb:21]:
Expected: #<struct args={}>
Actual: #<struct args=nil>
(*args, **kwargs)
の委譲は、Ruby 2.7ではターゲットが常にキーワード引数を受け取れるようになっていないと正しくない(Struct.new(:args).new
は正しくないケース)。以下を参照。
参考: Correct Delegation with Ruby 2.6, 2.7 and 3.0 · On the Edge of Ruby
同様の#42266も同じように修正された。
同PRより大意
「...
は、受け取った引数を丸ごと渡す感じなんですね」「トリプルドットはRuby 2.7から入った機能か↓」
「そういえばRuby 3.0で...
を改良して引数の先頭以外を...
で渡せるようになってましたね↓」
引数のフォワーディングの記法で先頭に引数を書けるようになりました。
www.ruby-lang.orgより
# www.ruby-lang.orgより
def method_missing(meth, ...)
send(:"do_#{ meth }", ...)
end
「トリプルドット記法使ったことなかった」「コードで見かけたら二度見しそう」「このようにコンストラクタで受けた引数を親クラスのコンストラクタにそのまま渡すのは昔からよくある操作ですね」「新しい書き方ですし、使うなら適切な場所を見きわめてからにしたい気持ち」「このプルリクでトリプルドット記法を思い出せました」
参考: Rubyで使われる記号の意味(正規表現の複雑な記号は除く) (Ruby 3.0.0 リファレンスマニュアル)
受け取った引数をそのまま別のメソッドに渡すための記法です。受け取る側も渡す側もカッコでくくる必要があります。
docs.ruby-lang.orgより
# docs.ruby-lang.orgより
def foo(...)
bar(...)
end
🔗 assert_no_changes
にfrom:
オプションが追加
つっつきボイス:「Minitestのアサーションでfrom:
を書けるようになった: いかにもRSpec的な記法ですね」「なるほど」
# 同PRより
assert_no_changes -> { Status.all_good? }, from: true do
post :create, params: { status: { ok: true } }
end
- API:
assert_no_changes
--ActiveSupport::Testing::Assertions
参考change
matcher - Built in matchers - RSpec Expectations - RSpec - Relish
「RSpecだとfrom
とto
で"〜から〜に変わる"というのが書けます↓: 変更前の値も指定できるのがポイント」「それと似たものがMinitestにも入ったということですね」「アサーションを見るだけで"〜から〜に変わる"という仕様がわかるので、この書き方は昔から好き❤️」「この機能が入ったら使ってみたいです」
# relishapp.comより
require "counter"
RSpec.describe Counter, "#increment" do
it "should increment the count" do
expect { Counter.increment }.to change { Counter.count }.from(0).to(1)
end
# deliberate failure
it "should increment the count by 2" do
expect { Counter.increment }.to change { Counter.count }.by(2)
end
end
🔗 rails dbconsole
でマルチDBのreplicaをサポート
- PR: Support using replicas when using `rails dbconsole` by cthornton · Pull Request #42285 · rails/rails
つっつきボイス:「rails dbconsole
っていう機能があるとは!」「rails db:なんちゃら
しか使ったことなかった」「rails db
だけでもいいみたい」「今までdatabase.yml見ながらpsqlやMySQLクライアントを使ってたけど、まだまだ知らない機能があるな〜」「修正はrails db
でマルチDBのreplicaを使えるようにしたんですね」
現在は
rails dbconsole
でreplicaがすべて無視されるが、replicaが使えればかなり有用(例: 分析のために、production環境から動いていないデータベースにクエリをかける)。
これが何らかの理由でbreaking changeになるのであれば、dbconsole
の引数に--include-replicas flag
を足してもよい。
同PRより大意
「手元のMySQL+Railsでやったら動かない...」「rails dbconsole
は単にデータベースクライアント(mysqlコマンドやpsqlコマンド)を呼び出すようですね: つまりデータベースクライアントが入ってなければ動かない」「あ、それで動かなかったのか」「コンテナ環境のRailsだとクライアントライブラリは入っててもコマンドラインのCLIプログラムが入っていないのはありがちかも」「まさにそれでした😅」「こちらはPostgreSQL+Railsでrails db
をやってみるとたしかにpsqlが起動しました」
🔗 Active Storageのタイムスタンプにprecision:
を追加
つっつきボイス:「Active Storageでデータベースのconnection
がsupports_datetime_with_precision?
に対応している場合にprecision: 6
が指定されるようになった」「今までは普通のタイムスタンプだったのか」
# activestorage/db/migrate/20170806125915_create_active_storage_tables.rb
class CreateActiveStorageTables < ActiveRecord::Migration[5.2]
def change
create_table :active_storage_blobs do |t|
t.string :key, null: false
t.string :filename, null: false
t.string :content_type
t.text :metadata
t.string :service_name, null: false
t.bigint :byte_size, null: false
t.string :checksum, null: false
- t.datetime :created_at, null: false
+
+ if connection.supports_datetime_with_precision?
+ t.datetime :created_at, precision: 6, null: false
+ else
+ t.datetime :created_at, null: false
+ end
t.index [ :key ], unique: true
end
- API:
supports_datetime_with_precision?
--ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
- API:
supports_datetime_with_precision?
--ActiveRecord::ConnectionAdapters::SQLite3Adapter
「precision
というオプションがあるんですね」「この記事↓にActive Recordのprecision
のことが載ってる: MySQLの場合、Rails 5まではタイムスタンプの秒に小数部分がなくて、Rails 6からはprecision: 6
がデフォルトになった」「precision
はkamipoさんが2年前に#34970でデフォルトにしたんですね」「このprecision: 6
がActive Storage用のテーブルでも付けられるようになったようですね」
参考: Rails6 のちょい足しな新機能を試す65(timestamps precision編) - Qiita
-- 同記事より: Rails 6.0.0.rc1の場合
mysql> select * from users;
+----+------+----------------------------+----------------------------+
| id | name | created_at | updated_at |
+----+------+----------------------------+----------------------------+
| 1 | Taro | 2019-07-26 06:52:38.606642 | 2019-07-26 06:52:38.606642 |
+----+------+----------------------------+----------------------------+
1 row in set (0.00 sec)
-- 同記事より: Rails 5.2.3の場合
mysql> select * from users;
+----+------+---------------------+---------------------+
| id | name | created_at | updated_at |
+----+------+---------------------+---------------------+
| 1 | Taro | 2019-07-26 06:42:34 | 2019-07-26 06:42:34 |
+----+------+---------------------+---------------------+
1 row in set (0.00 sec)
🔗Rails
🔗 RailsConf 2021の動画が出揃う(Ruby Weeklyより)
- 動画リスト: (8) RailsConf 2021 - YouTube
- スケジュール: Schedule | RailsConf 2021
つっつきボイス:「おぉ、RailsConf 2021の動画が公開されたんですね🎉」「今回のRailsConfは有料だった気がする: FAQを見るとたしかに有料チケットを販売していたけど、動画を公開してくれたんですね」「ありがたいです🙏」「ワークショップやLTもひとつひとつ動画を切り分けられていて、動画数すっごく多いですね」「ちゃんと手間とコストをかけて制作したのがわかる👍」
🔗 GraphQLのN+1クエリを自動で回避する(Ruby Weeklyより)
つっつきボイス:「GraphQLのN+1って割とよく聞きますよね」「GraphQLは構造上ネステッドにするとN+1になるから仕方ないでしょうね」「たしかに」「GraphQLはスキーマでネステッドな階層を組んでhas_many
したデータを取れるようにしていくと簡単にN+1クエリが発生するので、この記事のように自動化したい気持ちもわかる」
「記事ではbatch-loader gemを使ったようです↓」
「N+1って自動で回避できるものなんでしょうか?」「どうだろう...N+1が起きると事前にわかっていればincludes
を使ったり以下のようにActiveRecord::Associations::Preloader
でeager lodingできるんですが」
# 同記事より
class Types::PreloadableField < Types::BaseField
def initialize(*args, preload: nil, **kwargs, &block)
@preloads = preload
super(*args, **kwargs, &block)
end
def resolve(type, args, ctx)
return super unless @preloads
BatchLoader::GraphQL.for(type).batch(key: self) do |records, loader|
ActiveRecord::Associations::Preloader.new.preload(records.map(&:object), @preloads)
records.each { |r| loader.call(r, super(r, args, ctx)) }
end
end
end
「実はごく最近、GraphQLの#resolve
メソッド中の条件処理でeager loadingしたことで、Active Recordのキャッシュが効き過ぎて結果が期待どおりにならないという事案に遭遇しまして」「う、それはつらそう...」
「たしかそのときは、#resolve
メソッド中で結果セットをhas_many :through
したテーブルの条件をfilterするような処理を書いていて、その中で#eager_loading
しました。そして#resolve
の戻り値にはそのままeager_loading
済みのActive Recordオブジェクトを設定したんですが、GraphQL Query側でeager loadingしたテーブルのデータを要求すると、filter済みのデータしか取れなくて想定と異なる、みたいなことが発生しました(言葉で伝わるかな💦)」「何と」「お疲れさまです...」「調べてみたらeager loaingの部分が問題だったことがわかったので修正しました」
「こういうことが起きるかもしれないので、GraphQLの#resolve
で返却するActive Recordオブジェクトでは、eager loadingの使い方に気をつけたいと思いました」「ごもっともです」「検索に使ったActive Recordインスタンスと、GraphQLのレスポンスに渡すActive Recordのオブジェクトは、想定しないクエリキャッシュが効かないように注意して使おうという教訓を得た思いです」
「クエリキャッシュについては、たとえば個別にreload
することで対応できます: このreload
も書き忘れがちなんですが、RailsのコードならActive Recordでhas_many
しているものを取り出すときなどにreload
を書き忘れたら即結果が変わるので、その場で気づけるんですよ」「なるほど」「でもGraphQLの場合は#resolve
が返す結果だけ見ても一見問題がないのに、Query側でネステッドなデータを取り出すときにはじめて影響することが分かるので気づきにくい」「あ〜そうか!」
🔗 Ruby 3のJITとRails
Link: Ruby 3 JIT can make Rails faster. I’ve wondered Why Rails becomes slow… | by k0kubun | May, 2021 | Medium https://t.co/Qjg466jb3F
— Yukihiro Matz (@yukihiro_matz) May 22, 2021
つっつきボイス:「こちらはk0kubunさんの記事で、RailsがRuby 3のJITで遅くならないようにできたそうです」「お〜それは凄い!」
「記事の読ませ方がうまいですね: "MJITでRailsが速くならない"から始まってOptcarrotやGCCなどこれまでの話題や経緯をたどってから本題に入るという構成」「さすがですね」「Ruby 2.7ではベンチマーク対象のトップ100メソッドだけをコンパイルしていたのを、すべてのメソッドをコンパイルするように変えてみたら少し速くなったそうです」
「JITでSinatraが11%速くなって、Railsの2つのベンチマークもVMと比べて1.04倍と1.03倍速くなったんですね」「DiscourceのRailsアプリもJITで速くなったのが凄い↓: 増加は1.03倍でも、Discourseのように実際に使われている大規模RailsアプリがJITで遅くならなかったことが大事」「この結果は頼もしいですね」「RailsがJITで遅くならなければJITをオンにすることも増えると思います」「これはいい記事👍」「後で読もうっと」
🔗 その他Rails
つっつきボイス:「はてブでバズっていたGist集記事です」「true/falseを厳密にバリデーションするのか、なるほど↓」
「空白を自動でstrip
するStrippedString型↓も面白いですね」
「見出しを見ているとこういうニーズがあるというのがよくわかる」「よさげなGistがいろいろ載ってますね: 使っちゃおうかな」「使うならちゃんと内部ロジックも読んだ上で理解して使っていきたいですね」「はい!」
前編は以上です。
バックナンバー(2021年度第2四半期)
週刊Railsウォッチ(20210525後編)Rubyのオブジェクトアロケーション改善、RubyKaigi Takeout 2021開催日発表、AWS App Runnerほか
- 20210524前編 Active Supportの知られてなさそうな機能5つ、RSpecの歴史、書籍『Practicing Rails』ほか
- 20210518後編 RubyのGCを深掘りする、Psych gemのbreaking change、11月のRubyConf 2021ほか
- 20210517前編 Bootstrap 5リリース、productionでSQLiteがwarning表示、rails-ujsの舞台裏ほか
- 20210511後編 AWS Lambda関数ハンドラをDSLで書けるyake gem、VPC Peeringが同一AZ転送量無料化ほか
- 20210511後編 AWS Lambda関数ハンドラをDSLで書けるyake gem、VPC Peeringが同一AZ転送量無料化ほか
- 20210510前編 属性メソッドをキャッシュして最適化、Railsのガバナンスに関する声明、bundle install高速化ほか
- 20210427後編 RactorでUDPサーバーを作る、JSONシリアライザalba gem、AppleのAirTagほか
- 20210420後編 ShopifyのJITコンパイラYJIT、PicoRuby、DynamoDBの3つの制約ほか
- 20210419前編 RailsのN+1クエリを定番以外の方法で修正する、GitLabのセキュリティ修正リリースほか
- 20210413後編 RubyMineのRBSサポートとCode With Me、GitHub ActionとDockerレイヤキャッシュほか
- 20210412前編 Active Record属性暗号化機能がRails 7にマージ、RailsNew.ioでrails newオプションを生成ほか
- 20210407後編 エイプリルフールのRuby構文プロポーザル、AWSのVPC Reachability Analyzerほか
- 20210406前編 GitHubが修正したRailsセッションハンドリングの競合、erb/haml/slimの速度比較ほか
今週の主なニュースソース
ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。
週刊Railsウォッチについて
TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)