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

週刊Railsウォッチ: フォームヘルパーの改修、Railsの監査ログgem比較、DHHとimport-mapほか(20211129前編)

こんにちは、hachi8833です。

週刊Railsウォッチについて

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

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

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

以下の公式情報から見繕いました。

🔗 フォームヘルパーでurl: falseaction: falseオプションが指定可能になった

以下を用いて<form>要素のレンダリングをaction属性なしで行えるようになった。

  • form_with url: falseまたはform_with ..., html: { action: false }
  • form_for ..., url: falseまたはform_for ..., html: { action: false }
  • form_tag falseまたはform_tag ..., action: false
  • button_to "...", falseまたはbutton_to(false) { ... }

Sean Doyle
同Changelogより


つっつきボイス:「久しぶりにform_*系ヘルパーに改修が入ったようです」「この改修ではRailsのフォームヘルパーでurl: falseaction: falseを明示的に指定することでアクションなしを指定できるようになったんですね: 現在のRailsのデフォルトではaction属性が必ず付与されるようになっていますが、HTMLの仕様ではaction属性なしでフォームを生成すると現在のURLが生成されて自分自身へのアクションを参照するようになっています」「フォームを自分自身に投げるならaction属性を書かなくてもいいんですね」

参考: HTML Standard -- 4.10.21.3 Form submission algorithm html.spec.whatwg.org

  1. If action is the empty string, let action be the URL of the form document.
    html.spec.whatwg.orgより

「今までは以下のように<form method="get">をアクションなしで作りたくてもform_withform_forなどではデフォルトでurl_for({})が使われるので作る方法がなかったということみたい」「たしかに、今まではform_withform_forを使うと必ずaction="/postsみたいなアクションが<form>タグに入ってたので、アクションなしのフォームもありとは知りませんでした」「この機能を自分で使う状況はあまり思いつかないけど、HTMLの仕様で許されていることができないとRailsの自由度が下がってしまうことになるので、できる方がいいでしょうね👍」

<form method="get">
  <button name="sort" value="desc">Most to least</button>
  <button name="sort" value="asc">Least to most</button>
</form>

Rails 5.1〜7.1: 'form_with' APIドキュメント(翻訳)

🔗 button_toauthenticity_token:オプションをサポート

form_withform_forの呼び出しでauthenticity_token:オプションを渡せるようになった。

button_to "Create", Post.new, authenticity_token: false
  # => <form class="button_to" method="post" action="/posts"><button type="submit">Create</button></form>

button_to "Create", Post.new, authenticity_token: true
  # => <form class="button_to" method="post" action="/posts"><button type="submit">Create</button><input type="hidden" name="form_token" value="abc123..." autocomplete="off" /></form>

button_to "Create", Post.new, authenticity_token: "secret"
  # => <form class="button_to" method="post" action="/posts"><button type="submit">Create</button><input type="hidden" name="form_token" value="secret" autocomplete="off" /></form>

同PRより


つっつきボイス:「authenticity_token:オプションでtruefalseを渡したり、任意の文字列を渡したりできるようになった」「trueや文字列の場合は<input type="hidden">の中にトークンが埋められるんですね」「このオプションがbutton_toヘルパーで使えるようになったので、フォームの中に含まれていない単体のボタンでもトークンが使えるようになる: これを使いたくなる気持ちはちょっとわかるかも」

🔗 field_nameビューヘルパーが追加

field_nameビューヘルパーを導入する。これはFormBuilder#field_nameに相当する。

form_for @post do |f|
  f.field_tag :tag, name: f.field_name(:tag, multiple: true)
  # => <input type="text" name="post[tag][]">
end

Sean Doyle
同PRより


つっつきボイス:「今回はフォーム周りの改修が多いですね」「お〜、フォームのフィールドでネステッドな連想配列形式のフィールド名を指定する公式の方法がついにできた🎉(シンボル渡しだとtag[hoge][]のようにできなかった)」「しかもmultiple: trueにも対応しているのが偉い!」

# 同PRより
text_field_tag :post, :title, name: field_name(:post, :title, :subtitle)
  # => <input type="text" name="post[title][subtitle]">

text_field_tag :post, :tag, name: field_name(:post, :tag, multiple: true)
  # => <input type="text" name="post[tag][]">

form_for @post do |f|
  f.field_tag :tag, name: f.field_name(:tag, multiple: true)
  # => <input type="text" name="post[tag][]">
end

