- Ruby / Rails関連
週刊Railsウォッチ(20200914前編)10月のKaigi on Rails情報発表、JS入れ過ぎRailsアプリ、テストで便利なpuffing-billy gemほか
こんにちは、hachi8833です。ついに届きました。
Tシャツ届いた#rubykaigi pic.twitter.com/za5d35Okyc
— ハングリィ・ライク・カネゴン (@hachi8833) September 9, 2020
- 各記事冒頭には⚓でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
- 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
お知らせ: 来週の週刊Railsウォッチは連休につきお休みいたします🎌。
⚓臨時ニュース: Kaigi on Railsのタイムテーブルとスピーカー情報が発表
以下は本日見つけたツイートです。
【お知らせ】
公式サイトにタイムテーブル( https://t.co/NSbWcZXm4s )とスピーカーページ( https://t.co/5jMYx3okpS )を追加しました✨
開催1ヶ月を切りました!開催前にチェックいただけるとより一層楽しめると思います😉#kaigionrails— Kaigi on Rails (@kaigionrails) September 14, 2020
⚓Rails: 先週の改修(Rails公式ニュースより)
以下のコミットリストのChangelogを中心に見繕いました。
- コミットリスト: Comparing master@{2020-09-04}...@{2020-09-10} · rails/rails
- 6.1マイルストーン: 6.1.0 Milestone (27件)
⚓新機能: 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より大意
つっつきボイス:「修正された#40029↓を見ると、bin/rails db:create db:migrate db:test:prepare
を実行するとActiveRecord::StatementInvalid
になってるのか」「タスクを同時かつ連続的に実行すると発生するというのはたしかにエッジケース」「見た感じdb:migrate
でコネクションが解放されてなかったっぽい」「CIなんかだと連続実行はありそうですね」
- fixed issue: New Advisory Lock that opens a second connection causes bug in Postgres app · Issue #40029 · rails/rails
書き起こししていてふと思ったのですが、以下の記事の末尾に書いたbin/setup
実行時のエラーはもしかするとこのissueに関係するのかも? Rails 6.1が出たら試してみます。
⚓expanded_version
とexpanded_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::Request
とAbstractController::Base
のinspect
の出力を簡潔にした
- PR: Shorten inspect on ActionDispatch::Request by p8 · Pull Request #40173 · rails/rails
- PR: Shorten inspect on AbstractController::Base by p8 · Pull Request #40172 · rails/rails
つっつきボイス:「変更前は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.name
をbase_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っていうビジュアライザーがあるのね↓」
「moment-localesとかいうのが思い切り重複してますね↓」「jQueryが2つ3つ入ったりするのはあるある😆」
記事によると、著者が普段愛用している以下のwebpack-bundle-analyzerは、サイトの中の人でないと使えなかったとのことです。
「ここはほとんどが重複してるし↓」「ほぼ丸かぶり😆」
「JSの重複をどこまで解決するかって割と悩ましい問題なんですよね: 今どきのJSコードはgzipとかもっと高効率な手法で圧縮転送できますし、2回目以降はキャッシュも効かせられるので、JSのサイズ削減が効いてくるのは一番最初にサイトにアクセスするときぐらいだと思います」「たしかに」
参考: 2016年のOSS圧縮ツール選択カタログ - Qiita
「もちろんJSのフットプリントが小さい方がクライアント側での速度は上がりますけど、サイズ削減の効果は誤差とあんまり違わないんじゃないかなという気もしますし」
「それ以外にも、たとえばA/Bテストを実施しているサイトでは実装次第でこうやってJSが重複することもあるんじゃないかなと想像しています」「モバイル版とPC版でアプリを分けるって最近はあんまりしなくなった気がするけどどうなんだろう?🤔」
「そしてCookpadさんのサイトはモジュール重複なし↓か!」「さすがキレイ✨」
⚓motion: RubyコードだけでRailsをリアクティブにする
以下の記事で知りました。
# 同記事より
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より)
# 同リポジトリより
# テキスト/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みたいなものと考えるとよさそう」「あ〜なるほど!」
「こういうツールを使うと、外部サービスへのリクエストテストを、外部サービスを叩かずにできますね👍」「もちろんVCRでもできますけど、それをブラウザレベルでフックをかけてやれるということでしょうね」「RSpecで使えるんですね」「なかなかよさそうじゃないですか😋」
「puffing-billyって何だろうと思ったら、オーストラリアにある保存鉄道の名前だそうです↓」「この機能にこのライブラリ名を付けた理由は謎ですけど😆」「私もわかりません😆」
⚓その他Rails
つっつきボイス:「これは昔からいろんなところで記事になってる、RSpecのlet
とlet!
とインスタンス変数の違いを説明した記事みたい」「定期的に話題になるヤツですね」
前編は以上です。
バックナンバー(2020年度第3四半期)
週刊Railsウォッチ(20200908後編)Shopify版Rubyスタイルガイド、JavaScript Primerが2.0に、GitHub Container Registryほか
- 20200901後編 RubyKaigi 2020 Takeout登壇者発表、Ruby開発版が2.8から3.0へ、マイクロサービス分割ほか
- 20200831前編 GitHubがRuby 2.7にアップグレード、Durationに変換メソッドが追加、hair_triggerでデータベーストリガほか
- 20200825後編 Rubyクラスライブラリをgem化、Rubyテストフレームワークrr、ChromebookでWindowsが動くほか
- 20200824前編 「Active Jobスタイルガイド」は有用、SiderがGitLabに対応、eager loading時のselectを修正ほか
- 20200818後編 ruby_jardデバッガがスゴい、RubyオンラインマニュアルにEdit機能が追加、Ruby 2.7のBundlerを消す方法ほか
- 20200817前編 お盆も続くRails改修、Rails 6.1にManyモナドが入る?rails-auth gemでクライアント認証ほか
- 20200811山の日短縮版 RSpec Queueでパラレルテスト、カロリーメイトとRubyのコラボ、Rubyのcoercionほか
- 20200804後編 「RubyKaigi Takeout 2020」9月オンライン開催、メールバリデータtruemail、Gitのmasterが変更可能にほか
- 20200803前編 書籍『パーフェクトRuby on Rails』増補改訂版、マルチDBで抽象クラスをscaffold生成、GitLabがPumaに乗り換えほか
- 20200721後編 『パーフェクトRuby on Rails』増補改訂版発売間近、scan_left gemでレイジーなinjectほか
- 20200720前編 10月開催「Kaigi on Rails」CFP募集中、enumにデフォルト値設定機能、RailsでBitemporal Data Modelほか
- 20200714後編 ruby-warning gemでワーニングを手軽に抑制、rubocop -aの振る舞いが変わる、書籍『MySQL徹底入門 第4版』ほか
- 20200713前編 rspec-openapiでスキーマ自動生成、Rails Architect Conf動画、
where()
ハッシュキーに比較演算子条件を書ける機能ほか - 20200707後編 Rubyで無名structリテラル提案、書籍『AWS認定ソリューションアーキテクト』、21世紀のC言語ほか
- 20200706前編 Railsでのマルチテナンシー実装戦略を比較、Railsでサブクエリを使う、URI.parserが非推奨化ほか)
今週の主なニュースソース
ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。