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

週刊Railsウォッチ: Railsリポジトリで進行中のPropshaft、inverse_ofを自動推論ほか(20211018前編)

こんにちは、hachi8833です。Kaigi on Rails 2021が今週の金曜と土曜に開催されますね。

週刊Railsウォッチについて

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

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

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

以下の公式更新情報を中心に見繕いました。

🔗 コントローラのコールバックでinstance_execを回避

procが(条件またはコールバック自身として)コールバックに渡されると、このprocがコントローラのインスタンス上でinstance_execを用いて評価される。instance_execを呼ぶとそのオブジェクトのシングルトンクラスが新たに作成され、インラインメソッドキャッシュが新たに要求される。

このコミットは、:only:exceptがコールバックに渡された場合にコントローラで余分なシングルトンクラスが作成されるのを回避する。
この効果を誇張した以下のベンチマークも作った。
https://gist.github.com/jhawthorn/bded5bc1d5f1afd4cdd7fb5b800312e1
同PRより


つっつきボイス:「なるほど、改修前のようにprocで処理するとオブジェクトのシングルトンクラスが生成されてしまう↓」

# actionpack/lib/abstract_controller/callbacks.rb#L77
      def _normalize_callback_option(options, from, to) # :nodoc:
        if from = options.delete(from)
-         _from = Array(from).map(&:to_s).to_set
-         from = proc { |c| _from.include? c.action_name }
+         from = ActionFilter.new(from)
          options[to] = Array(options[to]).unshift(from)
        end
      end

「代わりにActionFilterクラスを定義しておいてそれで処理する方が、procで回すよりも生成を削減できて高速化につながるということのようですね」「なるほど、そういえばJeremy Evansさんの『Polished Ruby Programming』にもこんな話がどこかにあった気がします」「考え方としてはValue Objectを連想しますね」

「呼び出される回数がものすごく多いコードをこうやって最適化すると、改善がたとえ数%であっても顕著な差が出る」「たしかにコールバックは呼び出しが多そう」

# actionpack/lib/abstract_controller/callbacks.rb#L38
+   class ActionFilter
+     def initialize(actions)
+       @actions = Array(actions).map(&:to_s).to_set
+     end
+
+     def match?(controller)
+       @actions.include?(controller.action_name)
+     end
+
+     alias after  match?
+     alias before match?
+     alias around match?
+   end

🔗 スコープ付き関連付けでinverse_ofを自動推論

  • スコープ付き関連付けでinverse_ofを自動的に検出できるようになった

inverse_ofの自動検出がスコープ付き関連付けで動くようになった。たとえば、以下のcomments関連付けで自動的にinverse_of: :postが検出されるので、このオプションを渡す必要がなくなる。

class Post < ActiveRecord::Base
  has_many :comments, -> { visible }
end

class Comment < ActiveRecord::Base
  belongs_to :post
end

ただし、まだこの自動検出は逆関連付けにスコープがあると動作しない。この例では、post関連付けにスコープがあると、Railsがcomments関連付けの逆関連付けを探索できなくなる。

これはRails 7の新規アプリでデフォルトになる。オプトインするには以下の設定を用いる。

config.active_record.automatic_scope_inversing = true

Daniel Colson, Chris Bloom
同Changelogより


つっつきボイス:「お、can_find_inverse_of_automaticallyというメソッドを改修することで、スコープ付きの関連付けでinverse_ofを自動検出するようにしたんですね↓」「こんなメソッドがあったとは」

このコミットはcan_find_inverse_of_automaticallyを変更して、関連付けにスコープがあるが逆の関連付けにスコープが存在する可能性がない場合にinverse_ofを自動検出するようにした(can_find_inverse_of_automaticallyは関連付けのリフレクションで最初に呼び出され、trueが返されれば逆のリフレクションを探索し、最終的に逆のリフレクションでそのメソッドを再び呼び出すことでそのメソッドを確実に呼び出せるようにする)。
同PRより抜粋

inverse_ofを毎回手動で書くのは割と手間だったので、副作用がないならこういうのはありがたい👍」「Changelogには逆の関連付けにスコープがあると動かないと書かれていますね」「単純なものなら手動で書けば動かせますけど、逆の関連付けにスコープが付いていると自動でコードを生成するのは簡単ではなさそう」

「コミットメッセージによるとGitHubには関連付けが171個もあるので、これを使ってinverse_ofを自動追加したいとありますね: 確かにこれだけたくさんあると手動でやってたら間違えそう」「コミットしたcomposerinteraliaさんはGitHubのスタッフなんですね」

🔗 コネクションのスキーマキャッシュをlazy loadingできる機能を追加

  • コネクションのスキーマキャッシュをlazy loadingするオプションを追加