「今までは以下のような書き方↓しかなかったのがイケてないな〜と思いながら使っていましたけど、改修後はようやくname: field_name(:post, :title, :subtitle)のようなRubyらしい方法でフィールド名を指定できるようになった」「今回のフォーム周りのプルリクを投げてくれたSean Doyleさんは自分たちがビューで欲しいものをわかってくれている感じで嬉しいです」「覚えてたら使おうっと」

# api.rubyonrails.orgより
text_field_tag 'name'
# => <input id="name" name="name" type="text" />

text_field_tag 'query', 'Enter your search query here'
# => <input id="query" name="query" type="text" value="Enter your search query here" />

text_field_tag 'search', nil, placeholder: 'Enter search term...'
# => <input id="search" name="search" placeholder="Enter search term..." type="text" />

text_field_tag 'request', nil, class: 'special_input'
# => <input class="special_input" id="request" name="request" type="text" />

text_field_tag 'address', '', size: 75
# => <input id="address" name="address" size="75" type="text" value="" />

text_field_tag 'zip', nil, maxlength: 5
# => <input id="zip" maxlength="5" name="zip" type="text" />

text_field_tag 'payment_amount', '$0.00', disabled: true
# => <input disabled="disabled" id="payment_amount" name="payment_amount" type="text" value="$0.00" />

text_field_tag 'ip', '0.0.0.0', maxlength: 15, size: 20, class: "ip-input"
# => <input class="ip-input" id="ip" maxlength="15" name="ip" size="20" type="text" value="0.0.0.0" />

🔗 Rails標準のエラーレポートインターフェイスを追加

修正: #43472

このレポーターはExecutorにあるが、このRailsモジュールはもっと便利なRails.errorというショートカットを提供している。

使いやすさのため、2個のブロックをベースとする専用メソッドを公開している。

handleは、エラーを飲み込んでサブスクライバに転送する。

Rails.error.handle do
  1 + '1' # raises TypeError
end
1 + 1 # これは実行される

recordは、エラーをサブスクライバに転送するが、コールスタックを巻き戻して継続させる。

Rails.error.record do
  1 + '1' # raises TypeError
end
1 + 1 # ここは実行されない

ブロックベースのAPIに合わない場合は、低レベルのreportメソッドを使える。

Rails.error.report(error, handled: true / false)

インターフェース
このプルリクではRails.errorのみを導入したが、後で「ローカル」エラーレポーターを導入してctive SupportやActive Recordなどのgemでもエラーをレポートできるようにしたい。現在のRails.loggerと少し似た感じで動くだろう。

例: ActiveSupport.errorはデフォルトではエラーをログ出力するだけのエラーレポーターだが、RailtieでこれをRails.errorに置き換える。


つっつきボイス:「Rails.errorというショートハンドでエラーレポートできるようにしたということのようですね」「ActiveSupport:: ErrorReporterが追加されてる」

「考えてみればログ出力とエラーレポートは別に指定できる方がいいですね: 今までは自分でraiseしてrescue_fromするとか、Slack通知などを各自が実装しますけど、こういうふうにRails公式のインターフェイスでエラーレポートを使えると便利でしょうね」「Rails.errorだとraiseとかを書かなくてよくなりそうですね」「そうそう」

「このActiveSupport::ErrorReporterと機能が近いのはActiveSupport::Instrumentationですが、前者はエラーレポートに特化したインターフェースを持ち、後者は汎用的なpub/subで、subscriberがいなければスルーされるという点で使い分けが想定されてる感じかな」

参考: Active Support の Instrumentation 機能 - Railsガイド

「このインターフェイスを使って、airbrakeやsentryやrollbarといったエラーレポートツールも今後共通化できそうかなと思いました: それぞれの*-reporterみたいなgemやSlack-reporterライブラリなどをリリースしたりして」「そうなるといいですね」

airbrake/airbrake-ruby - GitHub

getsentry/sentry-ruby - GitHub

rollbar/rollbar-gem - GitHub

🔗 jbuilderのコレクションレンダリングが高速化


つっつきボイス:「おぉ、コレクションのレンダリングが高速化されるのは嬉しい🎉」「cached: trueをオンにするとキャッシュが効くのね」「アプリケーションの性質次第では速くなりそう」

「でもこれよく見たらjbuilder gemの改修ですね」「jbuilder使ってない...」「私も...」

🔗 jb gemは優秀

「自分は最近jbuilderの代わりにこのjbというgemを使ってます↓」「amatsudaさんのgemだ」

amatsuda/jb - GitHub

# amatsuda/jbより
# app/views/messages/show.json.jb

json = {
  content: format_content(@message.content),
  created_at: @message.created_at,
  updated_at: @message.updated_at,
  author: {
    name: @message.creator.name.familiar,
    email_address: @message.creator.email_address_with_name,
    url: url_for(@message.creator, format: :json)
  }
}

