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

週刊Railsウォッチ(20200914前編)10月のKaigi on Rails情報発表、JS入れ過ぎRailsアプリ、テストで便利なpuffing-billy gemほか

こんにちは、hachi8833です。ついに届きました。

  • 各記事冒頭には⚓でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄

お知らせ: 来週の週刊Railsウォッチは連休につきお休みいたします🎌。

臨時ニュース: Kaigi on Railsのタイムテーブルとスピーカー情報が発表

以下は本日見つけたツイートです。

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

以下のコミットリストのChangelogを中心に見繕いました。

新機能: PostgreSQLのNOT VALIDチェック制約をサポート

# activerecord/lib/active_record/connection_adapters/postgresql/schema_creation.rb#L12
          def visit_AddForeignKey(o)
            super.dup.tap { |sql| sql << " NOT VALID" unless o.validate? }
          end

+         def visit_CheckConstraintDefinition(o)
+           super.dup.tap { |sql| sql << " NOT VALID" unless o.validate? }
+         end
+

Active Recordには既にPostgreSQLの外部キー制約へのNOT VALIDの追加はサポートされているが、#31323でチェック制約が初期段階で追加されたものの、チェック制約やバリデーションを個別に追加する機能が含まれていなかった。このプルリクはその機能を追加する!
同PRより大意


つっつきボイス:「NOT VALIDって知らない〜」「使ったことない機能がぽすぐれにはいろいろある」「使えるようになったのはいいですね」「ぽすぐれ推しきた😆」「まあPostgreSQLはいろいろ融通が利くのがいいところですし😆」

参考: ALTER TABLE -- NOT VALID制約

abstract/database_statements.rbにexplainを追加

# activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb#L158
+     def explain(arel, binds = []) # :nodoc:
+       raise NotImplementedError
+     end
+

#37970をやっていて、explainがすべてのアダプタで実装されているにもかかわらず抽象の方には定義されていないことに気づいた。定義されていない理由はよくわからないし、ドキュメントが必要かどうかもわからないのでとりあえずnodocを付けておいた。
同PRより大意


つっつきボイス:「抽象クラスでexplainすることってありませんよね?」「raise NotImplementedErrorしてるだけか」「たぶんNoMethodErrorになるよりはNotImplementedErrorの方が適切という判断なんでしょう」

advisory lockで使ったコネクションがプールに残る問題を修正

# activerecord/lib/active_record/migration.rb#L1394
      def with_advisory_lock_connection
        pool = ActiveRecord::ConnectionAdapters::ConnectionHandler.new.establish_connection(
          ActiveRecord::Base.connection_db_config
        )

        pool.with_connection { |connection| yield(connection) }
+     ensure
+       pool&.disconnect!
      end

プール内のコネクションが切断されないため、DB内のセッションがアイドリングのまま残ってしまう。
アイドリング状態のセッションがあるとデータベースをdropできなくなり、bin/rails db:test:prepareなどのコマンドで影響が生じる。
これはエッジケースではあるが、それでもプール内のコネクションをクリーンアップするのはよい方法なので。
同PRより大意

参考: ロック (情報工学) - Wikipedia


つっつきボイス:「修正された#40029↓を見ると、bin/rails db:create db:migrate db:test:prepareを実行するとActiveRecord::StatementInvalidになってるのか」「タスクを同時かつ連続的に実行すると発生するというのはたしかにエッジケース」「見た感じdb:migrateでコネクションが解放されてなかったっぽい」「CIなんかだと連続実行はありそうですね」


書き起こししていてふと思ったのですが、以下の記事の末尾に書いたbin/setup実行時のエラーはもしかするとこのissueに関係するのかも? Rails 6.1が出たら試してみます。

docker-composeを便利にするツール「dip」を使ってみた

expanded_versionexpanded_keyのアロケーションを大幅に削減

このプルリクはキャッシュ系メソッドに3つのシンプルな変更を追加する。

  • expanded_keyではsort!を優先的に使ってアロケーションを節約する(この配列はcollectで作っているので)
  • expanded_versionではcompact!を優先的に使ってアロケーションを節約(この配列はmapで作っているので)
  • countをブロック付きで使ってdelete_multi_entriesをシンプルにする。これはinjectよりわずかに高速だがアロケーション数は同じ
    同PRより大意
