Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

週刊Railsウォッチ(20210531前編)RailsConf 2021の動画が公開、GraphQLのN+1を自動回避、Ruby 3のJITとRailsほか

こんにちは、hachi8833です。

週刊Railsウォッチについて

  • 各記事冒頭には🔗でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • お気づきの点がありましたら@hachi8833までメンションをいただければ確認・対応いたします🙇

TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)

🔗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_changesfrom:オプションが追加


つっつきボイス:「Minitestのアサーションでfrom:を書けるようになった: いかにもRSpec的な記法ですね」「なるほど」

# 同PRより
assert_no_changes -> { Status.all_good? }, from: true do
  post :create, params: { status: { ok: true } }
end

「RSpecだとfromtoで"〜から〜に変わる"というのが書けます↓: 変更前の値も指定できるのがポイント」「それと似たものが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をサポート


つっつきボイス:「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でデータベースのconnectionsupports_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

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より)


つっつきボイス:「おぉ、RailsConf 2021の動画が公開されたんですね🎉」「今回のRailsConfは有料だった気がする: FAQを見るとたしかに有料チケットを販売していたけど、動画を公開してくれたんですね」「ありがたいです🙏」「ワークショップやLTもひとつひとつ動画を切り分けられていて、動画数すっごく多いですね」「ちゃんと手間とコストをかけて制作したのがわかる👍」

🔗 GraphQLのN+1クエリを自動で回避する(Ruby Weeklyより)


つっつきボイス:「GraphQLのN+1って割とよく聞きますよね」「GraphQLは構造上ネステッドにするとN+1になるから仕方ないでしょうね」「たしかに」「GraphQLはスキーマでネステッドな階層を組んでhas_manyしたデータを取れるようにしていくと簡単にN+1クエリが発生するので、この記事のように自動化したい気持ちもわかる」

「記事ではbatch-loader gemを使ったようです↓」

exAspArk/batch-loader - GitHub

「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側でネステッドなデータを取り出すときにはじめて影響することが分かるので気づきにくい」「あ〜そうか!」

Rails: N+1クエリを「バッチング」で解決するBatchLoader gem(翻訳)

🔗 Ruby 3のJITとRails


つっつきボイス:「こちらは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をオンにすることも増えると思います」「これはいい記事👍」「後で読もうっと」


同記事より

discourse/discourse - GitHub

🔗 その他Rails

つっつきボイス:「はてブでバズっていたGist集記事です」「true/falseを厳密にバリデーションするのか、なるほど↓」

「空白を自動でstripするStrippedString型↓も面白いですね」

「見出しを見ているとこういうニーズがあるというのがよくわかる」「よさげなGistがいろいろ載ってますね: 使っちゃおうかな」「使うならちゃんと内部ロジックも読んだ上で理解して使っていきたいですね」「はい!」


前編は以上です。

バックナンバー(2021年度第2四半期)

週刊Railsウォッチ(20210525後編)Rubyのオブジェクトアロケーション改善、RubyKaigi Takeout 2021開催日発表、AWS App Runnerほか

今週の主なニュースソース

ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。

Rails公式ニュース

Ruby Weekly


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。