if current_user.admin?
  json[:visitors] = calculate_visitors(@message)
end

json[:comments] = @message.comments.map do |comment|
  {
    content: comment.content,
    created_at: comment.created_at
  }
end

json[:attachments] = @message.attachments.map do |attachment|
  {
    filename: attachment.filename,
    url: url_for(attachment)
  }
end

json

「jbだとRubyっぽく書けるのが嬉しいんですよ😂」「わかります、jbuilderの書き方はDSL的ですよね」「それそれ、jbuilderで思ったとおりのJSONやXMLを出力しようと思ったらDSLの書き方を覚えないといけないんですよ」「Response.jsonとかxmlを組み立てていて細部の挙動が思うようにならないと、ERBで書く方がましという気持ちになったりしますよね」

参考: Response.json\() - Web API | MDN

「jbだとRubyで書いたとおりにJSONが出力されるのでホント楽」「ちょっと大きいけど、active_model_serializers gemも比較的そういう感じで書けるところが好き」「キャッシュみたいなものはDSLに任せたいけど、JSONみたいなものはRubyらしいシンプルな方法でビルドしたい気持ちです」

rails-api/active_model_serializers - GitHub

🔗Rails

🔗 rails_multisite: Discourseから切り出されたマルチテナントgem(Ruby Weeklyより)

discourse/rails_multisite - GitHub


つっつきボイス:「マルチテナント系のgemはいろいろありますけど、これはDBも含めて完全に切り分けるマルチサイトがやれるライブラリのようですね」

# 同リポジトリより
mlp:
  adapter: postgresql
  database: discourse_mlp
  username: discourse_mlp
  password: applejack
  host: dbhost
  pool: 5
  timeout: 5000
  host_names:
    - discourse.equestria.com
    - discourse.equestria.internal

drwho:
  adapter: postgresql
  database: discourse_who
  username: discourse_who
  password: "Up the time stream without a TARDIS"
  host: dbhost
  pool: 5
  timeout: 5000
  host_names:
    - discuss.tardis.gallifrey

「そこまで分けるなら別アプリにするかなとも思いますが、Discourseでそういう需要があるのは何となく想像できる」「いずれ別アプリに分けたいというリクエストが来るかもしれませんね」「レンタルサーバーでよくある共有サーバープランから専用サーバープランに移行するような感じでアプリを切り離せるんじゃないかな」

🔗 Railsの監査ログgemを比較する(RubyFlowより)


つっつきボイス:「監査ログ機能のgemを比較する記事だそうです」「知っているのや知らないのや、いろんなのがありますね」「記事の末尾で使い分けが書かれていました」「自分はpaper_trailを使ってます」「監査ログ機能は常に何らかの形で求められる機能なので、こういうふうに定期的にまとめてくれるのはいいですね👍」

paper-trail-gem/paper_trail - GitHub

collectiveidea/audited - GitHub

chaps-io/public_activity - GitHub

palkan/logidze - GitHub

rails-engine/audit-log - GitHub

🔗 ログの置き場所

「ただ、監査ログについてはなるべくCloudWatch↓のような外部サービスにおまかせしたい気持ちがあります」「たしかに」

参考: Amazon CloudWatch(リソースとアプリケーションの監視と管理)| AWS

「後でログを調べたりするだけならSQLクエリでさっと取り出せるのは一見便利ですが、アプリのデータベースに監査ログを置くとものすごく量が増える可能性があるのと、作業者がセンシティブなデータを不用意にSELECT *してしまう可能性があるんですよ」「そうそう、データベースのインスタンスは別にしておきたいです」「直近のログだけならまだしも、少なくとも永続化するログを同じデータベースに置くのは避けたい」「古くなったログをexpireするのも忘れないようにしないと」

「ちなみにPostgreSQLやBigTableだと自動的にテーブルをパーティショニングするオプションがありますけどね」「MySQL派ですけど、ぽすぐれにそんな機能もあるんですか」「ググってみるとMySQLにもありますね」

参考: PostgreSQLドキュメント 5.11. テーブルのパーティショニング
参考: BigTable - Wikipedia
参考: MySQL :: MySQL 5.6 リファレンスマニュアル :: 19.6 パーティショニングの制約と制限

「なおlogidzeは翻訳記事↓でも取り上げたgemですが、ウクライナ語なので読み方をいつも忘れてしまいます😅」

Rails: Logidze gemでActive Record背後のPostgreSQL DB更新をトラッキング(翻訳)