Comparison:
delete_multi_entries:             80120 allocated
fast_delete_multi_entries:     80120 allocated - same
...
Comparison:
   fast_expanded_key:        488 allocated
        expanded_key:           528 allocated - 1.08x more
...
Comparison:
fast_expanded_version:        120 allocated
    expanded_version:          8168 allocated - 68.07x more

つっつきボイス:「これはActiveSupport::Cache::Storeのリファクタリングか」「sortよりsort!の方がいいと↓」「アロケーションが随分減りましたね」「エントリをコピーするよりそのまま処理する方が速いですし」「これぞリファクタリング」「これぞ高速化」

# activesupport/lib/active_support/cache.rb#L673
        def expanded_key(key)
          return key.cache_key.to_s if key.respond_to?(:cache_key)
          case key
          when Array
            if key.size > 1
              key.collect { |element| expanded_key(element) }
            else
              expanded_key(key.first)
            end
          when Hash
-           key.collect { |k, v| "#{k}=#{v}" }.sort
+           key.collect { |k, v| "#{k}=#{v}" }.sort!
          else
            key
          end.to_param
        end
# activesupport/lib/active_support/cache.rb#694
        def expanded_version(key)
          case
          when key.respond_to?(:cache_version) then key.cache_version.to_param
-         when key.is_a?(Array)                then key.map { |element| expanded_version(element) }.compact.to_param
+         when key.is_a?(Array)                then key.map { |element| expanded_version(element) }.tap(&:compact!).to_param
          when key.respond_to?(:to_a)          then expanded_version(key.to_a)
          end
        end

ActionDispatch::RequestAbstractController::Baseinspectの出力を簡潔にした


つっつきボイス:「変更前はinspectでストリームが延々出ていたのを、#39439のこれ↓みたいに表示するようにしたのか」「requestはたしかにこういう中身が見たいですよね」

request.inspect
# => "#<ActionDispatch::Request request_method=POST, original_url=https://glu.ttono.us/path/of/some/uri?mapped=1, remote_ip=1.2.3.4, media_type=application/x-www-form-urlencoded>"

「コントローラのinspectも短くなった↓」「たしかに今のままだと冗長ですし」

MyController.new.inspect # => "#<MyController:0x00000000005028>"

コントローラのアクションで自分自身を呼ぶと文字のストリームが延々生成され、その中にrequestオブジェクトやすべてのインスタンス変数がある。
これはコントローラのデバッグでイラつくし、うっかり自分自身を呼ぶと出力が止まるまで数秒待たされる。
このプルリクはinspectでクラス名だけを出力するようにする。
#40173より大意


コントローラのアクションでrequestを呼ぶと文字のストリームが延々生成され、その中にRackやさまざまなミドルウェアが含まれる。
これはコントローラのデバッグでイラつくし、うっかりrequestを呼ぶと出力が止まるまで数秒待たされる。
このプルリクはinspectで関係のある属性だけを出力するようにする。
#40172より大意

signed_idがSTIモデルで動かない問題を修正

signed_idは、class.nameとその目的の組み合わせのせいでSTIモデルで動かない。

class User < ActiveRecord::Base; end
class Student < User; end
class Teacher < User; end

User.create(type: 'Student')
User.find_signed User.first.signed_id  # nil

class.namebase_class.nameに置き換えることで通常のモデルでもSTIモデルでも扱えるようになる。
同PRより大意


つっつきボイス:「signed_idは以前も話題に出たヤツですね(ウォッチ20200525)」「STIで、同じレコードだけど親クラスのレコードか子クラスのレコードかで結果が違っていたのを修正したのか」「実際に同じものを参照しているならこれが正しいでしょうね」

# activerecord/lib/active_record/signed_id.rb#L92
      def combine_signed_id_purposes(purpose)
-       [ name.underscore, purpose.to_s ].compact_blank.join("/")
+       [ base_class.name.underscore, purpose.to_s ].compact_blank.join("/")
      end

Rails

JavaScript詰め込みすぎのRailsアプリ(RubyFlowより)


つっつきボイス:「よそのRailsアプリのJS詰め込み事情を調べた記事のようです」「まあ今どきのアプリはJSコード数十KBとか簡単に達してしまいますし」

