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

週刊Railsウォッチ: SorbetでRailsアプリの型シグネチャを書く、activerecord-cte gemとanycable-client gem(20210803前編)

こんにちは、hachi8833です。RubyKaigi Takeout 2021のチケット販売が開始されました。

週刊Railsウォッチについて

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

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

🔗Rails: 先週の改修(Rails公式ニュースより)

今回は以下の公式更新情報から見繕いました。

🔗 Active SupportのNilClass#tryNilClass#try!がRuby 2.7以降で遅かった問題を修正


つっつきボイス:「以前#34068でRuby 2.5向けにNilClass#tryNilClass#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

🔗 ハッシュ構文でorderしたときのeager_loading?を修正

ハッシュ構文で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

🔗 パラレルテストの最小数を設定可能になった


つっつきボイス:「test_parallelization_minimum_number_of_testsコンフィグが追加されたんですね」「デフォルトの最小パラレルテスト数は50か」「環境によって使えるメモリ量も違うので、これはたしかにコンフィグ可能であって欲しい👍」

# Changelogより
config.active_support.test_parallelization_minimum_number_of_tests = 100

参考: 3 並列テスト -- 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より


つっつきボイス:「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関連ツールはひととおり揃ってきたようですね」

Shopify/tapioca - GitHub

chanzuckerberg/sorbet-rails - GitHub

sorbet/sorbet-typed - GitHub

「このあたりを見極めるには、Sorbet環境の整ったRailsプロジェクトで数か月ぐらい開発を体験してから、自分でもスクラッチでいくつかアプリを作ってみたりする必要があるかも」「自分もそんな気がします」「Sorbetで開発が順調に回っているところを実際に体験してみないとなかなかわからなそう」「これができるとどのあたりが嬉しいんでしょうか?」「少数精鋭のプロジェクトだと実感しにくいですが、特に人数が多いプロジェクトがSorbetなどで型チェックがうまく回るようになれば、メンバーの出入りがあったときの安心感が違いますし、他にも嬉しいことがいろいろあると思います」「お〜」

🔗 activerecord-cte(Ruby Weeklyより)

vlado/activerecord-cte - GitHub


つっつきボイス:「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

「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クライアント


同記事より

anycable/anycable-client - GitHub


つっつきボイス:「Evil MartiansのVladimirさんがAnyCable↓用のJSクライアントも作ったそうです」「AnyCableはRailsのAction Cableの高速版的なgemでしたね」「元記事によると、このanycable-clientはAction CableとJSONプロトコルレベルで完全互換とある」「TypeScriptでクライアントを書けるのはよさそう👍」

AnyCable 1.0: RubyとGoによるリアルタイムWebの4年間(翻訳)

🔗 SeleniumとCupriteとPlaywright


つっつきボイス:「ruby-jp Slackで見かけた上の記事にTechRachoの翻訳記事が引用されていたのでピックアップしました」「あ〜、こういう問題は実際にやってみないとわからないヤツでしょうね」「最初からCupriteで書いていたらもっとスムーズだったのかな?」「既に動いているE2Eテストはさんざん試行錯誤して書かれることが多そうなので、差し替えたときにデフォルトの挙動が細かく違ったりするのかも」

「これで思い出しましたけど、ちょっと前の銀座Railsの発表で、playwright-ruby-clientというgemを作った方がSeleniumやCupriteなどのWebドライバ周りも含めて解説していましたね」「お、後で探してみます」

参考: Playwright

以下のスライドとリポジトリです。なおplaywrightは「劇作家」という意味だそうです。

YusukeIwaki/playwright-ruby-client - GitHub

🔗 その他Rails


つっつきボイス:「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後編)

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

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

Rails公式ニュース

Ruby Weekly


CONTACT

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