従来は、Active Recordでスキーマキャッシュを読み込むには起動時にRailtieを用いる方法しかなかった。このオプションは、コネクションが確立した後でコネクションのスキーマキャッシュを読み込める機能を提供する。コネクションの確立後にキャッシュが読み込まれるので、コネクションのキャッシュを遅延読み込みする機能はマルチプルデータベースを用いるRailsアプリケーションで重宝するだろう。現時点では、Railtiesは起動前にコネクションにアクセスできない。

このキャッシュを用いるには、アプリケーションのコンフィグでconfig.active_record.lazily_load_schema_cache = trueを設定する。さらに、デフォルトの"db/schema_cache.yml"パスを使いたくない場合は、データベースコンフィグにschema_cache_pathも設定するべき。
Eileen M. Uchitelle
同Changelogより


つっつきボイス:「コネクションのスキーマキャッシュをlazy loadingする機能、通常はそれほど必要なさそうに見えるけど、マルチプルデータベースで便利な可能性か、なるほど」「あくまでオプションなので、使いたければ使えるという感じですね」

🔗 pg:dumpでCOMMENTの出力を回避

このプルリクは、PostgreSQL利用時にCOMMENT ON EXTENSION plpgsql IS 'PL/pgSQL procedural language';のような行がdb:structure:dump実行時に出力されないようにする。
この種のCOMMENTがあってもあまり価値はないように思える。
しかしこのCOMMENTがあると、#36816で指摘されているようにdb:structure:loadがDBのスーパーユーザーでないと動かなくなり、ActiveRecord::StatementInvalid: PG::InsufficientPrivilege: ERROR: must be owner of extensionのような一般的なエラーが出力されてデバッグがやりにくくなる点がよくない。
同PRより


つっつきボイス:「PostgreSQLバージョンが11以上のときはdb:structure:dumpでCOMMENTを出力しないようにしたんですね」

「なるほど、COMMENT ON EXTENSION plpgsql IS 'PL/pgSQL procedural language';みたいな行があるとスーパーユーザー以外はdb:structure:loadが失敗するので、失敗しないようにCOMMENTを抑制したということか」「なるほど」「作ったダンプが読み込めないのかと思って焦りそうなので、余分なエラーがなくなるのはいい👍」「ですよね」

🔗 PostgreSQLのクエリパラメータ構文を利用できるようになった

従来は以下を実行すると

ActiveRecord::DatabaseConfigurations::UrlConfig.new(:production, :production, 'postgres:///?user=user&password=passwd&dbname=theapp_production', {}).configuration_hash

以下が返された。

{ :user=>"user", :dbname=>"theapp_production", :adapter=>"postgresql" }

uri.passwordnilなのでpassword属性がnilを返し、このハッシュをマージしたものが上の結果となる。

このプルリクはこの点を修正して以下を返すようになる。

{ :user=>"user", :password=>"passwd", :dbname=>"theapp_production", :adapter=>"postgresql" }

この問題は#42797で指摘された。なお同issueでは、postgres://user:passwd@/theapp_productionはPostgreSQLのURIとして有効という指摘もあった。現在は、以下を実行すると

ActiveRecord::DatabaseConfigurations::UrlConfig.new(:production, :production, 'postgres://user:passwd@/theapp_production', {})

以下のエラーが出力される。これはURIが有効なRFC 2396実装でないことが原因。これを修正するアイデアがあれば求む。

/home/abeid/.rbenv/versions/3.0.1/lib/ruby/3.0.0/uri/generic.rb:207:in 'initialize': the scheme postgres does not accept registry part: user:passwd@ (or bad hostname?) (URI::InvalidURIError)

同PRより


つっつきボイス:「postgres:///?user=user&password=passwd&dbname=theapp_productiみたいにpostgres:///で始まるURIを指定できるようになったんですね」「こんなふうにuserやpasswordを指定するのか」「これはたぶん本当のURI形式とは違うんじゃないかな?」「あくまでURI風ということなのかも」「JDBCとかでこの形式のURIを使った覚えがあります」「そうそう」

参考: Java Database Connectivity - Wikipedia

🔗Rails

🔗 RailsリポジトリにあるPropshaft


つっつきボイス:「この間Railsウォッチをレビューいただいたときに(ウォッチ20211004)このPropshaftの存在を教わったので取り上げてみました」「そうそう、アセットパイプラインの新しい選択肢のひとつが作り中のはずと思って探してみたら、Railsのリポジトリの中にこのProfshaftがあったんですよね」

rails/propshaft - GitHub

「アセットパイプラインで従来のSprocketsの代わりにProfshaftも使えるようになるということみたい」「Propshaftって造語かと思ったらプロペラシャフトでした↓」「Sprocketsもそうですけど、機械の部品っぽい命名にしてるんでしょうね」「くるくる回り続ける部品感ある」

参考: propshaftとは何? Weblio辞書
参考: sprocketの意味・使い方・読み方 | Weblio英和辞書