「へ〜、source-map-explorerっていうビジュアライザーがあるのね↓」
danvk/source-map-explorer - GitHub

「moment-localesとかいうのが思い切り重複してますね↓」「jQueryが2つ3つ入ったりするのはあるある😆」


同記事より


記事によると、著者が普段愛用している以下のwebpack-bundle-analyzerは、サイトの中の人でないと使えなかったとのことです。

webpack-contrib/webpack-bundle-analyzer - GitHub


「ここはほとんどが重複してるし↓」「ほぼ丸かぶり😆」


同記事より

「JSの重複をどこまで解決するかって割と悩ましい問題なんですよね: 今どきのJSコードはgzipとかもっと高効率な手法で圧縮転送できますし、2回目以降はキャッシュも効かせられるので、JSのサイズ削減が効いてくるのは一番最初にサイトにアクセスするときぐらいだと思います」「たしかに」

参考: 2016年のOSS圧縮ツール選択カタログ - Qiita

「もちろんJSのフットプリントが小さい方がクライアント側での速度は上がりますけど、サイズ削減の効果は誤差とあんまり違わないんじゃないかなという気もしますし」

「それ以外にも、たとえばA/Bテストを実施しているサイトでは実装次第でこうやってJSが重複することもあるんじゃないかなと想像しています」「モバイル版とPC版でアプリを分けるって最近はあんまりしなくなった気がするけどどうなんだろう?🤔」

「そしてCookpadさんのサイトはモジュール重複なし↓か!」「さすがキレイ✨」


同記事より

motion: RubyコードだけでRailsをリアクティブにする

unabridged/motion - GitHub

以下の記事で知りました。

# 同記事より
class ButtonComponent < ViewComponent::Base
  include Motion::Component  attr_reader :total  def initialize(total: 0)
    @total = total
  end  map_motion :add
  def add
    @total += 1
  end
end


同記事より


つっつきボイス:「JSを書かずにRubyコードでインタラクティブなフロントエンド操作をやれるようにするみたいです」「いわゆるビューコンポーネント的なことをやれるっぽい」「似たようなgemはこれまでもあった気がしますね」「そうかも」

「バックエンドとフロントエンドの間でオブジェクトをシリアライズ通信で同期みたいなことをやりたい人はいますけど、そううまくはいかないだろうという感じで自分はあんまり好きじゃないかな〜」「わかる気がします」「フロントエンドのオブジェクトをバックエンドに保存するのはともかく、バックエンドのオブジェクトをフロントエンドに持ってくるのは何だかつらみが走りそうな予感」

「チャットアプリとかゲームみたいにわかりやすい使い方ならありかも」「まあこういうのは今あまり流行ってませんけど」


同リポジトリより、motionでやれること:

Websockets通信
RailsバックエンドとAction Cable経由で通信
ページ全体のリロードが不要
ユーザーのカレントページをその場で更新
高速なDOM差分
既存のコンテンツを新しいコンテンツで置き換えるときにDOM差分を実行
サーバーがトリガーするイベント
サーバーサイドイベントで任意の多数のコンポーネントをWebSocketチャネル経由で更新可能
ページの一部のみの置き換え
motionはページ全体の置き換えを使わず、高速化のためにDOM差分を用いてページ上のコンポーネントだけを新しいHTMLで置き換える
カプセル化され一貫したステートフルコンポーネント
コンポーネントは永続化および更新される連続内部ステートを持つ。つまりコンポーネントを変更するたびに、元の場所を置き換えられる新しいレンダリング済みHTMLが生成される
非常に高速
通信はRailsルーターやコントローラスタックを経由することが必須ではない。ルーティングやコントローラを変更しなくてもmotionの機能をすべて利用できる

puffing-billy: テストでブラウザと外部サイトのやりとりを再現するプロキシ(Ruby Weeklyより)

oesmith/puffing-billy - GitHub


# 同リポジトリより
# テキスト/JSON/JSONP(その他何でもよい)をスタブして返す
proxy.stub('http://example.com/text/').and_return(text: 'Foobar')
proxy.stub('http://example.com/json/').and_return(json: { foo: 'bar' })
proxy.stub('http://example.com/jsonp/').and_return(jsonp: { foo: 'bar' })
proxy.stub('http://example.com/headers/').and_return(
  headers: { 'Access-Control-Allow-Origin' => '*' },
  json: { foo: 'bar' }
)
proxy.stub('http://example.com/wtf/').and_return(body: 'WTF!?', content_type: 'text/wtf')

