- Ruby / Rails関連
週刊Railsウォッチ(20210119前編)PostgreSQLのCTEを使えるActiveRecordExtended gem、2021年初頭のRails展望記事ほか
こんにちは、hachi8833です。
- 各記事冒頭には⚓でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
- 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
- お気づきの点がありましたら@hachi8833までメンションをいただければ確認・対応いたします🙇
TechRachoではRubyやRailsの最新情報などの記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)
⚓Rails: 先週の改修(Rails公式ニュースより)
今回は公式更新情報を中心に見繕いました。いくつかは先週のウォッチで先回りしてました(ウォッチ20210112)。
つっつきボイス:「Rails公式の更新情報も新年号っぽく出ていました🎍」
⚓ 同じフォームを複数のHTTPメソッドで送信するformmethod:
オプション
ブラウザは
<form>
を送信するときに、送信を開始した要素をFormData
の一部としてシリアライズし、そこにname
やvalue
といった属性を含めている。
ブラウザが<form>
送信でサポートするHTTP verbはGET
とPOST
のみに限られている。現在のRailsでは、_method="VERB"
をシリアライズしてFormData
に加える<input type="hidden" name="_method" value="VERB">
をビルドすることでこの制約を回避している。
このコミットは同一のフォーム内で複数のHTTP verbをサポートするために、
フォームのビルド中にform.button formmethod: "..."
が呼び出されると、_method
の値をformmethod:
で指定した任意の値に書き換える。
同PRより大意
つっつきボイス:「なるほど、button
ヘルパーでformmethod:
を使ってHTTP verbを指定できるようになったんですね: 以下のDeleteボタンの場合、form_with
側のmethod:
で指定しているPUTをDELETEで上書きしてからフォームを送信するようになる」「これをできるようにした気持ちは何となく想像が付きますね」
<!-- changelogより -->
<%= form_with model: post, method: :put do %>
<%= form.button "Update" %>
<%= form.button "Delete", formmethod: :delete %>
<% end %>
<!-- changelogより: 上の生成結果 -->
<form action="posts/1">
<input type="hidden" name="_method" value="put">
<button type="submit">Update</button>
<button type="submit" formmethod="post" name="_method" value="delete">Delete</button>
</form>
「どんなときに便利なんでしょうか?」「まさに上のコード例でやっているように、たとえば同じフォームの中にUpdateボタンとDeleteボタンの両方を楽に置きたいときなどですね: 同じことはformmethod:
がなくてもできますが、その場合はDeleteボタンのリンクを別途作って、このPostモデルのidをリンクで指定してHTTPのDELETE
メソッドを送信できるようにする必要があったんですよ」「そういえばそうだったかも」
「formmethod:
ができたおかげで、そういうことをしなくてもbutton
メソッドにformmethod: :delete
と書くだけで、モデルのidをリンクで指定せずに同じフォームにDeleteボタンも置けるわけです」「あ、なるほど!」「idを指定せずに書けるのは便利だと思います👍」「上のコード例のような代表的なCRUDのときに便利ですよね」
参考: CRUD - Wikipedia
⚓ Action Textのカスタマイズ機能を強化
拡張可能なレイアウト
(このプルリクは、)リッチテキストコンテンツを「囲む」HTMLのレンダリング方法を、layouts/action_text/contents/_content.html.erbのような拡張可能なテンプレートとして公開する。それによってユーザーランド側でのカスタマイズを促進しつつ、#render_action_text_content
ヘルパー呼び出しをaction_text/contents/_content.html.erbパーシャルに移動することで、リッチテキスト自身のレンダリング方法を制御するprivate APIが失われないようにする。拡張可能かつアタッチ可能な
to_attachable_partial_path
あるレコードについてアプリケーションがカノニカルなパーシャルを宣言すると、リッチテキストに変換された場合にそのパーシャルの使われ方をオーバーライドする方法がなかった。たとえば、デフォルトの
Person < ApplicationRecord
インスタンスが#to_partial_path
への呼び出しに対して"people/person"
を返すと、app/views/people/_person.html.erbパーシャルが(問答無用で)レンダリングされる。改修前は、
<action-text-attachment sgid="...">
要素があるとAction Textがそれに対応するAttachable
インスタンス(普通はActiveRecord::Base
のインスタンス)を取得し、それが持つ#to_partial_path
に対応するパーシャルをレンダリングすることでリッチテキストHTMLに変換していた。ここで提案する改修は、
#to_partial_path
ではなくAttachable#to_attachable_partial_path
を呼ぶというものだ。なおデフォルトでは#to_attachable_partial_path
は#to_partial_path
のエイリアスになる。ガイド
これらのテンプレートのカスタマイズ方法と、ActionText::Attachable
インスタンスがレンダリングされてHTMLになるまでのわかりやすい説明をRails GuidesのAction Text Overviewに追記した。
同PRより大意
つっつきボイス:「Action Textで表示する中身のリッチテキストのレンダリング方法をカスタマイズできるようにした、ということみたい: このあたり↓とかを見ると、attachment(添付ファイル)周りをカスタマイズしたいという要望があるのかも」「ふむふむ」「レンダリング方法をオーバーライドできるようにしたいというのはもっともですね」
# actiontext/app/helpers/action_text/content_helper.rb#L21
def render_action_text_attachments(content)
content.render_attachments do |attachment|
unless attachment.in?(content.gallery_attachments)
attachment.node.tap do |node|
- node.inner_html = render(attachment, in_gallery: false).chomp
+ node.inner_html = render_action_text_attachment attachment, locals: { in_gallery: false }
end
end
end.render_attachment_galleries do |attachment_gallery|
render(layout: attachment_gallery, object: attachment_gallery) do
attachment_gallery.attachments.map do |attachment|
- attachment.node.inner_html = render(attachment, in_gallery: true).chomp
+ attachment.node.inner_html = render_action_text_attachment attachment, locals: { in_gallery: true }
attachment.to_html
end.join.html_safe
end.chomp
end
end
「サードパーティのWYSIWYG機能をそのまま使うのでなく、Railsの機能でWYSIWYGを作り込んでしまうと、フロントエンド側のメンバーもWYSIWYGを扱おうとするときにconflictが起きてしまうので、慎重に考えたいところです: 特にWYSIWYGは生成したHTMLや中間言語に変換されたテキストがRDBMSの中にデータとして保存されるので、一度使い始めると乗り換えが難しい」「たしかに!」
⚓ Redisの情報を取る#info
を追加
つっつきボイス:「RedisCacheStore
に#info
ができたということは、Redisにもinfo
というメソッドがありそう(しばらく探す): コマンドのところにあった↓」
参考: INFO – Redis
「このINFOコマンドにもstats
があるから、たぶんそれに相当する情報を取得するんでしょうね」「あるなら欲しい値ですね」
# activesupport/lib/active_support/cache/redis_cache_store.rb#L
def stats
redis.with { |c| c.info }
end
⚓ Link
ヘッダーを無効にするオプションを追加
stylesheet_link_tag
やjavascript_include_tag
を使うとLink
ヘッダーを自動生成するサポートがPR #39939で追加された。しかしすべてをプリロードすべきとも限らない(例: IEはこのヘッダをサポートしていないのでレガシーIEスタイルシートへのリンクのプリロードは不要だし、ブラウザによってはstylesheet_link_tag
とjavascript_include_tag
がIEの条件付きコメントの中に入り、使わない場合でもプリロードがトリガされる)。これによって帯域幅のコストが増加してアプリケーションのパフォーマンスが落ちる。
Link
ヘッダーの複雑なニーズを抱えているサイトで柔軟性を高めるために、本コミットでは同ヘッダーを完全に無効にするオプションを追加して、Link
ヘッダーの生成方法をアプリケーション側で決定できるようにする。
メモ: 本コミットの意図は、6-1-stableにバックポートすることと、new_framework_defaults_6_1.rb
にそれ用のエントリを追加して、アプリケーションを6.1にアップグレードするときにデフォルトのオプションをどうするかを決められるようにすること。多くのサイトはレガシーIEの影響を受けるので。
同PRより大意
つっつきボイス:「stylesheet / javascript helperで読み込むコンテンツを常にプリロードしたいとは限らない」「IE対応している場合、RailsがLink
ヘッダーを常に直接生成するのはうれしくないときがあるので、生成するかどうかを制御するようにした: たしかにこれが欲しい場合ありますね」「if lt IE 7
みたいな書き方をするレガシーIE対応、最近だんだん見かけなくなってきましたけどそういえばありましたね↓」「あったあった」
参考: 条件付きコメントを使ったIE対策コーディング - HTML・CSSテックラボ - [SMART]
<!-- rfs.jpの同記事より -->
<!-- IE6以下のみ表示 -->
<!--[if lt IE 7]><html class="ie6" lang="ja"><![endif]-->
<!-- IE7のみ表示 -->
<!--[if IE 7]><html class="ie7" lang="ja"><![endif]-->
<!-- IE7のみ表示 -->
<!--[if IE 8]><html class="ie8" lang="ja"><![endif]-->
<!-- IE8より上、もしくはIE以外のみ表示 -->
<!--[if (gt IE 8)|!(IE)]><!--><html lang="ja"><!--<![endif]-->
⚓ Action Viewの#excerpt
ヘルパーのパフォーマンスを改善
ここからはコミットリストから見繕いました。
つっつきボイス:「#excerpt
ってメソッドあったのね↓」「excerptは"抜粋"か」「お〜、以下のQiita記事を見るとWeb上の長いテキストの一部を取り出してそれ以外を...
とかに置き換えてくれるメソッドなのか」「なるほど、Googleの検索結果とか、表紙の記事リストで記事冒頭だけ抜粋するときとかで使われるヤツですね」「これ自分で実装したことあったんですが、それ用のメソッドがあったとは...」
参考: Rails TextHelperまとめ - Qiita
# api.rubyonrails.orgより
excerpt('This is an example', 'an', radius: 5)
# => ...s is an exam...
excerpt('This is an example', 'is', radius: 5)
# => This is a...
excerpt('This is an example', 'is')
# => This is an example
excerpt('This next thing is an example', 'ex', radius: 2)
# => ...next...
excerpt('This is also an example', 'an', radius: 8, omission: '<chop> ')
# => <chop> is also an example
excerpt('This is a very beautiful morning', 'very', separator: ' ', radius: 1)
# => ...a very beautiful...
「Railsの#excerpt
↓はsplit
を使ってやってる: 当時の自分が実装した方法とは違うな」「今回改良したのは、このメソッドのコア部分のcut_excerpt_part
のパフォーマンスだったんですね」
# File actionview/lib/action_view/helpers/text_helper.rb, line 175
def excerpt(text, phrase, options = {})
return unless text && phrase
separator = options.fetch(:separator, nil) || ""
case phrase
when Regexp
regex = phrase
else
regex = /#{Regexp.escape(phrase)}/i
end
return unless matches = text.match(regex)
phrase = matches[0]
unless separator.empty?
text.split(separator).each do |value|
if value.match?(regex)
phrase = value
break
end
end
end
first_part, second_part = text.split(phrase, 2)
prefix, first_part = cut_excerpt_part(:first, first_part, separator, options)
postfix, second_part = cut_excerpt_part(:second, second_part, separator, options)
affix = [first_part, separator, phrase, separator, second_part].join.strip
[prefix, affix, postfix].join
end
「自分のときはどう実装したかな(一同で当時のコードを見る): RubyのStringScanner
とscan_until
を使って実装してた」「お〜、これですか」「このときは他の処理もやってたので少し複雑でしたね」
「excerpt
ヘルパー、いつからあったんだろう?」「コミットリストを見ると13 years agoとあるので随分前からあったらしい」「Railsにこれだけたくさんメソッドがあると、知らないものもまだまだありそうですね」
⚓ 番外: ルーティングテーブルのダークモード用CSSを修正
つっつきボイス:「修正はシンプルですが、ダークモード対応がちょっと面白かったので拾いました」「1行おきに背景色を変えて縞々にしてる」「CSSのtr:nth-child(even)
を追加するという方法でやってますね↓」
# actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb#L53
@media (prefers-color-scheme: dark) {
+ body {
+ background-color: #222;
+ color: #ECECEC;
+ }
+
#route_table tbody tr:nth-child(odd) {
background: #333;
}
+ #route_table tbody tr:nth-child(even) {
+ background: #444;
+ }
+
参考: :nth-child()
- CSS: カスケーディングスタイルシート | MDN
⚓Rails
⚓ 2021年初頭にRailsの現状を展望する(Hacklinesより)
つっつきボイス:「見出しからして、Railsは2021年にもありか?という感じの記事かな」「はい、新年っぽいRailsの現状展望記事を拾いました」
「こういう記事のグラフやデータは、何を母集団とした調査かによって変わってくるので、そこを踏まえたうえで読みたい」「そうですね」「2021年のRailsコミュニティに関するセクションとか面白そう」「Railsは画期的に新しいサービスの構築に向いているという感じの締めくくりですね」
「眺めた限りですが、記事の立場としては割とRails擁護的かな: こういう記事に書かれているようなことは、Railsを長年使っている開発者ならだいたい心得ているものですが、これからRailsを学ぼうかどうしようか迷っている人にはよさそうですね」
「実はここ1年ほど、以下の名記事↓のような『Railsは今後もありか?』とか「Railsを選ぶ理由」とか「Railsは死んだ」的ないい英語記事がなかなか見当たらなくて、ウォッチで取り上げられそうなものがめったになかったんですよ」「お、そうでしたか😆」「記事の件数は多いんですが、この1年ほどは主観で書いたような記事ばかり目について、こういういい記事はもう出てこないんだろうかと思い始めていたところでした」
「言われてみればこの記事はややRails寄りっぽくはあるものの、エビデンスや出典を示すなど比較的ちゃんと書かれたもののように思えますね」「私もそんな気がしています: この記事なら翻訳してもいいかなと思いました」
⚓ 🌟 ActiveRecordExtended: RailsにPostgreSQL独自の機能を追加(Ruby Weeklyより)🌟
つっつきボイス:「ActiveRecordExtendedはPostgreSQLを使うRails向けの拡張だそうです」「どれどれ」
「なるほど、ぽすぐれのArray
カラム用のANYやALLのような機能がRailsのメソッドとして使える↓: こういうのを自分で作ったことありました」「all
は既存のメソッドと名前がかぶってますけどね」
# 同リポジトリより
alice = User.create!(tags: [1])
bob = User.create!(tags: [1, 2])
randy = User.create!(tags: [3])
User.where.any(tags: 1) #=> [alice, bob]
# 同リポジトリより
alice = User.create!(tags: [1])
bob = User.create!(tags: [1, 2])
randy = User.create!(tags: [3])
User.where.all(tags: 1) #=> [alice]
「IPアドレスを扱うメソッドなどもある↓」「全般にこのgemは、PostgreSQLに組み込まれている特定の型を活用するメソッドを提供するもののようですね: 使うかどうかは好みが分かれそうではありますが」「なるほど」
alice = User.create!(ip: "127.0.0.1/16")
bob = User.create!(ip: "192.168.0.1/16")
User.where.inet_contains(ip: "127.0.0.254") #=> [alice]
User.where.inet_contains(ip: "192.168.20.44") #=> [bob]
User.where.inet_contains(ip: "192.255.1.1") #=> []
「やや、このgemはPostgreSQLのCTE(Common Table Expression)のメソッド↓をサポートしてるじゃない!!」「おぉ?」「これはちょっとアツいかも: CTEのためだけにこのgemを使ってもいいんじゃないかって思いました」「俄然色めき立ってきましたね」
# 同リポジトリより
alice = User.create!
bob = User.create!
randy = User.create!
ProfileL.create!(user_id: alice.id, likes: 200)
ProfileL.create!(user_id: bob.id, likes: 400)
ProfileL.create!(user_id: randy.id, likes: 600)
User.with(highly_liked: ProfileL.where("likes > 300"))
.joins("JOIN highly_liked ON highly_liked.user_id = users.id") #=> [bob, randy]
- ドキュメント: 7.8. WITH問い合わせ(共通テーブル式) -- CTE
「しかもwith
やjoins
をこうやってつなげられる↓」「おおぉ〜」「これでついにぽすぐれのWITH
句が使える!」
User.with(highly_liked: ProfileL.where("likes > 300"), less_liked: ProfileL.where("likes <= 200"))
.joins("JOIN highly_liked ON highly_liked.user_id = users.id")
.joins("JOIN less_liked ON less_liked.user_id = users.id")
# OR
User.with(highly_liked: ProfileL.where("likes > 300"))
.with(less_liked: ProfileL.where("likes <= 200"))
.joins("JOIN highly_liked ON highly_liked.user_id = users.id")
.joins("JOIN less_liked ON less_liked.user_id = users.id")
「こんなふうにあっさりPostgreSQLのCTEを使えるというのは相当なものですね👍」「今までActive RecordではWITH
句をうまく書けないなってずっと思っていたんですが、こんなふうに書けるなら使ってみたい」「後はこれをどのぐらいハードに使っても大丈夫なのかが知りたいですね」「単純なCTEは十分動きそうな感じ」
「CTEサポートって、ありそうでなかったんですね」「冒頭のany
やall
みたいなのは簡単にメソッドにできるんですけど、CTEだと以下のようにWITH
句が先行するので↓、単純に#from
の引数などを使って書き換えたりできないんですよ: でもこのgemでやっているということは可能なのか」
WITH "highly_liked" AS (SELECT "profile_ls".* FROM "profile_ls" WHERE (likes >= 300))
SELECT "users".*
FROM "users"
JOIN highly_liked ON highly_liked.user_id = users.id
「どうやって実現してるんだろう?」「リポジトリを掘ると、このあたり↓でWITH
を増やしてますね」「merge_ctes!
やbuild_with
とか、いろいろ頑張ってる感ある」「やるな〜」
#ActiveRecordExtended/lib/active_record_extended/active_record/relation_patch.rb#7
module ActiveRecordExtended
module RelationPatch
module QueryDelegation
delegate :with, :define_window, :select_window, :foster_select, to: :all
delegate(*::ActiveRecordExtended::QueryMethods::Unionize::UNIONIZE_METHODS, to: :all)
delegate(*::ActiveRecordExtended::QueryMethods::Json::JSON_QUERY_METHODS, to: :all)
end
module Merger
def normal_values
super + [:union, :define_window]
end
def merge
merge_ctes!
super
end
def merge_ctes!
return unless other.with_values?
if other.recursive_value? && !relation.recursive_value?
relation.with!(:chain).recursive(other.cte)
else
relation.with!(other.cte)
end
end
end
module ArelBuildPatch
def build_arel(*aliases)
super.tap do |arel|
build_windows(arel) if window_values?
build_unions(arel) if union_values?
build_with(arel) if with_values?
end
end
end
end
end
「CTEの機能だけ切り出してくれる方がいいのかも?」「他の機能は単なる拡張ですし、別にあっても困らないので大丈夫ですよ」「あ、そうでしたか」「へ〜、window関数やUNIONもこのgemで使えるようですが、こういう機能が一緒に入っても構いませんし、あれば使うかもしれませんね」「なるほど」「ALLやANYのためにこういうgemを入れようとは思いませんが、このgemでないとできない機能であるCTEサポートメソッドを使えるなら入れたい」
「ActiveRecordExtended、誰もがやりたいと思っていたCTEのメソッド化を実現したのは個人的にポイント高いですね: どれ、GitHubの★ポチっちゃおう」「私もポチりました: 久々にウォッチで🌟が出せます」
というわけで🌟を進呈します。おめでとうございます!
追いかけボイス:「CTE(WITH句)自体はPostgreSQL独自のものではなく、最近のMySQLでもサポートしているんですが(SQL標準ではSQL99以降)、このGemはMySQLで使えるとは書いていないのでMySQL版のCTE Gemも探せばあるかもしれないですね」
参考: MySQL :: MySQL 8.0 Reference Manual :: 13.2.15 WITH (Common Table Expressions)
⚓ Sidekiqやgood_jobがRactor導入を構想中(Ruby on Rails Discussionsより)
I’m the author of GoodJob 13; I plan to support Ractors once Ractors are supported in Rails. I’ve also seen Mike Perham mention similarly 15 about Sidekiq.
同コメントより(by bensheldon)
good_jobは以前取り上げました(ウォッチ20200803)。
つっつきボイス:「Rails Discussionsでたまたま見かけたんですが、sidekiqの作者とgood_jobの作者がRactorを取り入れたいと発言してました: まだ構想を述べてる段階ですが」「この流れはわかりますね: この辺のgemを作ってる人はRactor使いたいと思いますよ」「Ractorやりたいでしょうね」
「ジョブのスレッドをRactor化することはできると思いますけど、Railsのジョブはいろんなものを読み込むので、Ractorによってパフォーマンスがどのぐらいよくなるかはやってみないとわからないかもしれませんね」「たしかに」「RactorはRubyのクラス変数に自由にアクセスできなかったと思うので、Ractorを使うにはgemの改造が必要になるのかもしれないとちょっと思いました」「ふ〜む」
参考: ruby/ractor.md at master · ruby/ruby
⚓ その他Rails
つっつきボイス:「先週StimulusReflexの記事↓を書いてくれたWebチームのebiさんが読みたそうかと思って貼ってみました」
前編は以上です。
バックナンバー(2021年度第1四半期)
週刊Railsウォッチ(20210113後編)Ruby 3.0 Ractor解説記事、Vercelホスティングサービス、教育用OS xv6ほか
今週の主なニュースソース
ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。