🔗 Dry-monadsで「Railway指向プログラミング」設計(RubyFlowより)


つっつきボイス:「以前取り上げたRailway指向プログラミング(ROP)の図↓がこの記事に出てきたので拾ってみました(ウォッチ20200302)」



同記事より

「コードを見た感じでは特に変わったことをしているわけではなさそうかな↓」

# 同記事より
def deliver_car(year, model, color, city)
  yield check_year(year)
  yield check_model(model)
  yield check_city(city)
  yield check_color(color)

  Success("A #{color} #{year} Toyota #{model} will be delivered to #{city}")
end

def check_year(year)
  year < 2000 ? Failure("We have no cars manufactured in year #{year}") : Success('Cars of this year are available')
end

def check_model(model)
  @available_models.include?(model) ? Success('Model available') : Failure('The model requested is unavailable')
end
def check_color(color)
  @available_colors.include?(color) ? Success('This color is available') : Failure("Color #{color} is unavailable")
end

def check_city(city)
  @nearby_cities.include?(city) ? Success("Car deliverable to #{city}") : Failure('Apologies, we cannot deliver to this city')
end

「Railway指向とは?」「まさに上の図のようにステップごとにポイント切り替え的に処理を進めるという考え方ですね: ここでは失敗部分の処理を共通化するのに使っているように見える」「おぉ?」「よくあるオブジェクト指向的な設計だと、失敗ごとに別々の例外を投げることでエラーオブジェクトも別々になったりしますけど、この記事では失敗時はすべてFailureに流れる、つまり図で言うと赤い線路に流れることで共通化するということなんでしょうね」「あ、そういうことですか」

「この場合失敗の理由に応じたエラーオブジェクトは取れなくなりますが、処理はシンプルになりますね: 分岐がたくさんあって、失敗の理由に興味がない場合は、こういう設計にすることもあるでしょうね」「なるほど」「あくまで設計思想のひとつです」

「この記事はdry-monadsを使ってモナドっぽく書いているらしい」「dry-rbシリーズは地味にラインナップが増えていますね」「dry-rbシリーズはなかなかいいライブラリなので、この調子で広まって今後公式にも反映されたりしたらいいですよね」

dry-rb/dry-monads - GitHub

🔗 エンジニア3年目の人に向けて


つっつきボイス:「koicさんの記事にもあるように、Railsエンジニア3年目ぐらいの人を対象にした題材で銀座Rails#39に登壇いただきました: とてもいい話❤️」「お〜、スライドを見た感じでもよき話なのが伝わってきます」「最初の頃はタスクを拾うだけで一杯になりますけど、3年目ぐらいになってくるともっといい設計にすることに目が向くようになりますよね」

「koicさんの話を聞いていて、こういう話は時代が移っても比較的変わりにくいなと思いましたね」「たしかに10年前もこういうことしていたなという感じはありますね」「技術の移り変わりは激しいけど、変わりにくい部分はやはりあると改めて実感します」

🔗 DHHの『JavaScriptのバンドルとトランスパイルが不要なモダンWebアプリ』

つっつきボイス:「いい記事👍」「ruby-jp Slackで『DHHが日本語で記事書いたのかと思った』と話題になってました」「ないない😆」

「DHHがimport-mapの普及に力を入れているのを見ていると、import-mapがすべてのブラウザで使えるようになって、Rails以外のところでもフロントエンジニアがimport-mapを使いまくる世界を目指しているのかなと思いましたね」「あ、そうかも」

「caniuse.comで言うと、一日も早くすべてのブラウザでimport-mapが緑色になって欲しいと願っているんじゃないかなと思っています↓」「なるほど、今はChromeとEdgeあたりは緑か」「今のEdgeのレンダリングエンジンはChromeと同じなので緑なのは当然ですが」「FirefoxとSafari、頑張ってくれ〜」

Data on support for the import-maps feature across the major browsers from caniuse.com

参考: Import maps | Can I use... Support tables for HTML5, CSS3, etc

「DHHの記事によるとes-modules-shims↓を使えば今のFirefoxやSafariでもimport-mapのほとんどの機能を使えるらしい」

guybedford/es-module-shims - GitHub

「FirefoxとSafariでもimport-mapが使えるようになったらproductionで安心して使えますよね」「たぶんそれがDHHの目指す世界だと思っています」

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


前編は以上です。

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

週刊Railsウォッチ: Ruby Struct入門、書籍『進化的アーキテクチャ』、AWS Web問題集ほか(20211116後編)

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

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

Rails公式ニュース

Ruby Weekly

RubyFlow

160928_1638_XvIP4h


CONTACT

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