# リダイレクトや他のreturnコードをスタブする
proxy.stub('http://example.com/redirect/').and_return(redirect_to: 'http://example.com/other')
proxy.stub('http://example.com/missing/').and_return(code: 404, body: 'Not found')

# HTTPSもスタブできる!
proxy.stub('https://example.com:443/secure/').and_return(text: 'secrets!!1!')

# Proc(またはProcスタイルのオブジェクト)を渡して動的なレスポンスを作成する
#
# このprocは以下の引数で呼ばれる
#   params:  クエリ文字列のパラメーターハッシュ(CGI::escapeスタイル)
#   headers: ヘッダーのハッシュ
#   body:    リクエストbody文字列
#   url:     リクエストされた実際のURL
#   method:  リクエストされたHTTP verb
proxy.stub('https://example.com/proc/').and_return(Proc.new { |params, headers, body, url, method|
  {
    code: 200,
    text: "Hello, #{params['name'][0]}"
  }
})

# Puffing Billyを用いてリクエストやレスポンスをインターセプトできる。
# Procをひとつ渡してpass_requestメソッドを用いればよい。
# リクエスト(ヘッダー、URL、HTTPメソッド)を操作することも、上流側サーバーからのレスポンスを操作することもできる。
# 配信されたcallableのスコープは、それが定義された場所のuserスコープとなる。
# メソッドをallにすると、HTTPメソッドの種類にかかわらずリクエストをインターセプトする。
proxy.stub('http://example.com/', method: 'all').and_return(Proc.new { |*args|
  response = Billy.pass_request(*args)
  response[:headers]['Content-Type'] = 'text/plain'
  response[:body] = 'Hello World!'
  response[:code] = 200
  response
})

# POSTをスタブ化する。
# CORSリクエストを許可してメソッドにpostを指定するのを忘れずに。
proxy.stub('http://example.com/api', method: 'post').and_return(
  headers: { 'Access-Control-Allow-Origin' => '*' },
  code: 201
)

# OPTIONリクエストをスタブ化する。
# 必要な値はヘッダーに設定する。
proxy.stub('http://example.com/api', method: 'options').and_return(
  headers: {
    'Access-Control-Allow-Methods' => 'GET, PATCH, POST, PUT, OPTIONS',
    'Access-Control-Allow-Headers' => 'X-Requested-With, X-Prototype-Version, Content-Type',
    'Access-Control-Allow-Origin'  => '*'
  },
  code: 200
)

つっつきボイス:「見た感じ、puffing-billyはブラウザのプロキシとして設定してダミーのレスポンスを返してくれるサーバーなのかな?」「そんな感じです」「webmockやVCRに似ているとありますけど、ちょうとプロキシとして動くVCRみたいなものと考えるとよさそう」「あ〜なるほど!」

bblimke/webmock - GitHub
vcr/vcr - GitHub

「こういうツールを使うと、外部サービスへのリクエストテストを、外部サービスを叩かずにできますね👍」「もちろんVCRでもできますけど、それをブラウザレベルでフックをかけてやれるということでしょうね」「RSpecで使えるんですね」「なかなかよさそうじゃないですか😋」

「puffing-billyって何だろうと思ったら、オーストラリアにある保存鉄道の名前だそうです↓」「この機能にこのライブラリ名を付けた理由は謎ですけど😆」「私もわかりません😆」

参考: パッフィンビリー鉄道 - Wikipedia

その他Rails


つっつきボイス:「これは昔からいろんなところで記事になってる、RSpecのletlet!とインスタンス変数の違いを説明した記事みたい」「定期的に話題になるヤツですね」


前編は以上です。

バックナンバー(2020年度第3四半期)

週刊Railsウォッチ(20200908後編)Shopify版Rubyスタイルガイド、JavaScript Primerが2.0に、GitHub Container Registryほか

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

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

Rails公式ニュース

Ruby Weekly

RubyFlow

160928_1638_XvIP4h


CONTACT

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