「PropshaftのREADMEを見ると、アセットのバンドルを頑張らなくてよくなった時代のためのアセットパイプラインライブラリという感じの触れ込みですね」「Rails 7ではアセットのプリコンパイルを避ける方向に持っていきたいはずなので、Propshaftはその一環ということでしょうね: Rails 7で例のimport mapが導入されることで、Sprocketsのときよりも機能を軽量化できた感じ」「なるほど」

「READMEの末尾にある『PropshaftはRails 7に入るのか?』というFAQに『入る可能性は高いけど当面Sprocketsのサポートも必要』とありますね」「まだしばらくかかりそうかな」

Rails 7: importmap-rails gem README(翻訳)

🔗 Active Supportの#descendantsメソッドを深掘りする(Ruby Weeklyより)

参考: ActiveSupport::DescendantsTracker


つっつきボイス:「descendantsメソッドやancestorsメソッドは1〜2年おきぐらいに話題になる感じ」「クラスやモジュールの読み込みリストをチェックするメソッドですね」「記事にもあるように、モジュールをincludeprependした結果の順序などが罠になることがあります」

🔗 SeleniumによるシステムテストをCupriteに移行してみた(Ruby Weeklyより)


つっつきボイス:「ChromeでテストするならCupriteでいいんじゃないかな: 手順が丁寧に説明されていてよさそう👍」

「記事でも取り上げていますけど、Ajax/Fetch周りが割と複雑」「記事ではテストを書き直さないといけなかったそうです」「イベントを取得するとか、通信が発生して書き換えを待つような動作のテストは難しい部分ですね」

「そういえば少し前にもCupriteに変えてみた記事があったのを思い出しました↓」「こういう実際に動かしてみた記事が増えてくると助かります🙏」

参考: 2021年6月現在、Cupriteで"正しい"システムテストはできるのか?

🔗 SidekiqをActive Job経由ではなく直接使う(Ruby Weeklyより)

# 同記事より: Sidekiqを直接使う場合
class DoThingsInBackgroundJob
  include Sidekiq::Worker
  Sidekiq_options queue: "default"

  def perform(id)
    an_active_record_object = ActiveRecordObject.find_by(id: id)
    an_active_record_object.do_things
  end
end

つっつきボイス:「タイトルで言いたいことはだいたい理解できた😆」「気持ちわかります」「Active Jobは抽象化されている分機能が少な目なので、生のSidekiqを使う方が話が早い」

参考: Active Job の基礎 - Railsガイド

「記事の末尾に『SidekiqはPro版にするのがおすすめ』とありました」「Sidekiqの有料版を使ったことはないけど、十分普及していて経営も順調なサービスならサポートなどの面を考えてもお金を払う価値はあるでしょうね」「たしかに」

参考: Sidekiq: Simple, efficient background jobs for Ruby.

「お、Sidekiqの価格表に支払い方法が書かれているのがちょっと珍しいかも: Enterprise版は請求書払いができるとありますよ」「ほんとだ」「請求書払いに対応していないと日本企業でなかなか決済が通らなかったりしますよね」「そういう傾向はありますね」「最近はさすがに減りつつあると思いますけど」

🔗 RSpecのsubject


つっつきボイス:「RSpecのsubjectは、うまく適合するケースの方が少ないなと自分も思います: itがたくさんある場合にはsubjectがあると明らかに繰り返しが減りますけど、記事にもあるように以下のような使い方はたしかに最悪↓」「う、subjectの中でincrementしてる」「subjectの中での改変はやめて欲しい」

# 同記事より: subjectが向いてないケース
class Counter
  attr_reader :count

  def initialize
    @count = 0
  end

  def increment
    @count += 1
  end
end

describe 'Counter#increment' do
  let(:counter) { Counter.new }
  subject { counter.increment }
  it do
    subject
    expect(counter.count).to eq 1
  end
end

「行数の多いテストにsubjectがあると、このテストのsubjectはどこだっけと探さないとわからなくなりますよね」「たしかに」「subjectには名前を付けて呼び出せる機能もあるので、それを使うならsubjectがあってもいいかなと思います: 名前付きsubjectは嫌いじゃない」「なるほど」

参考: Explicit Subject - Subject - RSpec Core - RSpec - Relish

🔗 その他Rails

つっつきボイス:「上はr7kamuraさんのRailsアップグレード記事ですね」

「r7kamuraさんはたしか以前からRailsアップグレード作業を請け負っていますね↓」「お〜」「Railsのアップグレードは社内の人がなかなかやりたがらないこともあったりするので、Railsアップグレードに特化して業務を請け負うのはひとつの生存戦略だなと思いました」「そうそう、アップグレードのノウハウも蓄積できますよね」

参考:『Railsアップグレード百景』という題で発表した


前編は以上です。

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

週刊Railsウォッチ: Ruby 3.1にYJITマージのプロポーザル、Rubyのmagic historyメソッド、JSのPartytownほか(20211012後編)

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

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

Rails公式ニュース

Ruby Weekly


CONTACT

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