Quantcast
Channel: Ruby on Railsの記事一覧|TechRacho by BPS株式会社
Viewing all 1405 articles
Browse latest View live

週刊Railsウォッチ(20210517前編)Bootstrap 5リリース、productionでSQLiteがwarning表示、rails-ujsの舞台裏ほか

$
0
0

こんにちは、hachi8833です。

週刊Railsウォッチについて

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

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

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

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

🔗 CSPヘッダーのサポートを2つ追加


つっつきボイス:「require-trusted-types-fortrusted-types、知らないヘッダーだ」「issue #42034を見ると、MDNでexperimentalとなってるそうです」「これらはサーバー側が返すヘッダなので、Railsが対応してくれることでexperimentalな最新の機能を利用できるということですね」

参考: CSP: require-trusted-types-for - HTTP | MDN
参考: CSP: trusted-types - HTTP | MDN

# actionpack/lib/action_dispatch/http/content_security_policy.rb#L108
    MAPPINGS = {
      self:           "'self'",
      unsafe_eval:    "'unsafe-eval'",
      unsafe_inline:  "'unsafe-inline'",
      none:           "'none'",
      http:           "http:",
      https:          "https:",
      data:           "data:",
      mediastream:    "mediastream:",
      blob:           "blob:",
      filesystem:     "filesystem:",
      report_sample:  "'report-sample'",
      strict_dynamic: "'strict-dynamic'",
+     script:           "'script'",
      ws:             "ws:",
      wss:            "wss:"
    }.freeze

    DIRECTIVES = {
      base_uri:        "base-uri",
      child_src:       "child-src",
      connect_src:     "connect-src",
      default_src:     "default-src",
      font_src:        "font-src",
      form_action:     "form-action",
      frame_ancestors: "frame-ancestors",
      frame_src:       "frame-src",
      img_src:         "img-src",
      manifest_src:    "manifest-src",
      media_src:       "media-src",
      object_src:      "object-src",
      prefetch_src:    "prefetch-src",
+     require_trusted_types_for:  "require-trusted-types-for",
      script_src:      "script-src",
      script_src_attr: "script-src-attr",
      script_src_elem: "script-src-elem",
      style_src:       "style-src",
      style_src_attr:  "style-src-attr",
      style_src_elem:  "style-src-elem",
      worker_src:      "worker-src"
      script_src:                 "script-src",
      script_src_attr:            "script-src-attr",
      script_src_elem:            "script-src-elem",
      style_src:                  "style-src",
      style_src_attr:             "style-src-attr",
      style_src_elem:             "style-src-elem",
 +    trusted_types:              "trusted-types",
      worker_src:                 "worker-src"
    }.freeze

参考: Rails アプリケーションのセキュリティ対策(CORS/CSP/HSTS)

🔗 SQLiteをproductionで使うとwarningを出力

SQLiteをproductionで実行するとwarningをログ出力する。
warning出力はconfig.active_record.sqlite3_production_warning=falseで無効にできる。
closeするissue: #34715
同PRより


つっつきボイス:「SQLiteをproductionで使うとwarningを出すようになったそうです」「アプリの性質によってはSQLiteをproductionで使う可能性もなきにしもあらずですし、warning出すほどでもない気はしますけどね」「#34715のコメントには『自分はproductionで問題なくSQLite使ってるよ』という書き込みもありました」「コンフィグでwarning出さないようにできるのか」

参考: SQLite Home Page

「現実問題としてSQLiteをproductionで使う可能性はかなり低いと思うので、多いのは『間違ってSQLiteを使ってしまう』ケースでしょうね」「それありそうですね」

参考: SQLiteと他のデータベースの違いを紹介します。「SQLiteの特徴を教えてください」 - Python学習チャンネル by PyQ

ですが、SQLiteができるロックはそれほど柔軟でないのが特徴です。ロックする範囲をデータベース全体でしか制御できない(テーブル単位、行単位でのロックができない)特徴を持っています。
また、SQLiteはローカルのファイルを直接データベースとして扱うので、複数のサーバーとネットワークごしに接続するのには不向きです。Webサーバーを複数台起動する場合などはMySQLやPostgreSQLなどを使うほうが良いでしょう。
blog.pyq.jpより

「データベースの内容がユーザーから一切変更できないようなアプリなら、そうしたデータの保存にSQLiteを使うこともありますし、Androidのゲームアプリなんかでは、SQLiteのファイルをソフトウェア本体にbundleしてSQL互換アクセスできるread onlyなマスタデータベースとして使うなんてこともありますよね」「なるほど」「特に最近のRailsはマルチプルデータベースに対応しているので、変更が生じないマスターデータについてはSQLiteに入れるという選択肢はありでしょうね」「たしかに、私のenno.jp↓というアプリもデータ更新は管理者しか行わないので本当はSQLiteにしたかったんですけど、HerokuではSQLiteを利用できない仕様になってました😢

日本語エラーチェックサイトenno.jpを作った理由

「SQLiteは読み出し専用で使っている分には決して遅くありませんし、SQLiteのようなファイルベースのDBにはひとついい点があるんですよ: データがすべてOSのファイルキャッシュに乗ってくれること」「あぁそうか!」「OSのファイルキャッシュに乗るということは、プロセスをまたいでもキャッシュが効くということで、これが大きい」「そうそう😋」「RailsのSQL キャッシュは基本的にアクション単位なのでアクションをまたぐと解放されてしまいますが、OSのファイルキャッシュはプロセスをまたいで効くのが強い」

参考: 1.8 SQLキャッシュ — Rails のキャッシュ機構 - Railsガイド

「個人的にはproductionでのSQLiteでwarningを出すまでもないような気はしますけど、誤用を防ぐという意味では理解できます」「そうですね、理解したうえでSQLiteをproductionで使う人は自分でwarningをオフにできるでしょうし、それと誤用の可能性を秤にかけた結果この改修になったんでしょうね」「自分も本番でSQLiteを使うときは、よ〜く考えてからにしています」


SQLiteをRailsのproduction環境で使う主な問題は、

  • これがデフォルトになっているので初心者が気づかないまま使ってしまう可能性があること
  • SQLiteがディスクベースのデータベースでありながら、それを過渡的なクラウドベースの世界で使ってしまうこと

「自分のRailsアプリでは明確な意図を持ってSQLiteをproductionで使います」勢が多数派とは思えない。
#34715コメント(by DHH)より大意

🔗 has_one through:disable_joinsオプションが追加


つっつきボイス:「マルチプルデータベース関連でこれとよく似た改修がこの間もありましたね(ウォッチ20210426): あのときはhas_namy through:が対象だったかな」「今回はhas_one through:でもdisable_joinsが使えるようになったんですね」

#41937has_many ... through:が追加されたように、has_one ... through:リレーションにdisable_joinsオプションを追加する。目的は、マルチプルデータベースのRailsアプリでテーブルが別のデータベースクラスタに置かれている場合にhas_oneリレーションシップを使えうようにすること。この場合、クラスタ間でのJOINは行えないのでJOINを回避する必要がある。
同PRより大意

🔗 PostgreSQLでtimestamp with time zoneをネイティブでサポート


つっつきボイス:「お、PostgreSQLでタイムゾーンありのタイムスタンプを使えるtimestamptzが追加されたんですね」「デフォルトはタイムゾーンなしの方なのか」

  • PostgreSQL: ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.datetime_typeを導入
    この設定は、マイグレーションやスキーマでdatetimeを呼び出す際に、Active Recordで使うネイティブ型を制御する。この設定は、設定済みのNATIVE_DATABASE_TYPESのいずれかに対応するシンボルを受け取る。デフォルトは:timestampつまり、マイグレーションで、t.datetimeを使うと「timestamp without time zone」カラムが作成される。「timestamp with time zone」を使うには、イニシャライザで:timestamptzに変更する。
    これを変更した場合には、bin/rails db:migrateを実行してschema.rbを再構築する必要がある。
    Alex Ghiculescu
    同Changelogより大意

「たしかに、これまではPostgreSQLのtimestampを使うとタイムゾーンなしになってたんですよ」「タイムゾーンありの方が好きかも」「timestamptzという名前が微妙に長いけど、タイムゾーンのありなしを選べるようになったのはいいですね👍

🔗 Active SupportのEnumerable#sumArray#sumを非推奨化


つっつきボイス:「Active SupportにあるEnumerable#sumArray#sumが非推奨になったのね」「RubyネイティブのEnumerable#sumArray#sumの方が断然高速なので今後はそちらを使って欲しいそうです」「お〜、Active Supportのメソッドが昇格してRuby本体に取り入れられることがときどきありますけど、これもそのパターンですね」「Rails側としては冥利に尽きますね🎉

Ruby(2.4以降)にはパフォーマンスが著しく向上したネイティブのsum実装が含まれている。Rails 7.1からはEnumerable#sumArray#sumを削除してRubyのネイティブ実装を使うことにする。
このコミットでは、適切な初期引数を持たない非数値引数の呼び出しに非推奨の警告を追加している。以下で議論しているとおり、RailsからActive Support版の機能が削除されたときにこれらの引数が必要になる。

#41669 (comment)

以下のような例でdeprecation warningが表示されるようになった。

%w[foo bar].sum
[[1, 2], [3, 4, 5]].sum

warningを回避するには以下のように呼び出す必要がある。

%w[foo bar].sum('')
[[1, 2], [3, 4, 5]].sum([])

Rails 7.1での非推奨化を準備するため、Rubyのネイティブ実装でTypeErrorになる[nil].sum == 0についても非推奨化する。
同PRより大意

🔗Rails

🔗 Bootstrap 5がリリース


つっつきボイス:「そうそう、Bootstrap 5出ましたね」「IEサポートもついに終了」「今年の春学期に担当する大学の授業では検索のしやすさからBootstrap 4を使いますが、来年度やるならBootstrap 5でもいいかもですね」

「jQueryも必須でなくなったのはいい👍」「jQueryの目玉機能だった$("要素名")みたいなセレクタも今やquerySelectorAll()でできるようになりましたよね」「う、jQueryなしでできるようになってたとは知らなかった😅」「querySelectorAll()は普通にどのメジャーなブラウザでも使えるはずですよ」

参考: Document.querySelectorAll() - Web API | MDN
参考: Can I use... Support tables for HTML5, CSS3, etcquerySelectorAll()

「jQueryが当時画期的だったのは、$("")を1個書くだけでCSSセレクタでDOM要素を簡単に選択できたこと」「あれは感動的なインターフェイスでしたね」「jQueryのeach()的なメソッドも当時のブラウザでは標準で使えなかった」「現在のJavaScriptがそうしたものを標準として取り入れるようになったので、jQueryは役目を終えつつあるかなと感じますね」「今の人があの当時のJavaScriptを触ったらあまりに別物でびっくりするかも」

参考: .each() | jQuery 1.9 日本語リファレンス | js STUDIO

🔗 Hanami 2.0.0 alpha2がリリース(Ruby Weeklyより)


つっつきボイス:「おぉ、Hanamiの2.0.0が出そう」「コアを完全に書き直したそうです」「まったく新しいアプリをSinatraで書くぐらいならHanamiで書いてもいいかなという気持ちがありますね」「Hanamiのルーティングにroda gemを使うという提案がだいぶ前にあったのを思い出したんですが、結局ルーターもHanami独自のものを新たに作ったそうです」「ルーターはWebアプリで必ず通るところなので重要ですね: ここが重いと全体が重くなってしまうので」「たしかに」

jeremyevans/roda - GitHub

🔗 RSpecのネスト


つっつきボイス:「RSpecはsubjectletを使うかどうかみたいな議論になりがちなところがあるので、今はMinitestの方が好きです」「わかります」「RSpecだとネステッドなcontextを書きすぎてしまうんですよね」

「RSpecで書かれたテストが複雑になりすぎると、RSpecのテストに対するテストが必要なんじゃないかとすら思えるときもあります」「RSpecで頑張って抽象度を高めるほどそうなりがちかも」「テストコードに不安を抱えるぐらいならMinitestでベタに書きたい派」「RSpecはテストをきれいに書きたくなるところがあるんですけど、本来やりたいのはアプリケーションコードが正しく動作していることを確認することだという気持ちが強まって、今はMinitestで書いてます」

参考: Rails テスティングガイド - Railsガイド

RSpecえかきうた

🔗 Rails UJSの舞台裏(Ruby Weeklyより)


つっつきボイス:「RailsのUJS(Unobtrusive JavaScript: 控えめなJavaScript)についてかなり詳しく解説しているようです」

参考: Rails で JavaScript を使用する - Railsガイド

「RailsでAjaxコードを書くようになるとUJSの理解が重要になってきますね: この記事のようにremote: trueから追いかけ始めて、data-method属性のようなdata-系属性をRailsで書けるのはなぜかを調べたりするようになりますよ」「ふむふむ」

# 同記事より
link_to "Example", "#", remote: true
=> "<a href='#' data-remote>Example</a>"
# 同記事より
Rails.handleMethod = (e) ->
  link = this
  # gets the method provided
  method = link.getAttribute('data-method')
  return unless method
  ...

参考: Rails学習者にrails-ujsの動作説明したら感動された話 - INODEVLOG

「rails-ujsのコードはそんなに大きくないはずですし、jQueryがRailsから外されたことで以前よりもシンプルになっていて読みやすいので、一度読んで見ることをおすすめします🎓」「お〜!」


前編は以上です。

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

週刊Railsウォッチ(20210511後編)AWS Lambda関数ハンドラをDSLで書けるyake gem、VPC Peeringが同一AZ転送量無料化ほか

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

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

Rails公式ニュース

Ruby on Rails Discussions

Ruby Weekly

StatusCode Weekly

statuscode_weekly_banner

The post 週刊Railsウォッチ(20210517前編)Bootstrap 5リリース、productionでSQLiteがwarning表示、rails-ujsの舞台裏ほか first appeared on TechRacho.


週刊Railsウォッチ(20210518後編)RubyのGCを深掘りする、Psych gemのbreaking change、11月のRubyConf 2021ほか

$
0
0

こんにちは、hachi8833です。

週刊Railsウォッチについて

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

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

🔗Ruby

🔗 RubyのGCを深掘り


つっつきボイス:「RubyのGC(Garbage Collection)をDeep Dive、いいですね👍」「ちょうど先週のウォッチで追加コメントしたObjectSpaceIMEMOなどもRubyのメモリ管理やGCと関連しますね(ウォッチ20210510)」「あ、なるほど」「図が整ってて見やすいですね」「こういう記事は図が大事」

参考: ガベージコレクション - Wikipedia


同記事より

「ヒープが断片化すると、空きヒープ容量が同じでも新しいオブジェクトが入り切らないことがある↓: これはRubyの例ですが、OSでもまったく同じことが起きます」


同記事より

「このような言語のGCとかメモリ管理を勉強するときは、先に以下のような書籍でOSの基本的なメモリ管理を学んでからにするとよりわかりやすいと思います」「たしかに!」

「そもそもCRubyの実行系はVM(Virtual Machine)で、VMもOSのようなメモリ管理を行っているので、理解のためにはOSから学ぶ方がむしろ近道でしょうね: たとえばRubyの内部データアラインメントにはOSやCPUアーキテクチャのアラインメントも関連しているとか」「なるほど」「そういうふうにRubyのメモリ管理を理解するうえでOSのメモリ管理の前提知識が必要になってくる場面も出てきますので、OSのメモリ管理を理解してからこうした記事を読むとはかどると思います👍

参考: 第5章 ガーベージコレクション — 『Rubyソースコード完全解説』サポートページ

🔗 RuboCopがRuby 3.1のサポート対応開始

RuboCop AST 1.5はRuby 3.1をサポートするようになった#182
本PRではTargetRubyVersion 3.1をサポートする。
同PRより大意


つっつきボイス:「そうか、もう7か月後にはRuby 3.1が出るのか」「そう思うと近いですね」

🔗 Psych gemにbreaking change

ruby/psych - GitHub


つっつきボイス:「ruby-jp Slackで見かけたトピックです」「PsychというYAMLパーサーgemにあるloadメソッドが正しく使われないと脆弱になる可能性があるので、デフォルトでsafe_loadを実行するようにしようという提案か」「この変更がbreaking changesになるかもだそうです」「今まで読み込めたものが読み込めなくなるならその可能性はあるでしょうね」

「RailsでもYAMLローダーにPsychが使われていますね」「PsychはRubyの”default gem”に含まれていたと思います」

「ところでPsychっていうgemは前からちらほら見かけるんですが、何て読むんでしょう?」「あ、手元の辞書に載ってました↓」「サイク!」「サイケデリックとかサイキックとかに通じるんですね」

psych saɪk【名】《略式》=→psychology,
*【動】|他|《米略式》 …を精神分析する、…を正しい精神状態にする、…の精神を安定させる


Psych.loadは、信頼できないデータに対して使うと安全ではない。Psych.loadを誤って信頼できないデータに対して使い、ある種のセキュリティ脆弱性が生じているアプリケーションがあまりに多い。
本コミットはPsych.loadがデフォルトでsafe_loadを使うよう変更する。信頼できるデータをパースしたい場合はPsych.unsafe_loadを使える。
Psych.loadYAML.loadはRCE(Remote Code Execution)にさらされやすい。load関数そのものが問題なのではなく、システム内の他のオブジェクトを悪用してRCEを実行するのに使われてしまう可能性があることが問題。このブログ記事では、YAMLをRubyGemsと組み合わせてRCEに昇格させる方法が詳しく説明されている。信頼できないソースからYAMLを読み込もうとした場合、悪意のあるユーザーがこの問題に乗じて対象のシステムで任意のコードを実行できる可能性がある。

Psychでは、こうした攻撃に利用できないsafe_loadというメソッドも提供している。ここでの提案は、loadを実行したときにデフォルトでsafe_loadを実行するよう変更すること。当然ながら、safe_loadloadよりもずっと制約が大きいので、この変更によって既存のコードが動かなくなる可能性もある。

また、YAMLのload関数はRubyGems以外にもシステム内の他のオブジェクトで利用可能で、RCEに昇格する可能性があることについても言及しておく価値があるだろう。また、任意のオブジェクト読み込みに使えるもの(Marshal.loadなど)も同じように悪用される可能性があることも言及しておきたい。したがって、RubyGemsを変更しても別のシリアライズ方式(Marshalなど)に変更しても、この穴は塞げない。
同PRより大意


つっつきの後で確認してみました。

$ ruby -v
ruby 3.0.1p64 (2021-04-05 revision 0fb782ee38) [x86_64-darwin20]
$ gem list
...
pstore (default: 0.1.1)
psych (default: 3.3.0)
public_suffix (4.0.6)
...

上のプルリクがマージされた後だったので、gem update psychすると4.0.0にアップデートされました。

$ gem list
...
pstore (default: 0.1.1)
psych (4.0.0, default: 3.3.0)
public_suffix (4.0.6)
...

参考: bundled gem と default gem の違い - @znz blog

🔗 Rubyとボロノイ分割でアニメーション(Ruby Weeklyより)

mike-bourgeous/mb-geometry - GitHub


つっつきボイス:「ボロノイパーティションって初めて見ました」「グラフが石鹸の泡みたいに動いてる」「配置されたさまざまな点を基準にして領域を分けるアルゴリズムらしい🤔」「それをRubyでやってみたという記事なんですね」「これは楽しそう」

参考: ボロノイ図 - Wikipedia


以下はつっつき後に拾った動画です。

🔗 11月のRubyConf 2021


つっつきボイス:「今年の11/8〜11/10にコロラド州デンバーで開催が予定されているRubyConf 2021のサイトを見ると、今回はハイブリッド方式で開催するとありました」「お〜、現地でも開催されるのかな?」「詳しくはわかりませんが、対面式とバーチャルの二本立てだそうです」「今後のカンファレンスはこういうハイブリッド形式が主流になるかもしれませんね」「パンデミックも国によっては収束に向かっているところもありますし、ワクチンパスポートを持っていれば現地参加できるみたいなふうになるかも」

Q: カンファレンスが対面式とバーチャルのハイブリッドとのことですが、もう少し詳しくお願いします。
A: 詳しくは今後数か月のうちに詰めていく予定です。現時点では、対面式のカンファレンスに戻る方向で進んでいます。既にコロラド州のコンベンションセンターでは対面式のイベントが開催されていますし、米国でCOVIDワクチンが入手しやすくなったこともあり、この傾向はさらに強まると思われます。パンデミックに備えて、CDCとコロラド州保健局の安全ガイドラインに沿って、対面式イベントの形式を調整することになります。
私たちはこれまで2つのカンファレンスをバーチャルで開催してきましたが、今年のRubyConfでは両カンファレンスでうまくいった部分を統合する予定です。コミュニティメンバーの多くがコロラドに来ないことが予想されるため、対面式のイベントと並行して、できる限り最高のバーチャル体験を提供したいと考えています。
FAQより大意

参考: 英「ワクチンパスポート」 イベント会場で実証実験|テレ朝news-テレビ朝日のニュースサイト

その他Ruby

つっつきボイス:「このRuby-dev office hourというイベントを今になってruby-jp Slackで知りました」「そうそう、開発者同士が気軽に話せる場を設けようという感じで、雑談を交えて毎週開催しているらしいですよ」

つっつき後の2021/5/17の回では上述のPsychの件も話題になっていますね。


🔗DB

🔗 OracleのHeatWave


つっつきボイス:「HeatWaveはOracleのクラウドでMySQLを高速化するエンジンだそうです」「図を見た感じでは、MySQLサーバー内からプラグイン経由でHeatWaveクラスタにアクセスするようですね↓」


Understanding MySQL’s New Heatwave | Severalninesより

参考: HeatWave | オラクル | Oracle 日本

「こういうふうにセカンダリエンジンとしてHeatWave(RAPID)を指定して、SECONDARY_LOADでテーブルを読み込むのか: クエリ呼び出しではHeatWaveを特に意識しないで実行できるらしい」

# severalnines.comより
mysql> CREATE TABLE orders (id INT) SECONDARY_ENGINE = RAPID;
or
mysql> ALTER TABLE orders SECONDARY_ENGINE = RAPID;
mysql> ALTER TABLE orders SECONDARY_LOAD;

「見たところ、投機的実行っぽく両方にやらせて結果が先に返ってきた方を使うか、あるいはクエリオプティマイザが判断していずれかにクエリを投げるのかなと想像してみました🤔」「ふむふむ」

参考: 投機的実行とは - IT用語辞典 e-Words

「そういえばHeatWaveはcolumnar(列指向)なエンジンと書かれていました」「たとえばカラムを絞ってGROUP BYするような処理は、行指向DBよりも列指向DBの方が圧倒的に高速になりますね」「なるほど」

参考: 列指向データベース管理システム - Wikipedia

「こういうふうに行指向なDBと列指向なDBを両方使うことのメリットは、クエリを変更せずに高速化できることでしょうね」「たしかに」「単に自分が欲しいデータを取得するクエリを書ける難易度と、そうした欲しいデータを十分高速に最適化して取得するクエリを書ける難易度の差は結構大きいので、ある程度の高速化の部分をクエリ自体は変更せずにDBMS側で対応してくれるというのはデータ処理を頻繁に行う人たちにとってはありがたいと思います」

「カラム指向データベースはAmazon Redshiftなども含めて以前からあって、行指向だと遅いクエリがカラム指向だとものすごく高速になる(あるいはその逆)ことも以前から知られていますが、計算資源を潤沢に使えるという前提条件を付けて良いなら、HeatWaveのように両方を使うというのはなかなか理にかなっているかもしれませんね」

参考: 列指向ストレージ - Amazon Redshift


「1時間あたり4万円超えという値段にびっくりしました」「エンタープライズ方面などならその価格で買うところはあると思いますよ: 極端に言えばクエリ最適化のためにプログラマーを雇わなくても高速化できますし、結果整合性も保証されるし、デプロイも短期間で済む」「たしかに」「結局人間が一番値段が高い」

「クエリを書き換えるタイプの最適化だとテストを全面的にやり直さないといけなくなったりしますが、ミドルウェア対応だけで高速化できるならそれをしないで済みますね」「なるほど」「ちなみにRedshiftはPostgreSQLベースになっていて、基本的にはPostgreSQLクエリを変えずに使える: それと同じような感じで、HeatWaveもMySQLのクエリを基本的に変えずに高速化できるのはエンタープライズ方面にアピールしそうですね」

「ただしRedshiftのクエリはPostgreSQLと100%互換ではなく制約もあります↓: HeatWaveにももしかするとそうした制約があるかもしれませんね」

参考: Amazon Redshift and PostgreSQL - Amazon Redshift

🔗JavaScript

🔗 zx: JavaScriptでシェルスクリプトを書く

google/zx - GitHub


つっつきボイス:「はてブでバズっていたのを見つけました: JavaScriptでシェルスクリプトを書けて、async/awaitやPromiseなども使えるとか」「Rubyにもバッククォート``でシェルのコマンドを実行できる構文がありますけど、それをJavaScriptで書けるようにした感じかな」

参考: コマンド出力 — リテラル (Ruby 3.0.0 リファレンスマニュアル)

#!/usr/bin/env zx

await $`cat package.json | grep name`

let branch = await $`git branch --show-current`
await $`dep deploy --branch=${branch}`

await Promise.all([
  $`sleep 1; echo 1`,
  $`sleep 2; echo 2`,
  $`sleep 3; echo 3`,
])

let name = 'foo bar'
await $`mkdir /tmp/${name}`

「バッククォート構文でないと取れないデータもありますよね」「たとえばデータサイエンス系のツールやユニケージツールの出力をJavaScriptで組み立てて使いたい人向けかも」

参考: ユニケージ開発手法 - Wikipedia

「自分はRubyがプライマリ言語なのでzxはたぶん使わないと思いますが、JSがプライマリ言語の人にはこういうツールが便利でしょうね👍」「たしかにプライマリ言語は人によって違いますよね: この言語なら頑張れば必ずビジネスロジックを書けると確信できる言語」「そうそう」

🔗 その他JS


つっつきボイス:「短い記事ですが、ちょっとびっくりしました」「JavaScriptの配列にlengthというセッターがあるとはね〜」「面白いしわかるといえばわかりますけど、一瞬直感に反しているような気が…」「直感に反する書き方が流行ると後で読みにくくなったりすることもありますね」「たとえばTypeScriptからトランスパイルした結果にlength = 0が入るならいいけど、業務コードで直接書くのはためらうかも」

// 同記事より
const a = ['hoge', 'fuga']
console.log(a.length) // => 2

a.length = 0
console.log({a}) // => { a: [] }

🔗言語/ツール/OS/CPU

🔗 ミニLED


つっつきボイス:「次のMacBook ProのCPUはM2かM1Xか」「ここで言っているミニLEDは今度のiPadで採用されると言われているものですね↓: バックライトのLEDを領域ごとに分割してオンオフすることで消費電力を減らせる」「なるほど、黒い部分が液晶で遮られるんじゃなくて本当にその領域のバックライトをオフにするんですね」

参考: 液晶&有機ELに続く!「ミニLED」と「マイクロLED」って何? - 価格.comマガジン

いっぽうのミニLEDは、マイクロLEDと似て非なるもの。現在のところ、ミニLEDには明確な定義や規格がないのだが、おおむね液晶テレビを構成する技術の一部で、従来のバックライトを分割してエリアごとに輝度を制御する「局所輝度制御」あるいは「ローカルディミング」と呼ばれる技術の延長線上にあるものだ。バックライトの分割をより細かくすることで、画柄に合わせてよりきめ細やかな明るさ調整が行え、コントラストアップを狙える。
kakakumag.comより


「ちなみに手持ちのiPadがいい加減古くなったので新しいiPad注文しました: 届くのは7月ですけど」「いいな〜」「私もKindle Fireの昨年モデルを中古で見かけて衝動買いしちゃいました」

「そういえばリモートワークするようになって以来タブレットの利用頻度がとても上がりましたよ」「私はむしろ以前よりスマホ触るようになりましたけど」「タブレットはすごく使うようになって、逆にノートPCとスマホをほぼまったく触らなくなった」「そんなに!」


後編は以上です。

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

週刊Railsウォッチ(20210517前編)Bootstrap 5リリース、productionでSQLiteがwarning表示、rails-ujsの舞台裏ほか

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

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

Ruby Weekly

The post 週刊Railsウォッチ(20210518後編)RubyのGCを深掘りする、Psych gemのbreaking change、11月のRubyConf 2021ほか first appeared on TechRacho.

Rails 7: has_many :through関連付けにdisable_joins: trueオプションが追加(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

Rails 7: has_many :through関連付けにdisable_joins: trueオプションが追加(翻訳)

私たちはRailsアプリケーションで、マルチプルデータベースを多用しています。Rails 6からマルチプルデータベースへの接続が簡単に行えるようになりました。

ここで以下のユースケースを考えてみましょう。

  • 1人のユーザーは複数の記事(post)を作成できる
  • 記事を見ているユーザーは誰でも記事にコメントを付けられる(いわゆるcroud-sourcedなコメント)

この場合コメントが急速に増加する可能性があり、別の種類のデータ管理アプローチが必要になるかもしれないので、croud-sourcedなコメントを別のデータベースに保存することが考えられます。

この場合のdatabase.ymlは以下のようになるでしょう。

default: &default
  adapter: postgresql
  encoding: unicode
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
  password: password

development:
  primary:
    <<: *default
    database: rails_demo_development
  crowd_sourced:
    <<: *default
    migrations_paths: db/migrate_crowd_sourced
    database: rails_demo_crowd_sourced_development

モデルは以下のようになります。

# app/models/application_record.rb
class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true
  connects_to database: { writing: :primary, reading: :primary }
end
# app/models/crowd_sourced_record.rb
class CrowdSourcedRecord < ActiveRecord::Base
  self.abstract_class = true
  connects_to database: { writing: :crowd_sourced, reading: :crowd_sourced }
end
# app/models/user.rb
class User < ApplicationRecord
  has_many :posts
end
# app/models/user.rb
class Post < ApplicationRecord
  belongs_to :user
  has_many :comments
end
# app/models/comment.rb
class Comment < CrowdSourcedRecord
end

変更前

Rails 6で、あるユーザーが持つすべての記事に付けられたコメントをフェッチする場合は、以下のようにカスタムメソッドを追加することになります(データベースをまたぐ関連付けをJOINできないため)。

# app/models/user.rb
class User < ApplicationRecord
  has_many :posts

  def comments
    Comment.where(post_id: posts.pluck(:id))
  end
end

この場合、最初にユーザーが作成した記事idを取得し、続いてそれらの記事idに対応するすべてのコメントを取得しています。

> User.first.comments
  User Load (0.3ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT $1  [["LIMIT", 1]]
   (0.2ms)  SELECT "posts"."id" FROM "posts" WHERE "posts"."user_id" = $1  [["user_id", 1]]
  Comment Load (0.3ms)  SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = $1 /* loading for inspect */ LIMIT $2  [["post_id", 1], ["LIMIT", 11]]
 => #<ActiveRecord::Relation [#<Comment id: 1, post_id: 1, name: "Chantay Hayes", email: "ronna.dooley@friesen.us"...>]>

関連付けが異なるデータベース間にまたがる場合は、常に上記のようにカスタムメソッドを追加する必要があります。

変更後

Rails 7では、マルチプルデータベースのJOIN問題をエレガントに解決する方法として、has many throughの関連付けにあるオプションが追加されました(41937)。新しく追加されたdisable_joins: trueオプションは、以下のようにhas_many :through関連付けと組み合わせられます。

class User < ApplicationRecord
  has_many :posts
  has_many :comments, through: :posts, disable_joins: true
end

これで上の関連付けを読み込めば、「記事idを読み込むクエリ」と「その記事idを持つコメントを読み込むクエリ」がそれぞれ実行されます。

> User.first.comments
  User Load (0.3ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT $1  [["LIMIT", 1]]
  Post Pluck (0.3ms)  SELECT "posts"."id" FROM "posts" WHERE "posts"."user_id" = $1  [["user_id", 1]]
  Comment Load (0.3ms)  SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = $1 /* loading for inspect */ LIMIT $2  [["post_id", 1], ["LIMIT", 11]]
 => #<ActiveRecord::Associations::CollectionProxy [#<Comment id: 1, post_id: 1, name: "Chantay Hayes", email: "ronna.dooley@friesen.us", .....]>

これにより、コメントをeager loadingできるようになります。

ただし、以下のように複数のユーザーをイテレートしてコメントを読み込もうとすると、ユーザーごとに記事idをSELECTする追加のクエリも発生します。

> users = User.limit(5).includes(:comments).load
  User Load (0.2ms)  SELECT "users".* FROM "users" LIMIT $1  [["LIMIT", 5]]
  Post Load (0.2ms)  SELECT "posts".* FROM "posts" WHERE "posts"."user_id" IN ($1, $2, $3, $4, $5)  [["user_id", 1], ["user_id", 2], ["user_id", 3], ["user_id", 4], ["user_id", 5]]
  Comment Load (0.2ms)  SELECT "comments".* FROM "comments" WHERE "comments"."post_id" IN ($1, $2, $3, $4, $5)  [["post_id", 1], ["post_id", 2], ["post_id", 3], ["post_id", 4], ["post_id", 5]]
 => #<ActiveRecord::Relation [#<User id: 1, name: "Milton Dicki", email: "lyman@heller.us", created_at: "2021-04-20 17:13:20.492541000 +0000", updated_at: "2021-04-20 17:13:20.492541000...

> users.collect &:comments
  Post Pluck (0.2ms)  SELECT "posts"."id" FROM "posts" WHERE "posts"."user_id" = $1  [["user_id", 1]]
  Post Pluck (0.1ms)  SELECT "posts"."id" FROM "posts" WHERE "posts"."user_id" = $1  [["user_id", 2]]
  Post Pluck (0.1ms)  SELECT "posts"."id" FROM "posts" WHERE "posts"."user_id" = $1  [["user_id", 3]]
  Post Pluck (0.1ms)  SELECT "posts"."id" FROM "posts" WHERE "posts"."user_id" = $1  [["user_id", 4]]
  Post Pluck (0.1ms)  SELECT "posts"."id" FROM "posts" WHERE "posts"."user_id" = $1  [["user_id", 5]]
 => [#<ActiveRecord::Associations::CollectionProxy [#<Comment id: 1, post_id: 1, name: "Chantay Hayes", email: "ronna.dooley@friesen.us">.......]>]

なお、以下の回避方法を用いれば、関連する記事をメモリに読み込むコストと引き換えにこのN+1クエリの問題を解決できます。

> users = User.limit(5).includes(posts: :comments).load
  User Load (0.2ms)  SELECT "users".* FROM "users" LIMIT $1  [["LIMIT", 5]]
  Post Load (0.3ms)  SELECT "posts".* FROM "posts" WHERE "posts"."user_id" IN ($1, $2, $3, $4, $5)  [["user_id", 1], ["user_id", 2], ["user_id", 3], ["user_id", 4], ["user_id", 5]]
  Comment Load (0.3ms)  SELECT "comments".* FROM "comments" WHERE "comments"."post_id" IN ($1, $2, $3, $4, $5)  [["post_id", 1], ["post_id", 2], ["post_id", 3], ["post_id", 4], ["post_id", 5]]
 => #<ActiveRecord::Relation [#<User id: 1, name: "Milton Dicki", email: "lyman@heller.us", created_at: "2021-04-20 17:13:20.492541000 +0000", updated_at: "2021-04-20 17:13:20.492541000...

> users.collect { |u| u.posts.collect(&:comments) }.flatten
 => [#<Comment id: 1, post_id: 1, name: "Chantay Hayes", email: "ronna.dooley@friesen.us", comment: "Bad song!".....>]

関連記事

The post Rails 7: has_many :through関連付けにdisable_joins: trueオプションが追加(翻訳) first appeared on TechRacho.

Docker ComposeとDipで開発用コンテナを再利用可能にする(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

Docker ComposeとDipで開発用コンテナを再利用可能にする(翻訳)

はじめに:
Docker Composeファイルを管理しながら、最小限の労力で複数のDocker環境でコードを実行・テストする方法をご紹介します。YAML設定をいじくる時間を削減し、シンプルなコマンドを1つ実行するだけで任意のホストフォルダから指定のコンテナに取り込みましょう。本記事で紹介するRuby、Node.js、Erlangの事例をご覧いただき、ご自由に皆さんの環境に取り入れてください。

原文の免責事項

本記事(英語版)は、ベストな推奨方法を最新に保つため今後も更新を繰り返します。記事末尾のChangelogをご覧ください(原文Changelog)。

すべては1台の新しいMacから始まりました。私は商用プロジェクトに携わりながら1ダースほどの著名なオープンソースライブラリを抱えている関係上、自分のコンピュータ上でRuby、Node.js、Go、Erlangを最小限の手間で共存させる必要があります。プロジェクトAはRuby 2.6.6にロックされているかと思えば、ライブラリBはエッジ(Ruby 3.0)とレガシー(Ruby 2.5)の両方、および別実装(jrubyなど)でも動作する必要がある、といった具合です。

当然、何らかのツールで環境を管理する必要があります。rvm、nvm、rbenv、pipenv、virtualenv、asdf、gvmなどなど、この手の略語は増える一方です。そうしたツールが増えるたびにOSに「マイナー」な設定が追加され、echo $PATHの出力は画面からあふれ、ターミナルで新しいタブの読み込みに5秒もかかるというありさまです。

🔗 チャレンジを決意した

ホストOSに何もかもぶち込むのをやめて、さまざまなソフトウェアのさまざまなバージョンを個別に実行できたらどんなによいでしょう。コンテナはそんなときにうってつけです。私たちEvil Martiansのチームは、Docker黎明期から複数のサービスを必要とする複雑なプロジェクトでDocker化環境を使い続けています。

商用プロジェクトをDocker化する方法について詳しくは以下の記事をどうぞ。

クジラに乗ったRuby: Evil Martians流Docker+Ruby/Rails開発環境構築(翻訳)


そういうわけで、素のシステム設定でどこまで生産性を上げられるかを確かめるべく、私の新しいコンピュータにGitとDockerとDipだけをインストールしました。


🔗 Dockerとdipの世界に飛び込む

DockerとDocker Composeは間違いなく素晴らしいツールですが、たちまち設定で消耗するようになります。

DockerやDocker Composeには、ドキュメントを首っ引きで参照しなくても覚えられる程度のターミナルコマンドが「山ほど」あります。もちろん、それらすべてにエイリアスを設定してもよいのですが、結局.bashrc.zshrcを余計な設定で汚すことになってしまうでしょう。プロジェクトの大小にかかわらず、開発者向けのDocker設定をDockerfileやdocker-compose.ymlに全部ぶちこむのもやりすぎ感があります。

何か妙案はないものでしょうか?ありがたいことに、同僚のMisha MerkushinDipという素晴らしいツールを開発してくれました(dipは「Docker Interaction Process」の略です)。

bibendi/dip - GitHub

このツールを使えば、dip.ymlという設定ファイルを別途作成して、docker-composeの設定にあるさまざまなサービスを個別に実行する方法を思いのままに「制御」できるようになります。要するに設定可能なショートカットのリストをdip.ymlで管理するとお考えください。

$ dip ruby:latest

# 以下の長いコマンドを上のように短くできる

$ docker-compose -f ~/dev/shared/docker-compose.yml run --rm ruby-latest bash

上のdipコマンドを実行するだけで、ホストのボリューム内にマッピングされたソースコードフォルダ上で最新バージョンのRubyを実行するように設定されたLinux環境の一丁上がりです。

🔗 20個のYAMLファイルをわずか2個に

Dipで実にクールな点は、Docker Composeのショートカットを定義した柔軟なdip.ymlファイルが、PWD(カレントディレクトリ)から始まってファイルツリーのあらゆるディレクトリで探索されることです。つまり、自分のホームディレクトリにdip.ymlとdocker-compose.ymlを1個ずつ配置し、すべての共通設定をそこに保存しておけば、定型的なYAMLをプロジェクトからプロジェクトへコピペする必要がなくなります。それでは試してみましょう。最初にdip.ymlファイルを作成します。

cd $HOME
touch dip.yml
mkdir .dip && touch .dip/global-compose.yml

dip.ymlに以下を書き込みます。

# ~/dip.yml
version: "5.0"

compose:
  files:
    - ./.dip/global-compose.yml
  project_name: shared_dip_env

interaction:
  ruby: &ruby
    description: rubyサービスのターミナルを開く
    service: ruby
    command: /bin/bash
    subcommands:
      server:
        description: rubyサービスのターミナルを開く(公開ポート: 9292 -> 19292、3000 -> 13000、8080 -> 18080)
        compose:
          run_options: [service-ports]
  jruby:
    <<: *ruby
    service: jruby
  "ruby:latest":
    <<: *ruby
    service: ruby-latest
  psql:
    description: psqlコンソールを実行
    service: postgres
    command: psql -h postgres -U postgres
  createdb:
    description: PostgreSQLのcreatedbコマンドを実行
    service: postgres
    command: createdb -h postgres -U postgres
  "redis-cli":
    description: Redisコンソールを実行
    service: redis
    command: redis-cli -h redis

interactionセクションで対応付けられている各キーは、docker-composeフラグを置き換えるエイリアスとお考えください。serviceサブキーはどのDocker Composeサービスを実行するかを定義し、commandサブキーは通常のdocker-compose runに渡す引数です。今度はその強力なDocker Composeファイルを見てみましょう!

# ~/.dip/global-compose.yml
version: "2.4"

services:
  # カレントの安定版Ruby
  ruby: &ruby
    command: bash
    image: ruby:2.7
    volumes:
      # マジックはこれで全部です!
      - ${PWD}:/${PWD}:cached
      - bundler_data:/usr/local/bundle
      - history:/usr/local/hist
      # 開発エクスペリエスを向上させる
      # さまざまな設定もここでマウント
      - ./.bashrc:/root/.bashrc:ro
      - ./.irbrc:/root/.irbrc:ro
      - ./.pryrc:/root/.pryrc:ro
    environment:
      DATABASE_URL: postgres://postgres:postgres@postgres:5432
      REDIS_URL: redis://redis:6379/
      HISTFILE: /usr/local/hist/.bash_history
      LANG: C.UTF-8
      PROMPT_DIRTRIM: 2
      PS1: '[\W]\! '
      # gemfiles/*.gemfileファイルをCIでいい感じに使える
      BUNDLE_GEMFILE: ${BUNDLE_GEMFILE:-Gemfile}
    # 呪文その2
    working_dir: ${PWD}
    # よく使われる公開ポート(9292: Puma、3000: Rails)を指定
    # `dip ruby server`でこれらのポートが公開されたコンテナを実行する
    # ポート番号に1を追加している点に注意
    # 9292番はホスト側コンピュータの19292番で使えるようになる
    ports:
      - 19292:9292
      - 13000:3000
      - 18080:8080
    tmpfs:
      - /tmp
  # 別のRuby
  jruby:
    <<: *ruby
    image: jruby:latest
    volumes:
      - ${PWD}:/${PWD}:cached
      - bundler_jruby:/usr/local/bundle
      - history:/usr/local/hist
      - ./.bashrc:/root/.bashrc:ro
      - ./.irbrc:/root/.irbrc:ro
      - ./.pryrc:/root/.pryrc:ro
  # エッジ版Ruby
  ruby-latest:
    <<: *ruby
    image: rubocophq/ruby-snapshot:latest
    volumes:
      - ${PWD}:/${PWD}:cached
      - bundler_data_edge:/usr/local/bundle
      - history:/usr/local/hist
      - ./.bashrc:/root/.bashrc:ro
      - ./.irbrc:/root/.irbrc:ro
      - ./.pryrc:/root/.pryrc:ro
  # カレントのPostgreSQLフレーバー
  postgres:
    image: postgres:11.7
    volumes:
      - history:/usr/local/hist
      - ./.psqlrc:/root/.psqlrc:ro
      - postgres:/var/lib/postgresql/data
    environment:
      PSQL_HISTFILE: /usr/local/hist/.psql_history
      POSTGRES_PASSWORD: postgres
      PGPASSWORD: postgres
    ports:
      - 5432
  # カレントのRedisフレーバー
  redis:
    image: redis:5-alpine
    volumes:
      - redis:/data
    ports:
      - 6379
    healthcheck:
      test: redis-cli ping
      interval: 1s
      timeout: 3s
      retries: 30

# プロジェクト実行のたびに依存関係のリビルドを避けるためのボリューム
volumes:
  postgres:
  redis:
  bundler_data:
  bundler_jruby:
  bundler_data_edge:
  history:

Dockerのボリュームを使うようになると、ホスト上の正しいパスをどうするかにどうしても悩まされます。どういうわけか、オンラインのサンプルやチュートリアルでは、頭の中でファイルツリーをまったくたどらずに済むUnixの聖なる知恵がよく見逃されています。PWD環境変数はどんなときも、文字どおりカレントワーキングディレクトリとして評価されるのです。

この点を押さえておけば、docker-compose.ymlを(プロジェクトルートに限らず)好きな場所に保存し、${PWD}:/${PWD}:cachedという呪文を唱えさえすれば、DockerfileにどんなWORKDIR命令が書かれていてもカレントのフォルダをコンテナ内にマウントできます(私の例のようにベースイメージを使っている場合は、Dockerfileにアクセスすらできないかもしれません)。

共有Docker Composeファイルにあるサービスを利用すれば、PostgreSQLやRedisに依存するライブラリを開発できるようになります。必要なのは、コード内で環境変数DATABASE_URLREDIS_URLを使うことだけです。たとえば、以下のような感じです。

# PostgreSQLをバックグラウンドで起動する
dip up -d postgres
# データベースを作成する(createdbはdip.ymlに定義したショートカット)
dip createdb my_library_db
# psqlを実行する
dip psql
# 後はたとえばテストを実行する
# `dip ruby`は既にbashを実行しているので`-c`で引数を追加できる
dip ruby -c "bundle exec rspec"

データベースが他のコンテナと同じdocker-compose.ymlを使っているので、データベースも同じDockerネットワーク内で動きます。

PumaなどのWebサーバーを動かす場合は、serverサブコマンドでポートをホストシステムに公開できます。

$ dip ruby server -c "bundle exec puma"

Puma starting in single mode...
...
* Listening on http://0.0.0.0:9292

Webサーバーはhttp://localhost:19292でアクセスできます(ポートフォワーディング設定で9292の前に1をプレフィックスしています)。

🔗 無料ボーナス: VSCodeとの統合

VSCodeでIntelliSenseを使いたい方は、Remote Containersとの合わせ技をどうぞ。dip up -d rubyを実行して、実行中のコンテナにアタッチするだけでできます。

🔗 Ruby以外でも使える: DocsifyとNode.jsの例

Ruby以外の例も見てみることにしましょう。Docsifyというドキュメントサーバーを実行します。

Docsifyは、JavaScriptとNode.jsによるドキュメントサイト生成ツールで、私がやっているすべてのオープンソースプロジェクトでこれを使っています。Node.jsdocsify-cliパッケージのインストールが必要です。しかしDockerの他には何もインストールしないと最初にお約束しましたので、コンテナに詰め込むとしましょう。

最初に、ベースとなるnodeサービスを~/.dip/global-compose.ymlで宣言します。

# ~/.dip/global-compose.yml
services:
  # ...
  node: &node
    image: node:14
    volumes:
      - ${PWD}:/${PWD}:cached
      # Where to store global packages
      - npm_data:${NPM_CONFIG_PREFIX}
      - history:/usr/local/hist
      - ./.bashrc:/root/.bashrc:ro
    environment:
      NPM_CONFIG_PREFIX: ${NPM_CONFIG_PREFIX}
      HISTFILE: /usr/local/hist/.bash_history
      PROMPT_DIRTRIM: 2
      PS1: '[\W]\! '
    working_dir: ${PWD}
    tmpfs:
      - /tmp

グローバルな依存パッケージは、非rootユーザーのディレクトリに置くことをおすすめします↓。また、これらのパッケージをボリュームに入れて「キャッシュ」しておきましょう。

参考: docker-node/BestPractices.md at main · nodejs/docker-node

dipの設定ファイルで以下のようにNPM_CONFIG_PREFIX環境変数を定義できます。

# dip.yml
environment:
  NPM_CONFIG_PREFIX: /home/node/.npm-global

Docsifyサーバーを実行するのはドキュメントサイトにアクセスするためなので、ポートを公開する必要があります。それ用に別のサービスを定義し、サーバーを実行するためのコマンドも定義しましょう。

services:
  # ...
  node: &node # ...

  docsify:
    <<: *node
    working_dir: ${NPM_CONFIG_PREFIX}/bin
    command: docsify serve ${PWD}/docs -p 5000 --livereload-port 55729
    ports:
      - 5000:5000
      - 55729:55729

docsify-cliパッケージをグローバルにインストールするには、以下のコマンドを実行します。

dip compose run node npm i docsify-cli -g

以下のようにdip.ymlでnodeショートカットを定義しておけば、コマンドがさらにシンプルになります。

# ~/dip.yml
interaction:
  # ...
  node:
    description: Open Node service terminal
    service: node

これで、コマンドをdip node npm i docsify-cli -gと短くできました。

プロジェクトディレクトリでdip up docsifyを実行するだけでDocsifyサーバーを実行できます。

🔗 Erlangの例: アーティファクトのビルドを維持する

最後にコンパイル言語の世界の例をご紹介します。Erlangについてお話ししましょう。

これまでと同様、サービスを~/.dip/global-compose.ymlで定義し、対応するショートカットをdip.ymlで定義します。

# ~/.dip/global-compose.yml
services:
  # ...
  erlang: &erlang
    image: erlang:23
    volumes:
      - ${PWD}:/${PWD}:cached
      - rebar_cache:/rebar_data
      - history:/usr/local/hist
      - ./.bashrc:/root/.bashrc:ro
    environment:
      REBAR_CACHE_DIR: /rebar_data/.cache
      REBAR_GLOBAL_CONFIG_DIR: /rebar_data/.config
      REBAR_BASE_DIR: /rebar_data/.project-cache${PWD}
      HISTFILE: /usr/local/hist/.bash_history
      PROMPT_DIRTRIM: 2
      PS1: '[\W]\! '
    working_dir: ${PWD}
    tmpfs:
      - /tmp

# ~/dip.yml
interactions:
  # ...
  erl:
    description: Open Erlang service terminal
    service: erlang
    command: /bin/bash

既にご紹介したPWD の小技を使って、以下のように依存関係やビルドファイルを保存することもできます。

REBAR_BASE_DIR: /rebar_data/.project-cache${PWD}

これにより、デフォルトの_buildが、マウントされたボリューム内に変更されます (${PWD}のおかげで他のプロジェクトとの衝突も避けられます)。ホストへの書き込みが発生しないので、コンパイルを高速化できます(特にmacOSユーザーにとって有用です)。

🔗 最後の技: composeファイルを分割する

global-compose.ymlが育ちすぎたなと思ったら、複数のファイルに分割してサービスの「性質」に応じてグループ化できます。dipコマンドを実行すれば、すべてのファイルを探索して適切なサービスを見つけてくれます。

# dip.yml
compose:
  files:
    - ./.dip/docker-compose.base.yml
    - ./.dip/docker-compose.databases.yml
    - ./.dip/docker-compose.ruby.yml
    - ./.dip/docker-compose.node.yml
    - ./.dip/docker-compose.erlang.yml
  project_name: shared_dip_env

これで完成です。完全な設定例についてはgistをご自由にお使いいただけます。お気づきの点や他の利用例がありましたらEvil MartiansのTwitterアカウントまでお寄せください。


追伸: 実を言うと当初の「ローカルコンピュータに何もインストールしない」という計画は挫折したと認めざるを得ません。irbを手っ取り早く動かしたかったので、あきらめてbrew install ruby を実行しました(それほど頻繁には使っていませんが)。


さらに追伸: 最近GitHub Codespacesにアクセスできるようになりました。詳細の把握はまだですが、今後ライブラリ開発の筆頭候補になりそうなので、オフラインで作業しなければならない場合を除けばローカルマシンの環境整備は不要になるかもしれません(果たしてそんな時代が来るのでしょうか?)。

本記事の翻訳や転載についてのご相談は、まずメールにてお願いします。

🔗 原文Changelog

1.1.0 (2021-02-01)

  • dip ruby serverを追加。

関連記事

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

The post Docker ComposeとDipで開発用コンテナを再利用可能にする(翻訳) first appeared on TechRacho.

Rails 6: Webpacker+Yarn+Sprocketsを十分理解してJavaScriptを書く: 前編(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

タイトルは内容に即したものにしました。画像は元記事からの引用です。


  • 2020/01/16: 初版公開
  • 2021/05/13: 更新

Rails 6: Webpacker+Yarn+Sprocketsを十分理解してJavaScriptを書く: 前編(翻訳)

皆さんはアセットやJavaScript周りの変更で消耗してませんか?npm、Babel、ES6、Yarn、Webpack、Webpacker、Sprockets、これらのどこがどう違うのかさっぱりわからなかったりしますか?

Rails 6アプリケーションでJavaScriptエコシステム全体がどのように機能しているかという概念を急いで理解する必要にかられているそこのお方、あなたが探しているのはまさにこの記事です。

本記事の最後に、Rails 6プロジェクトにBootstrap 4とFontAwesome 6を追加する手順を記載しますのでご期待ください。

🔗 npm

npmは、JavaScript(正確にはNode.jsモジュール)のパッケージマネージャです。言ってみれば、JavaScript世界におけるRubygemです。

npm install <パッケージ名>

たとえばbootstrapをインストールするには以下を実行します。

npm install bootstrap

npmは、ダウンロードしたパッケージを./node_modulesに保存し、パッケージのリストを./package.jsonに保存します。

この時点では、npmとRailsがどう関係するのかについてはまだ何も説明していません。理由についてはこの先をお読みください。

🔗 yarn

yarnは、npmより新しいパッケージマネージャです。yarnはnpmのリポジトリからパッケージを取得する点は同じですが、その他の機能もあります。yarn.lockというファイルを自動生成し、必要なバージョンのnpmパッケージをそのファイルでロックします(RubyのGemfile.lockと同じようなものです)し、npmよりずっと高速です。

Rails 6アプリケーションでJavaScriptのライブラリが必要になった場合の操作は以下のとおりです。

  • 従来: JavaScriptのライブラリを提供するgemを追加し、app/assets/application.jsでライブラリをrequireします(これはSprocketsがコンパイルします)。
  • 現在: 同じ作業をyarn(https://yarnpkg.com)で行います。yarn add <パッケージ名>を実行してからrequireします(詳しくは後述します)。

原注: npmにもその後package-lock.jsonでロックする機能が追加されました。

🔗 ES6

ES6はJavaScriptの新しい標準です(JavaScriptの新バージョンと呼んでも結構です)。ES6にはクラス定義やデストラクタ、アロー関数といった極めて便利な機能が搭載されています。

さよならCoffeeScript、実は君のことがずっとキライだったよ。

訳注

CoffeeScriptはES6の仕様に影響を与えたという功績も指摘されています。

🔗 Babel

すべてのブラウザがES6を理解できるとは限らないため、ES6のJavaScriptがどんなブラウザでも動くようにするには、ES6のJavaScriptコードを読み取って旧来のES5 JavaScriptに変換するツールが必要になります。Babelはそのための変換を行うコンパイラです。

🔗 Webpack

BabelとYarn、それらの設定ファイルが揃ったら、次はアセットを自動コンパイルして環境を管理したりする手段が欲しくなります。

開発者はコードを書くこととアセットプリコンパイルの自動化だけに集中したいので、Webpackをバンドマスターとして使うことになります。Webpackはアセットを取り出して適切な個別のプラグインに渡します。プラグインは入力ファイルを適切なツールに処理させて、期待どおりの結果を得ます。

Webpackではたとえば以下のようなことができます。

  • ES6 JavaScriptコードを取り出す
  • babel-loaderプラグインでBabelにES6をコンパイルさせ、ES5 JavaScriptコードに変換する
  • できあがったpackをHTML DOMにインクルードできる形の1つのファイルにまとめる(<script type="text/javascript" src="path-to-es5-javascript-pack.js"></script>

🔗 Webpacker

Webpackerは、RailsアプリケーションにWebpackをいい感じに取り込めるgemです。Webpackerにはいくつかの初期設定ファイルが揃っているので(実際これだけで十分です)、設定のことを気にせずにコードを書き始められるようになります。

Webpackerのデフォルト設定では以下が指定されます。

  • 自分のJavaScript pack(つまりapplication.js)はapp/javascript/packs/の下に置かれること
  • ビューでJavaScript packをインクルードするにはjavascript_pack_tag '<pack名>'を使う(例: <%= javascript_pack_tag 'my_app' %>app/javascript/packs/my_app.jsをインクルードする)

本記事の最後に、これらをうまく動かせる極めて明快な例を示しますが、最初にSprocketsについて少しだけ説明しておく必要があります。

原注

他にもextract_css: falseというデフォルト設定がconfig/webpacker.ymlにあります。これは、WebpackがCSS packをstylesheet_pack_tagで提供する方法を認識していたとしても提供をオフにする設定です。本記事はJavaScriptに特化しているのでこれ以上深追いしませんが、この機能がデフォルトでオフになっていることは頭の隅に置いておくとよいでしょう。Webpackerではextract_css: falseがデフォルトの振る舞いであり、バグではないことを知っておけば、デバッグで無駄に時間を使わずに済みます。

もひとつ原注

rails assets:precompileを実行するとapp/assets/の下にあるものだけがプリコンパイルされると思っている方もいるかもしれません。実際のRailsは、app/javascript/の下にあるWebpackのアセットと、app/assets/の下にあるSprocketsのアセットを「両方」コンパイルします。

🔗 Sprockets 4

Webpackと同様に、Sprokectsも「アセットパイプライン」です。アセットパイプラインとは、アセットファイル(JavaScriptやCSSや画像など)を入力に取り、それらを処理して欲しいフォーマットを出力として生成するものです。

Rails 6から、Sprocketsに代わってWebpack(er)がRailsアプリケーションでJavaScriptを書くための新しい標準となりました。しかしSprocketsは「現在も」アプリケーションにCSSを追加するデフォルトの方法です。

Sprocketsは以下のように使われていました。

  • 従来: config.assets.precompileにある利用可能なアセットをリストするのに使われた(Sprockets 3とRails 5の時代)
  • 現在: 同じことをapp/assets/config/manifest.jsのマニフェストファイルでやるように変更された(Sprockets 4とRails 6の時代)

Sprocketsのパイプラインを用いてアセットをインクルードしたい場合は以下のようにします。

  • CSSを書く(ここではapp/assets/stylesheets/my_makeup.cssとする)
  • app/assets/config/manifest.jslink_treelink_directorylinkを用いて、stylesheet_link_tagでそのCSSを利用できるようにする(例: link my_makeup.css
  • ビューにstylesheet_link_tagを書いてCSSをインクルードする(例: <%= stylesheet_link_tag 'my_makeup' %>

🔗 Sprocketsを使ってたときのようにWebpackを使わないこと

流れに逆らって時間を無駄に溶かしたくなければ、このセクションの内容を理解しておくことが重要です。そんな時間があればES6を学ぶことに少しでも回せれば理想ですが、少なくともこれだけは言えます。

WebpackがSprocketsと違う点は、Webpackがコンパイルするのは「モジュールである」という点です。

正確には「ES6モジュール」です(Rails 6のデフォルト設定の場合)。これはどういうことなのでしょうか?つまり、モジュール内で宣言されるものは、ある意味すべて名前空間化されるということです。というのも、これはグローバルスコープからアクセスできるようにするものではなく、むしろインポートしてから使うものだからです。いくつか例を挙げてみましょう。

Sprocketsでは以下のように書けます。

  • app/assets/javascripts/hello.js:
function hello(name) {
  console.log("Hello " + name + "!");
}
  • app/assets/javascripts/user_greeting.js:
function greet_user(last_name, first_name) {
  hello(last_name + " " + first_name);
}
  • app/views/my_controller/index.html.erb:
<%= javascript_link_tag 'hello' %>
<%= javascript_link_tag 'user_greeting' %>

<button onclick="greet_user('Dire', 'Straits')">Hey!</button>

かなりシンプルに理解できますね。ではWebpackerではどうなるのでしょうか?

「上の2つのJavaScriptファイルをapp/javascript/packsの下に移動してjavascript_pack_tagでそれぞれインクルードすればいいんじゃね?」とお思いの方、ちょっと待った。それではたぶん動かないでしょう。

その理由は、hello()は1つのES6モジュールとしてコンパイルされるからです(user_greeting()も同様に(別の)ES6モジュールとしてコンパイルされます)。つまり、ビューで両方のJavaScriptファイルをインクルードしたとしても、user_greeting()関数にとってhello()関数は存在しないのです。

ではWebpackでSprocketsと同じ結果を得るには以下のようにすればよいのでしょうか?

  • app/javascript/packs/hello.js:
export function hello(name) {
  console.log("Hello " + name + "!");
}
  • app/javascript/packs/user_greeting.js:
import { hello } from './hello';

function greet_user(last_name, first_name) {
  hello(last_name + " " + first_name);
}
  • app/views/my_controller/index.html.erb:
<%= javascript_pack_tag 'user_greeting' %>

<button onclick="greet_user('Dire', 'Straits')">Hey!</button>

残念ながら、これも同じ理由で動きません。greet_user()がコンパイルされると、モジュール内に隠蔽されてしまうため、ビューからgreet_user()にはアクセスできないのです。

いよいよ本セクションで最も肝心な部分にたどり着きました。

  • Sprocketsでやる場合: ビューはJavaScriptファイルで公開されているものとやりとり可能(変数アクセスや関数呼び出しなど)
  • Webpackでやる場合: JavaScript packに含まれているものにはビューからアクセスできない

ではJavaScriptアクションをボタンでトリガーするにはどうすればよいのでしょう?答えは、packから、ある振る舞いをHTML要素に追加することです。後はvanilla JSだろうとjQueryだろうとStimulusJSだろうとどれでもやれます。

以下はjQueryでやる場合の例です。

import $ from 'jquery';
import { hello } from './hello';
function greet_user(last_name, first_name) {
  hello(last_name + " " + first_name);
}
$(document).ready(function() {
  $('button#greet-user-button').on(
    'click',
    function() {
      greet_user('Dire', 'Strait');
    }
  );
});

/* ES6の場合はこうやれる: */
$(() =>
  $('button#greet-user-button').on('click', () => greet_user('Dire', 'Strait'))
);
  • app/views/my_controller/index.html.erb:
<%= javascript_pack_tag 'user_greeting' %>

<button id="greet-user-button">Hey!</button>

結論: Webpackでは、欲しい振る舞いをビューではなく「packで」セットアップせよ

直前の例を用いて、繰り返し申し上げます。

何らかのライブラリ(たとえばselect2とかjQueryとか)が必要になった場合、そのライブラリをpackの中でインポートすればビューで使えるでしょうか?できません。あるpackでインポートしたライブラリはそのpackの中でお使いください。さもなければ本記事の次のセクションをお読みください。

原注

StimulusJSでJavaScriptコードを構成する方法やHTML要素に振る舞いをアタッチする方法を学びたいのであれば、StimulusJS on Rails 101がおすすめです。

「あらゆるものが隠蔽され名前空間化される世界」がどんなふうに動作するかを知りたい皆さまへ: ES6のあるモジュールがコンパイルされてES5コードに変換されると、そのモジュールの中身は1つの無名関数の内部に封じ込められ、その関数の外からはモジュール内で宣言された変数や関数に一切アクセスできなくなります。

(後編に続く)

Rails 6: Webpacker+Yarn+Sprocketsを十分理解してJavaScriptを書く: 後編(翻訳)

関連記事

Rails 6+Webpacker開発環境をJS強者ががっつりセットアップしてみた(翻訳)

Rails 5: Webpacker公式README — Webpack v4対応版(翻訳)

The post Rails 6: Webpacker+Yarn+Sprocketsを十分理解してJavaScriptを書く: 前編(翻訳) first appeared on TechRacho.

Railsの技: 日付同士をbefore?とafter?で比較する(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

日本語タイトルは内容に即したものにしました。

Railsの技: 日付同士をbefore?とafter?で比較する(翻訳)

私は日付同士を比較するコードを書くときに、わけもなく緊張してしまいます。気をつけていないと<>=の向きを取り違えて逆に書いてしまいがちなのです。

私は日付の比較を「より前(before)」か「より後(after)」で考えるので、たとえばstart_dateend_date「より大きい(greater than)か」あるいは「より小さい(less than)か」という発想に置き換えようとすると混乱してしまいます。

使い方

ありがたいことに、私が二度とこのような失敗をしないように、Railsはすべての日付関連の比較にbefore?メソッドとafter?メソッドを追加してくれています。おかげさまで私のような者にとっては助かります。

start_date = Date.new(2019, 3, 31)
end_date = Date.new(2019, 4, 1)

start_date.before? end_date
#=> true
end_date.after? start_date
#=> true

start_date = Date.new(2020, 8, 11)
end_date = Date.new(2018, 8, 11)

start_date.before? end_date
#=> false

気がつくと私は、以下のようにRailsのモデルで日付同士が有効範囲に収まっているかどうかをバリデーションするときにbefore?after?を愛用するようになりました。

class Promotion < ApplicationRecord
  validate :valid_eligiblity_range?

  def valid_eligiblity_range?
    return unless expiration_date? && start_date?

    if !expiration_date.after?(start_date)
      errors.add(:start_date, "must be before Expiration Date")
      errors.add(:expiration_date, "must be after Start Date")
    end
  end
end

参考資料

関連記事

Railsの技: 起動時に環境変数の存在を確認する(翻訳)

The post Railsの技: 日付同士をbefore?とafter?で比較する(翻訳) first appeared on TechRacho.

週刊Railsウォッチ(20210524前編)Active Supportの知られてなさそうな機能5つ、RSpecの歴史、書籍『Practicing Rails』ほか

$
0
0

こんにちは、hachi8833です。

週刊Railsウォッチについて

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

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

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

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

🔗 無効なセッションストアへの書き込みでエラーが出力されるようになった

従来はconfig.session_store :disabledを設定しても、リクエスト処理が終了する時点で単にセッションハッシュを捨てるだけだった。
書き込み時に明示的に失敗させることで、バグの早期発見に役立つ。
読み出しは引き続き許可する。
不明な点がそこそこあるのでとりあえずこのプルリクをドラフトとしてオープンしている。
同PRより大意


つっつきボイス:「config.session_store :disabledという設定がRailsにあったとは知らなかった」「セッションを使わない設定にしたときに従来だと書き込んでも警告なしで動いていたのか」「warningでもよさそうかなと思いましたけど、明示的にエラーを出すように変えたんですね」「この改修はbreaking changeだけど、これを踏む人はめったにいなさそう」

「Railsでセッションストアを無効にすることってあるんでしょうか?」「たとえば、静的なコンテンツを一方的に配信するだけのCMSサイトを構築する場合とかならセッションストアは不要になるので、無効にすることはあると思いますよ」「それもそうですね」「あるいはCookieだけで十分やれる状況なら、セッションストアをオフにする方がリソースは節約できるでしょうね」

「どちらもやったことはありませんけど😆」「Railsでセッションストアをオフにするなら、Rails以外の静的サイトジェネレータなどで作る方がよさそうですよね」「Railsでセッションストアをオフにできる機能があることがわかったのは収穫」

🔗 Active JobでRangeシリアライザを追加

# activejob/lib/active_job/serializers/range_serializer.rb
+# frozen_string_literal: true
+
+module ActiveJob
+  module Serializers
+    class RangeSerializer < ObjectSerializer
+     KEYS = %w[begin end exclude_end].freeze
+
+     def serialize(range)
+       args = Arguments.serialize([range.begin, range.end, range.exclude_end?])
+       hash = KEYS.zip(args).to_h
+       super(hash)
+     end
+
+     def deserialize(hash)
+       args = Arguments.deserialize(hash.values_at(*KEYS))
+       Range.new(*args)
+     end
+
+     private
+       def klass
+         ::Range
+       end
+    end
+  end
+end

つっつきボイス:「お、Active Jobに渡す引数でRangeオブジェクトもシリアライズできるようになったんですね」

「この改修の意味はこういうことだと思います: まずActive Jobはジョブにenqueueするプロセスとジョブを処理するプロセスが分かれますよね」「はい」「そのため、ジョブのパラメータをシリアライズしていったん何らかのデータベースに保存しておく必要があるわけです」「ふむふむ」

「Active Jobを使ってコードを書いているとわかると思いますけど、Active Jobの引数ではオブジェクトそのものではなくオブジェクトのid(DBのid)を渡すのが普通です: そしてその後Workerがジョブを処理するときに改めてfindするなどして取り直します」「ジョブを投入するRailsサーバーとジョブを処理するWorkerでプロセスが違っているから取り直さないといけないんですね」

「そうした理由があるのでジョブの引数はシリアライズできないといけないんですが、今回の改修ではRangeオブジェクトを渡したときもシリアライズできるようになったということですね」「つまり今までRangeはシリアライズしてくれなかったということか」「自分でシリアライズすることはできそうですが、これまではサポートしてなかったんでしょうね」

「テストコードを見ると.....記法をサポートするようになってる↓」「ドキュメントにも自動でシリアライズされるオブジェクトにRangeが追加されていますね」「何度か話題にしたglobalidも含まれている(ウォッチ20181203)」

# activejob/test/cases/argument_serialization_test.rb#L21
  [ nil, 1, 1.0, 1_000_000_000_000_000_000_000,
    "a", true, false, BigDecimal(5),
    :a,
    1.day,
    Date.new(2001, 2, 3),
    Time.new(2002, 10, 31, 2, 2, 2.123456789r, "+02:00"),
    DateTime.new(2001, 2, 3, 4, 5, 6.123456r, "+03:00"),
    ActiveSupport::TimeWithZone.new(Time.utc(1999, 12, 31, 23, 59, "59.123456789".to_r), ActiveSupport::TimeZone["UTC"]),
    [ 1, "a" ],
    { "a" => 1 },
    ModuleArgument,
    ModuleArgument::ClassArgument,
-   ClassArgument
+   ClassArgument,
+   1..,
+   1...,
+   1..5,
+   1...5,
+   Date.new(2001, 2, 3)..,
+   Time.new(2002, 10, 31, 2, 2, 2.123456789r, "+02:00")..,
+   DateTime.new(2001, 2, 3, 4, 5, 6.123456r, "+03:00")..,
+   ActiveSupport::TimeWithZone.new(Time.utc(1999, 12, 31, 23, 59, "59.123456789".to_r), ActiveSupport::TimeZone["UTC"])..,
  ].each do |arg|

参考: class Range (Ruby 3.0.0 リファレンスマニュアル)

🔗 Psych 4でのbreaking changesに対応

Psych 5のloadがデフォルトでsafe_loadになった: #487
そのため、エイリアスを明示的に許可する必要がある。
実際、Ruby 3.0でもload(aliases: true)を受け付けないPsychが同梱されている。
同PRより大意


つっつきボイス:「先週取り上げたPsych gemのbreaking changesがRailsにも来たそうです(ウォッチ20210518)」「先週出られなかったんですけどPsychって何でしたっけ」「yamlのパーサーですね」「修正はYAML.load()でエイリアスを有効にしたのか」

# activesupport/lib/active_support/configuration_file.rb#L21
    def parse(context: nil, **options)
-     YAML.load(render(context), **options) || {}
+     source = render(context)
+     begin
+       YAML.load(source, aliases: true, **options) || {}
+     rescue ArgumentError
+       YAML.load(source, **options) || {}
+     end
    rescue Psych::SyntaxError => error
      raise "YAML syntax error occurred while parsing #{@content_path}. " \
            "Please note that YAML must be consistently indented using spaces. Tabs are not allowed. " \
            "Error: #{error.message}"
    end

「#42257のテスト更新でもunsafe_loadloadの使い分けに対応している↓」「これは対応しないわけにいかないでしょうね」

# actionpack/test/controller/parameters/serialization_test.rb#L24
  test "yaml deserialization" do
-   params = ActionController::Parameters.new(key: :value)
+   roundtripped = YAML.load(YAML.dump(params))
+   payload = YAML.dump(params)
    roundtripped = YAML.respond_to?(:unsafe_load) ? YAML.unsafe_load(payload) : YAML.load(payload)

    assert_equal params, roundtripped
    assert_not_predicate roundtripped, :permitted?
  end

ruby/psych - GitHub

「そういえば先週のhackmd.ioのRuby-dev office hour↓でもPsychが話題になっていました」「変更前にChangelogやアナウンスがあってもいいのにという気持ちはわかる」「Railsの場合はbreaking changesの前にdeprecationによる猶予期間を置きますけど、Railsの外のgemなのでリリースポリシーがRailsと違うのは仕方ないかも」

参考: Ruby-dev office hour - HackMD

🔗 ActiveRecord::Base.loggerclass_attributeになった


つっつきボイス:「mattr_accessorからclass_attributeに変更されたのか↓」「ロガーが速くなったんですね」「プルリクにも書かれているようにここがホットスポット(実行の頻度が高いコード)なのは確かなので、これが速くなるのはいいことだと思います👍」

# activerecord/lib/active_record/core.rb#L20
-     mattr_accessor :logger, instance_writer: false
+     class_attribute :logger, instance_writer: false

cattr_accessorが依存しているクラス変数は、長大なancestorチェインでのパフォーマンスがよくない(詳しくは#17763を参照)。
ActiveRecord::Baseclass_attributeはそれよりも7倍程度高速。

Calculating -------------------------------------
              logger      1.700M (± 0.9%) i/s -      8.667M in   5.097595s
             clogger     11.556M (± 0.9%) i/s -     58.806M in   5.089282s

Comparison:
             clogger: 11555754.2 i/s
              logger:  1700280.4 i/s - 6.80x  (± 0.00) slower

高速な理由はActiveRecord::Base.ancestors.size == 62による。
ActiveRecord::Baseのその他のcattrの利用についても考慮すべきだが、さしあたってloggerが最大のホットスポットなので、ここから議論を始めるのがよさそう。
同PRより大意

Rails: クラスレベルの3つのアクセサを比較する(翻訳)

🔗 NULLS FIRSTをどのデータベースでも使えるnulls_first()が追加


つっつきボイス:「NULLS FIRSTは、SQLを長くやっている人ならご存知の機能だと思います: nullableなカラムをソートしたときに、NULLが冒頭に来るか末尾に来るかを指定する」「そういえばそんな機能ありましたね」

「並び順でのNULLの位置を変更する機能はプルリクにも書かれているようにDBMS依存なので、DBMSによって使えるものとそうでないものがあります」「そうでした」「もっともSQLの概念上は、本来NULLはどの値でもないものなので、NULLを含むカラムをORDER BYするのは無理があるとは思いますけどね」「たしかに」「DBMSによってはNULLがあるカラムをORDER BYすると遅くなるものもあったと思いますし、まずはORDER BYするカラムがnullableにならないようにしておくことでしょうね」

「プルリクによると、ANSI SQLではASC NULLS FIRSTについてはサポートされているそうですね↓」「へ〜!」「MySQLだけFIRST固定でLASTを選べないのか…頑張って欲しいです」「DBMSごとにこういう微妙な差があることは知っておくべきですね」

ほとんどのデータベースではテーブルを並べ替えるとNULL値が冒頭に来て、その他の値がそれに続く。PostgreSQLではNULLS LASTが使える。
ありがたいことに、ANSI SQLにはNULLSをソート順の冒頭に置くか末尾に置くかをデータベースで指定できるオプションがある。

    ORDER BY column ASC NULLS FIRST

MS SQL、SQLite、Oracle、PostgreSQLは上の構文をサポートしているが、残念ながらMySQLはこの構文をサポートしていない。

変更前:
* PostgreSQL: .nulls_first().nulls_last()も動く
* その他のDB: どちらの場合もエラーになる

変更後:
* PostgreSQL: どちらも動く
* MySQL: .nulls_first()は動くが.nulls_last()はランタイムエラーになる
* その他のDB どちらも動く

PostgreSQL向けの機能は#38131で導入された。このプルリクは他のDBにもこの機能を追加する。
同PRより大意

🔗Rails

🔗 active_period: 時間や期間のサポートを強化(Ruby Weeklyより)

billaul/active_period - GitHub


つっつきボイス:「Period.todayPeriod.year('01/01/2021')みたいな書き方ができるgemだそうです」「こういうのは普通に自分で書くこともできるでしょうし、RailsのActiveRecord::Durationがそもそも優秀なので、自分がこのgemを入れることはあまりなさそうかも」

参考: ActiveSupport::Duration

# The FreePeriod from 01/01/2021 to 01/02/2021 has 5 weeks
Period.new('01/01/2021'...'01/02/2021').weeks.count # 5

# The StandardPeriod::Month for 01/01/2021 has 4 weeks
Period.month('01/01/2021').weeks.count # 4

# How many day in the current quarter
Period.this_quarter.days.count

# Get all the quarters overlapping a Period of time
Period.new(Time.now..2.month.from_now).quarters.to_a

「上のquarter(四半期)みたいなのはあってもいいかもと一瞬思いましたけど、四半期の概念は国や企業ごとに異なるのでたぶん統一的にやりづらいと思うんですよ」「言われてみれば、第1週みたいな”週”の概念も会社ごとに違ったりしますよね: 第1四半期の第一週はどの週なのか、とか」「たしかに」「週を日曜始まりにするか月曜始まりにするかとかも考え始めると大変そう」「日付と期間はややこしいですね…」

🔗 RSpecテストをシンプルにする5つのルール(Ruby Weeklyより)


つっつきボイス:「先週に続いてRSpecテストの書き方記事です」「ネストを深くしない、letはトップに置く、この辺はわかる」

「ところで、このcreate_user.(name: "Jane", email: "jane@doe.org")という書き方がカリー化的で変わってるな〜↓」

RSpec.describe CreateUser do
  subject do
    CreateUser.new
  end

  context "with valid params" do
    specify {
      expect(create_user.(name: "Jane", email: "jane@doe.org")).to be_success
    }
  end
end

4. デフォルトではモックを使わない

(中略)少なくとも本物を使うと以下の問題がある場合にのみモックを使おう:
* テストのセットアップが著しく複雑になる場合
* テストが著しく遅くなる場合
* 外部システム(ファイルシステム)で望ましくない副作用が起きる場合
* インターネット接続に依存する場合
同記事より抜粋・大意

「4.はデフォルトではモックを使うなとありますけど、これはどういうことでしょう?」「自分も、他で再利用されていないコードをわざわざモックでテストする必要性をあまり感じない方です: そういうコードは結合した状態でテストしないと意義が薄いのではないかと感じることもよくあります」「あ、なるほど」

「そのコードが複数の場所で相互に利用されているのであればモックを使う意味はあると思いますけど、1箇所でしか使われていないコードだったら、そのコードを使っている場所でまとめてテストする方がいいんじゃないかという気持ちになりますね」「言われてみればそうかも」「モックを使って両方でテストを書く方がたしかにきれいですし、エラーがどちらで起こったかがわかりやすいという面もあるんですけど、テストを2つ書くコストに見合うかどうかを考えるとモックを使わずに書くことが多いかな」

「5.はテストをDRYではなくベタに書くことを容認しようということですね」「よく言われるヤツ」「テストはそういうさじ加減が難しい…」


「ところで4.にもサンプルコードがあったらいいのにとちょっと思いました」「テストに関する記事って、あまり具体的すぎても伝わりにくいところがあるんですよ: テストを書いた背景を説明し始めるとテーマがどんどん広がって収拾がつかなくなりがち」「それたしかに!」「なので、テストについて説明しようとすると、この記事のように抽象的なさじ加減について書くことが多いですね: そういうのにハマったことのある人はこの記事を読んでみると思い当たるところがあると思います👍」

🔗 Active Supportのあまり知られていない機能5つ(Ruby Weeklyより)


つっつきボイス:「知らないかもしれないActive Supportの機能か、どれどれ」

ActiveSupport::Callbacksは意識せずに使っている人も多いんじゃないかな」「お〜」「こうやってincludeして使える↓: コールバックは育ちすぎると後で苦しむことになるので、ほどほどにするのがよいと思います」

# 同記事より
class BaseService
  include ActiveSupport::Callbacks
  define_callbacks :call

  def call
    run_callbacks :call do
      process
    end
  end
end

ActiveSupport::Configurableは単体で使ったことはありませんが、そういえばActive Supportにありますね」「自分ならconfig gemを使うかな」「自分もそうするかも」

# 同記事より
class Example
  include ActiveSupport::Configurable
end

Example.config.some_option = 'value'

Example.config.some_option
=> "value"

rubyconfig/config - GitHub

「お、知る人ぞ知るActiveSupport::CurrentAttributesも紹介されていますね: 一種のスレッドセーフなグローバル変数的な機能」「そういえば以前翻訳した記事↓では反対意見が出ていました」「CurrentAttributesの是非はともかく、存在を知っておくことは重要でしょうね: Railsらしく書く方法が他にどうしても見つからないときに、スレッドセーフかつグローバルなデータアクセス方法としてCurrentAttributesを知っておけば思い出せるので」「なるほど」

Railsの`CurrentAttributes`は有害である(翻訳)

CurrentAttributesではスレッドセーフは保証されていますが、グローバルであることには変わりないので、基本的には非推奨ぐらいに思っておくのがよいと思います」「そうですね」

参考: ActiveSupport::CurrentAttributes

ActiveSupport::MessageVerifierは、そういえば以前署名付きidに関連してRails 6.1で追加されてましたね(ウォッチ20200525)」「これはどういうものでしょう?」「署名付きidという一種のシグネチャを一方向に生成できて、後でverifyしたり期限切れにしたりできるというものですね」「お〜」

# 同記事より
key = 'my_secret_key' # use env variable or generate via ActiveSupport::KeyGenerator
verifier = ActiveSupport::MessageVerifier.new(key)
verifier.generate('my message')
=> "BAhJIg9teSBtZXNzYWdlBjoGRVQ=--078c6389020294311bb45f099ab56450d9127d44" 

参考: ActiveSupport::MessageVerifier

ActiveSupport::MessageEncryptorはちょうどDeviseがらみで暗号化を手動で解除するのに使いましたよ」「うう、大変そう…」「ちなみにこれはRails 7に入る予定のActiveRecordのカラムの暗号化(ウォッチ20210330)とは別物で、Active SupportがRubyの世界の範囲内で暗号化を行うもので、昔からあります」「なるほど」

# 同記事より
key = SecureRandom.random_bytes(32)
crypt = ActiveSupport::MessageEncryptor.new(key)

message = crypt.encrypt_and_sign('my message')

crypt.decrypt_and_verify(message)
=> "my message"

参考: ActiveSupport::MessageEncryptor


「ちなみにActive Supportはよくできていて、自作のクラスにしれっとincludeしても普通に使えるんですよ」「なるほど、独立性が高いんですね」「Active Supportのコードをたまに眺めてみると、自分のところでも使える機能がちょくちょく見つかるので、おすすめですよ👍」

参考: Active Support コア拡張機能 - Railsガイド

🔗 RSpecの歴史


つっつきボイス:「すべてはアサーションから始まって、やがてRSpecが誕生し、DSLでより自然言語的に書けるようになっていた…という感じで歴史をたどる記事」「Railsを始めて間もない人やこの辺の歴史を知らない人は読んでおくとよいと思います: よさそうな記事👍」「翻訳してみたいです」

以下はつっつき後のツイートです。

🔗 その他Rails


つっつきボイス:「ついにtherubyracer gemがexecjsの対象から外されたのか〜」「therubyracerがインストールできなくてつらかった思い出があります😢」「therubyracerは、元々ビルド済みのtherubyracerを使えば環境に悩まされずに使えるJSエンジンとして登場したので、それだと逆ですよね😆」「まったくです…」

「事情があってJSエンジンをaptコマンドなどでインストールできない環境でも、gemとしてJSエンジンをインストールできるのがtherubyracerだったんですよ」「そういうものだったんですか」「それにしては随分このgemで苦労しました」

「therubyracerはたしかバージョン番号が偶数と奇数で違うんですよ: どちらかがローカルでビルドが走って、どちらかがビルド済みという違いだった気がします」「そうだったかも」「そのあたりを説明した記事が見つからない…いずれにしろお役御免なので別にいいかな」「今はこの辺で苦労しなくなって本当によかったと思います」


「Practicing Railsは、Railsチュートリアルを終えたぐらいの人をターゲットにした書籍みたいです」「たしかにそうした読者層にとって助けになるものがあるといいでしょうね」「新技術をどうキャッチアップしていくかみたいな、Railsエンジニアとしての心構えなどに重点が置かれていそうな感じかな👀」

参考: Ruby on Rails チュートリアル:プロダクト開発の0→1を学ぼう


前編は以上です。

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

週刊Railsウォッチ(20210518後編)RubyのGCを深掘りする、Psych gemのbreaking change、11月のRubyConf 2021ほか

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

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

Rails公式ニュース

Ruby Weekly

The post 週刊Railsウォッチ(20210524前編)Active Supportの知られてなさそうな機能5つ、RSpecの歴史、書籍『Practicing Rails』ほか first appeared on TechRacho.

週刊Railsウォッチ(20210525後編)Rubyのオブジェクトアロケーション改善、RubyKaigi Takeout 2021開催日発表、AWS App Runnerほか

$
0
0

こんにちは、hachi8833です。

週刊Railsウォッチについて

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

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

🔗Ruby

🔗 Rubyのオブジェクトアロケーションを改善する方法


つっつきボイス:「翻訳記事でお世話になっているSchneemsさんの昨年の記事が面白そうなので拾ってみました」「ライフチェンジング!」

  • オブジェクトアロケーションの片付けについて
  • 片付け例1: Active Recordのrespond_to?ロジック
  • パフォーマンスと統計的有意性
  • 片付け例2: stringからtimeへの変換は時間がかかる
  • 片付け例3: 極めて高速なキャッシュキー
    同記事見出しより

「memory_profilerや自作のderailed_benchmarksを使って丁寧に測定しながらアロケーションを改善してるようですね」

SamSaffron/memory_profiler - GitHub

schneems/derailed_benchmarks - GitHub

# 同記事より
$ bundle exec derailed exec perf:objects

allocated memory by gem
-----------------------------------
    227058  activesupport/lib
    134366  codetriage/app
    # ...


allocated memory by file
-----------------------------------
    126489  …/code/rails/activesupport/lib/active_support/core_ext/string/output_safety.rb
     49448  …/code/codetriage/app/views/layouts/_app.html.slim
     49328  …/code/codetriage/app/views/layouts/application.html.slim
     36097  …/code/rails/activemodel/lib/active_model/type/helpers/time_value.rb
     25096  …/code/codetriage/app/views/pages/_repos_with_pagination.html.slim
     24432  …/code/rails/activesupport/lib/active_support/core_ext/object/to_query.rb
     23526  …/code/codetriage/.gem/ruby/2.5.3/gems/rack-mini-profiler-1.0.0/lib/patches/db/pg.rb
     21912  …/code/rails/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
     18000  …/code/rails/activemodel/lib/active_model/attribute_set/builder.rb
     15888  …/code/rails/activerecord/lib/active_record/result.rb
     14610  …/code/rails/activesupport/lib/active_support/cache.rb
     11109  …/code/codetriage/.gem/ruby/2.5.3/gems/rack-mini-profiler-1.0.0/lib/mini_profiler/storage/file_store.rb
      9824  …/code/rails/actionpack/lib/abstract_controller/caching/fragments.rb
      9360  …/.rubies/ruby-2.5.3/lib/ruby/2.5.0/logger.rb
      8440  …/code/rails/activerecord/lib/active_record/attribute_methods.rb
      8304  …/code/rails/activemodel/lib/active_model/attribute.rb
      8160  …/code/rails/actionview/lib/action_view/renderer/partial_renderer.rb
      8000  …/code/rails/activerecord/lib/active_record/integration.rb
      7880  …/code/rails/actionview/lib/action_view/log_subscriber.rb
      7478  …/code/rails/actionview/lib/action_view/helpers/tag_helper.rb
      7096  …/code/rails/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb
      # ...

「上のような大量の出力から以下のような行をいかに見つけて調べるかが腕の見せどころですね」「経験がものを言いそう」「この記事の改善でかなり速くなったものもあるみたいですね」

8440  …/code/rails/activerecord/lib/active_record/attribute_methods.rb

「アロケーション改善をステップバイステップで解説してくれているのがいい👍」「Rubyのbenchmarkライブラリは業務ロジックのようなコードをメインで書いていると普段は使わないけど、年に数回ぐらいは使わなければならなくなることもあります」「改善結果をRailsにプルリク投げたのかなと思ったけど、記事には見当たらないみたい…」

参考: library benchmark (Ruby 3.0.0 リファレンスマニュアル)

🔗 ruby/debugのバックトレース書式提案

ruby/debug - GitHub


つっつきボイス:「ko1さんのruby/debugに、st0012さんがバックトレースの書式案をいくつか提案していました」「こういうリッチな出力のデバッガーが標準で入ったらすごいかも」「入って欲しいですね」

# 同issueより: |でアラインする案
=>#0    foo.rb:21 | Foo#forth_call(num1=20, num2=10) #=> true
  #1    foo.rb:8  | block{|ten=10|} in second_call
  #2    foo.rb:15 | Foo#third_call_with_block(block=#<Proc:0x00007fc645174d10 foo.rb:7>)
  #3    foo.rb:7  | Foo#second_call(num=20)
  #4    foo.rb:3  | first_call
  #5    foo.rb:24 | <main>
# 同issueより: 2行ずつ出力する案
          | Foo#forth_call(num1=20, num2=10) #=> true
  #1    foo.rb:8
          | block{|ten=10|} in second_call
  #2    foo.rb:15 
          | Foo#third_call_with_block(block=#<Proc:0x00007fc645174d10 foo.rb:7>)
  #3    foo.rb:7 
          | Foo#second_call(num=20)
  #4    foo.rb:3 
          | first_call
  #5    foo.rb:24 
          | <main>

その後、ターミナルの幅に応じて書式を変えるかどうかも議論されています。

🔗 byebugとzeitwerkで問題(Hacklinesより)


つっつきボイス:「byebug gemがRailsのzeitwerkでうまく動かない問題が起きているのか」「issueも上がっていました↓」

「解決方法は『別のデバッガを使う』だそうです」「何と…」

deivid-rodriguez/byebug - GitHub

参考: 定数の自動読み込みと再読み込み (Zeitwerk) - Railsガイド

「byebugは最近使ってないな〜」「byebugはコードの任意の場所にアタッチできるのが長所」

🔗 RubyKaigi Takeout 2021の開催日が決定


つっつきボイス:「今年のRubyKaigiはTakeoutと銘打って9/9(木)〜9/11(土)開催とサイトに掲載されました」「今年は参加するためにチケットを買う必要があるそうです」

「今回はオンライン参加のみでしょうか?」「オンライン開催はサイトにも書かれていますが、詳しくは今後の更新待ちですね」「プロポーザルの募集もこれからみたい」「チケットを買えば動画を後から視聴できるかどうかとか、そのあたりが知りたいですね」

「現地開催イベントに参加しなくなって久しいですけど、やっぱり一抹の寂しさがありますよね」「旅行の楽しさとか、会場でもらえるグッズとか」「地元の料理やお酒とか」「出かけたいな〜」

🔗クラウド/コンテナ/インフラ/Serverless

🔗 AWS App Runner(StatusCode Weeklyより)


つっつきボイス:「新しく出たAWS App Runnerはまだ詳しく見てませんが、既存のサービスを組み合わせてテンプレート化した新サービスでしょうね: AWSのサービスはこういう形になっているものがよくあります」「AWSあるあるですね」

「App Runnerは、ロードバランサーやTLS証明書のような、コンテナでのサービス構築に必要なものがひととおり揃っている感じなのはよさそう」「GitHubリポジトリとApp Runnerを接続してgit pushで自動デプロイできるのか」「ECR Public↓のリポジトリも使えるんですね」

参考: ECR Public Gallery

「これは便利そうですね」「もちろん現在でもECSやFargateなどを自分で組み合わせれば同じことができますけど、CI連携のパイプラインなども自分で書かないといけないとか、IAMアカウントの作成やらポリシーの設定やら、要するにAWSエンジニアが必要なんですよ」「あ、たしかに」「一度構築が完了すれば後は楽なんですけど、最初の構築が大変」

「App Runnerはそういう面倒な初期構築を詰め合わせにして楽にやれるようにしたということですね」「おそらくAWSに詳しくない人でもAWSエンジニアの手を煩わせずに構築できる方針で作っているんじゃないかな: 眺めた感じではRailsも普通に動かせそうに見えるので、今度使ってみよう」「お〜」「もしかするとApp Runnerは、自分でやる場合に比べて細かいコンフィグがやりにくいということはあるかもしれませんけどね」

🔗 GitHubで動画アップロード


つっつきボイス:「動画を貼れるのはありがたい」「GitHubも地道に改良を重ねてますね」「そういえばGitHubは、たしかCSVファイルをissueに添付できなかったんですよ」「え、知りませんでした」「Excelファイルなども以前貼れなかった覚えがあるので、おそらく情報漏えいにつながりそうな一部のファイル形式はアップロードできないようにしているんじゃないかなと想像してます」

参考: Issue およびプルリクエストのファイル添付 - GitHub Docs

🔗JavaScript

🔗 domevents.dev


つっつきボイス:「はてブでバズってました」「サイト上のDispatchをクリックすると、DOMイベントが伝搬する様子をビジュアル表示してくれるのね」「多少カスタマイズもできるみたい」「こういう動作はこれまで自分の頭の中にはありましたけど、ここまで作り込んだのは凄い」「こんなふうにしくみをビジュアライズするのは貴重ですね👍

参考: Document Object Model - Wikipedia

🔗CSS/HTML/フロントエンド/テスト/デザイン

🔗 IEサポート終了の足音


つっつきボイス:「IEもついに終了に向かうか」「長かったですね😂」「上の記事はMicrosoft公式のもので、下はWordPressでのIEサポート打ち切り宣言でした」

「上の記事を見ると、EdgeブラウザにIEモードというのがあるんですって↓」「やっぱり欲しい人はいるんですね」

参考: Microsoft Edge の Internet Explorer モード - Office サポート

「実際IEを使うことが本当になくなった」「いいことです!」「大昔はMacにもIEがあったのに」「Mac版IE、覚えてます」

参考: Internet Explorer for Mac - Wikipedia

🔗言語/ツール/OS/CPU

🔗 M1 Macのスケジューラ


つっつきボイス:「この記事見た見た: macOSのスケジューラは前から優秀でしたけど、M1 Macではそれをチップレベルで制御している、スケジューラが優秀だと処理速度の体感が違ってくる、という話」「へ〜、知らなかった」「ちなみにQoSはもともとネットワーク方面の用語ですけど、この記事では操作上の応答性能のような感じで使われてますね」

参考: QoS(Quality of Service)とは

「M1チップのマルチコアはヘテロジニアスな構成になっていて↓、図上半分のIcestormコア(Efficiency)は遅いけど消費電力が少ない、下半分のFirestormコア(Performance)は消費電力は多いけど高パフォーマンス、というように分かれています」「お〜」「タスクをどのコアに振り分けるかについてもスケジューラがチップレベルでさらに有効活用するようになった感じですね」


How M1 Macs feel faster than Intel models: it’s about QoS – The Eclectic Light Companyより

参考: ヘテロジニアスマルチコア - Wikipedia

「macOSは前からスケジューラにそういう機能がちゃんとあるんですよ: それもあってmacOSはWindowsと比べるとトラブル時に完全にグリッチすることが少ないですね」「それ実感してます」「大昔のMac OS 9までは爆弾アイコンが頻発してましたけど」「そうそう、OS 9はひどかった」「NeXTSTEPベースのOS Xになってからはグリッチすることがめったになくなりましたね」

参考: What do we call the CPU or the process scheduling algorithm used in macOS? - Quora
参考: グリッチ - Wikipedia
参考: Mac OS 9 - Wikipedia
参考: NEXTSTEP - Wikipedia

「ヘテロジニアスな構成は、特定のタスクに特化したASICなんかで割と見かけるんですが、M1のような汎用CPUで使われるのはちょっと珍しく思えました」「なるほど」「ヘテロジニアスなASICだと命令セットに互換性がないのが普通ですが、M1のIcestormコアとFirestormコアはどちらも汎用プロセスを動かす命令を持っているんだろうと思いました」

🔗 中古PC価格

「実はおとといIntelチップのMacを買っちゃいまして」「ありゃ、M1にしなかったんですか?」「今のMacだとマイグレーションとかが遅すぎたので買い換えないとつらくって😢

「ちなみに今は中古PCが高いので、今買うならむしろ新品の方がいいかもしれませんね」「おぉ?」「今はGPUの値段がすごく高いので、最近メルカリを見ていると中古コンピュータの値段がびっくりするぐらい上がっていますよ」「マジですか?」「自分が持ってる数世代前のGTX1070 16GB i7 6700あたりでメルカリを検索しただけで、自分が買ったときの半額ぐらいの7万円とか9万円とかで出てますよ」「ホントだ」「すげ〜」「いいGPUを積んでるPCなら中古でも高値で売れます」


後編は以上です。

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

週刊Railsウォッチ(20210524前編)Active Supportの知られてなさそうな機能5つ、RSpecの歴史、書籍『Practicing Rails』ほか

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

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

Ruby Weekly

Hacklines

Hacklines

StatusCode Weekly

statuscode_weekly_banner

The post 週刊Railsウォッチ(20210525後編)Rubyのオブジェクトアロケーション改善、RubyKaigi Takeout 2021開催日発表、AWS App Runnerほか first appeared on TechRacho.


Railsの技: 関連先レコードがないデータをwhere.missingで検索する(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

#missingはRails 6.1で追加された機能です。

参考: 週刊Railsウォッチ(20190513)

Railsの技: 関連先レコードがないデータをwhere.missingで検索する(翻訳)

否定は一般には立証できませんが(訳注: 消極的事実の証明)、データベースに否定のクエリを投げることについてはどうでしょうか。クエリはデータを検索するために書くのが普通ですが、逆にデータが「存在しない」ことを検出するためにクエリを書くこともあります。

生SQLで言うなら、LEFT OUTER JOINNULLチェックを組み合わせることで、特定の関連付けを持たないレコードを検出できます。

利用法

Railsでは、上のようなSQLの概念をActive Recordで直接適用できます。

以下のモデルがあるとしましょう。

class Account < ApplicationRecord
  has_many :recovery_email_addresses
end

リカバリーメールのバックアップが設定されていないAccountを探索したいのであれば、以下のようなクエリを書くだけで問題なくできます。

Account.left_joins(:recovery_email_addresses).where(recovery_email_addresses: { id: nil })

# SELECT "accounts".* FROM "accounts" LEFT OUTER JOIN "recovery_email_addresses" ON "recovery_email_addresses"."account_id" = "accounts"."id" WHERE "recovery_email_addresses"."id" IS NULL

しかしこれではコードが長くなります。Rails 6.1からは、同じクエリを以下のようにずっと簡潔に書けるようになりました。

Account.where.missing(:recovery_email_addresses)

# SELECT "accounts".* FROM "accounts" LEFT OUTER JOIN "recovery_email_addresses" ON "recovery_email_addresses"."account_id" = "accounts"."id" WHERE "recovery_email_addresses"."id" IS NULL

生成されるSQLは同一になりますし、コードもずっと読みやすくなります。以下のようにbelongs_toリレーションシップでも同じことができます。

class Contract < ApplicationRecord
  belongs_to :promiser, class_name: "User"
  belongs_to :promisee, class_name: "User"
  belongs_to :beneficiary, optional: true, class_name: "User"
end

Contract.where.missing(:promiser) # promiserがないcontact
Contract.where.missing(:promiser, :beneficiary) # promiserもbeneficiaryもないcontact

以下のようにmissingを通常のActive Recordメソッドチェーンと組み合わせることもできます。

Contact.where("amount > ?", 1200).where.missing(:promiser)
Contact.where(signed: true).where.missing(:beneficiary)

参考資料

関連記事

Rails 6.1: 孤立化したレコードのリストを取れる’missing’クエリメソッドが追加(翻訳)

The post Railsの技: 関連先レコードがないデータをwhere.missingで検索する(翻訳) first appeared on TechRacho.

Rails 7: ERBでRubyのハッシュをHTML属性に変換する機能(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

日本語タイトルは内容に即したものにしました。

Rails 7: ERBでRubyのハッシュをHTML属性に変換する機能(翻訳)

Rails開発者の基本教養の1つに、Rubyコードスニペットをテンプレートファイルで式展開(interpolation)する方法があり、ERBもそのひとつです。ERB以外にもhamlやslimなど様々なテンプレートエンジンがありますが、RailsではERBがデフォルトなので、以下に示す例ではERB構文を使うことにします。

ここでは、ペットの犬🐶画像を表示する例を考えてみましょう。ハッシュに以下のような属性があるとします。

  pet = { image_url: "https://data.whicdn.com/images/23516263/original.jpg",
  name: "Pommya", breed: "Pomeranian", color: "white" }

変更前

上のハッシュの値は、以下のように書くことでHTMLのimgタグに追加できます。

<img src="<%= pet[:image_url] %>", alt="<%= pet[:name] %>",
  data-breed="<%= pet[:breed] %>", data-color="<%= pet[:color] %>">

=> <img src="https://data.whicdn.com/images/23516263/original.jpg" alt="Pommya" data-breed="Pomeranean" data-color="white">

 tagメソッドやcontent_tagメソッドを使って、HTMLタグを置き換えることもできます。以下ではimgタグのすべての属性をRubyのハッシュで定義しています。

<!-- tagヘルパーの場合 -->

<%= tag.img src: pet[:image_url], alt: pet[:name],
  data: { breed: pet[:breed], color: pet[:color] } %>
<!-- content_tagヘルパーの場合 -->

<%= content_tag(:img, "", src: pet[:image_url], alt: pet[:name],
  data: { breed: pet[:breed], color: pet[:color] }) %>

=> <img src="https://data.whicdn.com/images/23516263/original.jpg" alt="Pommya" data-breed="Pomeranean" data-color="white">

変更後

ActionView::Helpers::TagHelper#tag_optionsの実装を使って、ERBの式展開でRubyのハッシュをHTML属性に変換する機能がRails 7にマージされました(#40657)。

これにより、HTMLタグにある他のHTML属性にRubyのハッシュも同居させて、ERBでも同じ結果を得ることができます。

たとえば、HTML要素に何らかのインラインスタイルを追加したいとします(ここでは通常のHTML属性を使いたいところです)。

<!-- tag.attributesの場合 -->

<img <%= tag.attributes src: pet[:image_url], alt: pet[:name],
  data: { breed: pet[:breed], color: pet[:color] } %> >

=> <img src="https://data.whicdn.com/images/23516263/original.jpg" alt="Pommya" data-breed="Pomeranean" data-color="white">
<!-- インラインスタイルを追加する -->

<img <%= tag.attributes src: pet[:image_url], alt: pet[:name],
         data: { breed: pet[:breed], color: pet[:color] } %>
         style="border-radius: 8px; background-color: white;" >

=> <img src="https://data.whicdn.com/images/23516263/original.jpg" alt="Pommya" data-breed="Pomeranean" data-color="white" style="border-radius: 8px; background-color: white;">

この変更によって、HTMLの通常のstyle属性を用いてインラインスタイルを追加できるようになります。

関連記事

Rubyの式展開(string interpolation)についてまとめ: `#{}`、`%`、Railsの`?`

The post Rails 7: ERBでRubyのハッシュをHTML属性に変換する機能(翻訳) first appeared on TechRacho.

週刊Railsウォッチ(20210531前編)RailsConf 2021の動画が公開、GraphQLのN+1を自動回避、Ruby 3のJITとRailsほか

$
0
0

こんにちは、hachi8833です。

週刊Railsウォッチについて

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

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

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

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

🔗 ActiveModel::Type.lookupの委譲を...に変更

# activemodel/lib/active_model/type.rb#L33
-     def lookup(*args, **kwargs) # :nodoc:
-       registry.lookup(*args, **kwargs)
+     def lookup(...) # :nodoc:
+       registry.lookup(...)
      end

つっつきボイス:「引数の*args, **kwargsをトリプルドット構文...に変えてバグを修正したのね」「...構文ってありましたね」「空ハッシュを渡すとargs={}になるはずがargs=nilになっていたのか」

変更しない場合、以下のような新しいテストが失敗する。

  Failure:
  ActiveModel::TypeTest#test_registering_a_new_type [test/cases/type_test.rb:21]:
  Expected: #<struct args={}>
    Actual: #<struct args=nil>

(*args, **kwargs)の委譲は、Ruby 2.7ではターゲットが常にキーワード引数を受け取れるようになっていないと正しくない(Struct.new(:args).newは正しくないケース)。以下を参照。
参考: Correct Delegation with Ruby 2.6, 2.7 and 3.0 · On the Edge of Ruby
同様の#42266も同じように修正された。
同PRより大意

...は、受け取った引数を丸ごと渡す感じなんですね」「トリプルドットはRuby 2.7から入った機能か↓」

「そういえばRuby 3.0で...を改良して引数の先頭以外を...で渡せるようになってましたね↓」

引数のフォワーディングの記法で先頭に引数を書けるようになりました。
www.ruby-lang.orgより

# www.ruby-lang.orgより
def method_missing(meth, ...)
  send(:"do_#{ meth }", ...)
end

「トリプルドット記法使ったことなかった」「コードで見かけたら二度見しそう」「このようにコンストラクタで受けた引数を親クラスのコンストラクタにそのまま渡すのは昔からよくある操作ですね」「新しい書き方ですし、使うなら適切な場所を見きわめてからにしたい気持ち」「このプルリクでトリプルドット記法を思い出せました」

参考: Rubyで使われる記号の意味(正規表現の複雑な記号は除く) (Ruby 3.0.0 リファレンスマニュアル)

受け取った引数をそのまま別のメソッドに渡すための記法です。受け取る側も渡す側もカッコでくくる必要があります。
docs.ruby-lang.orgより

# docs.ruby-lang.orgより
    def foo(...)
      bar(...)
    end

🔗 assert_no_changesfrom:オプションが追加


つっつきボイス:「Minitestのアサーションでfrom:を書けるようになった: いかにもRSpec的な記法ですね」「なるほど」

# 同PRより
assert_no_changes -> { Status.all_good? }, from: true do
  post :create, params: { status: { ok: true } }
end

「RSpecだとfromtoで”〜から〜に変わる”というのが書けます↓: 変更前の値も指定できるのがポイント」「それと似たものがMinitestにも入ったということですね」「アサーションを見るだけで”〜から〜に変わる”という仕様がわかるので、この書き方は昔から好き❤」「この機能が入ったら使ってみたいです」

# relishapp.comより
require "counter"

RSpec.describe Counter, "#increment" do
  it "should increment the count" do
    expect { Counter.increment }.to change { Counter.count }.from(0).to(1)
  end

  # deliberate failure
  it "should increment the count by 2" do
    expect { Counter.increment }.to change { Counter.count }.by(2)
  end
end

🔗 rails dbconsoleでマルチDBのreplicaをサポート


つっつきボイス:「rails dbconsoleっていう機能があるとは!」「rails db:なんちゃらしか使ったことなかった」「rails dbだけでもいいみたい」「今までdatabase.yml見ながらpsqlやMySQLクライアントを使ってたけど、まだまだ知らない機能があるな〜」「修正はrails dbでマルチDBのreplicaを使えるようにしたんですね」

現在はrails dbconsoleでreplicaがすべて無視されるが、replicaが使えればかなり有用(例: 分析のために、production環境から動いていないデータベースにクエリをかける)。
これが何らかの理由でbreaking changeになるのであれば、dbconsoleの引数に--include-replicas flagを足してもよい。
同PRより大意

「手元のMySQL+Railsでやったら動かない…」「rails dbconsoleは単にデータベースクライアント(mysqlコマンドやpsqlコマンド)を呼び出すようですね: つまりデータベースクライアントが入ってなければ動かない」「あ、それで動かなかったのか」「コンテナ環境のRailsだとクライアントライブラリは入っててもコマンドラインのCLIプログラムが入っていないのはありがちかも」「まさにそれでした😅」「こちらはPostgreSQL+Railsでrails dbをやってみるとたしかにpsqlが起動しました」

🔗 Active Storageのタイムスタンプにprecision:を追加


つっつきボイス:「Active Storageでデータベースのconnectionsupports_datetime_with_precision?に対応している場合にprecision: 6が指定されるようになった」「今までは普通のタイムスタンプだったのか」

# activestorage/db/migrate/20170806125915_create_active_storage_tables.rb
class CreateActiveStorageTables < ActiveRecord::Migration[5.2]
  def change
    create_table :active_storage_blobs do |t|
      t.string   :key,          null: false
      t.string   :filename,     null: false
      t.string   :content_type
      t.text     :metadata
      t.string   :service_name, null: false
      t.bigint   :byte_size,    null: false
      t.string   :checksum,     null: false
-     t.datetime :created_at,   null: false
+
+     if connection.supports_datetime_with_precision?
+       t.datetime :created_at, precision: 6, null: false
+     else
+       t.datetime :created_at, null: false
+     end

      t.index [ :key ], unique: true
    end

precisionというオプションがあるんですね」「この記事↓にActive Recordのprecisionのことが載ってる: MySQLの場合、Rails 5まではタイムスタンプの秒に小数部分がなくて、Rails 6からはprecision: 6がデフォルトになった」「precisionはkamipoさんが2年前に#34970でデフォルトにしたんですね」「このprecision: 6がActive Storage用のテーブルでも付けられるようになったようですね」

参考: Rails6 のちょい足しな新機能を試す65(timestamps precision編) - Qiita

-- 同記事より: Rails 6.0.0.rc1の場合
mysql> select * from users;
+----+------+----------------------------+----------------------------+
| id | name | created_at                 | updated_at                 |
+----+------+----------------------------+----------------------------+
|  1 | Taro | 2019-07-26 06:52:38.606642 | 2019-07-26 06:52:38.606642 |
+----+------+----------------------------+----------------------------+
1 row in set (0.00 sec)
-- 同記事より: Rails 5.2.3の場合
mysql> select * from users;
+----+------+---------------------+---------------------+
| id | name | created_at          | updated_at          |
+----+------+---------------------+---------------------+
|  1 | Taro | 2019-07-26 06:42:34 | 2019-07-26 06:42:34 |
+----+------+---------------------+---------------------+
1 row in set (0.00 sec)

🔗Rails

🔗 RailsConf 2021の動画が出揃う(Ruby Weeklyより)


つっつきボイス:「おぉ、RailsConf 2021の動画が公開されたんですね🎉」「今回のRailsConfは有料だった気がする: FAQを見るとたしかに有料チケットを販売していたけど、動画を公開してくれたんですね」「ありがたいです🙏」「ワークショップやLTもひとつひとつ動画を切り分けられていて、動画数すっごく多いですね」「ちゃんと手間とコストをかけて制作したのがわかる👍

🔗 GraphQLのN+1クエリを自動で回避する(Ruby Weeklyより)


つっつきボイス:「GraphQLのN+1って割とよく聞きますよね」「GraphQLは構造上ネステッドにするとN+1になるから仕方ないでしょうね」「たしかに」「GraphQLはスキーマでネステッドな階層を組んでhas_manyしたデータを取れるようにしていくと簡単にN+1クエリが発生するので、この記事のように自動化したい気持ちもわかる」

「記事ではbatch-loader gemを使ったようです↓」

exAspArk/batch-loader - GitHub

「N+1って自動で回避できるものなんでしょうか?」「どうだろう…N+1が起きると事前にわかっていればincludesを使ったり以下のようにActiveRecord::Associations::Preloaderでeager lodingできるんですが」

# 同記事より
class Types::PreloadableField < Types::BaseField
  def initialize(*args, preload: nil, **kwargs, &block)
    @preloads = preload
    super(*args, **kwargs, &block)
  end

  def resolve(type, args, ctx)
    return super unless @preloads

    BatchLoader::GraphQL.for(type).batch(key: self) do |records, loader|
      ActiveRecord::Associations::Preloader.new.preload(records.map(&:object), @preloads)
      records.each { |r| loader.call(r, super(r, args, ctx)) }
    end
  end
end

「実はごく最近、GraphQLの#resolveメソッド中の条件処理でeager loadingしたことで、Active Recordのキャッシュが効き過ぎて結果が期待どおりにならないいう事案に遭遇しまして」「う、それはつらそう…」

「たしかそのときは、#resolveメソッド中で結果セットをhas_many :throughしたテーブルの条件をfilterするような処理を書いていて、その中で#eager_loadingしました。そして#resolveの戻り値にはそのままeager_loading済みのActive Recordオブジェクトを設定したんですが、GraphQL Query側でeager loadingしたテーブルのデータを要求すると、filter済みのデータしか取れなくて想定と異なる、みたいなことが発生しました(言葉で伝わるかな💦)」「何と」「お疲れさまです…」「調べてみたらeager loaingの部分が問題だったことがわかったので修正しました」

「こういうことが起きるかもしれないので、GraphQLの#resolveで返却するActive Recordオブジェクトでは、eager loadingの使い方に気をつけたいと思いました」「ごもっともです」「検索に使ったActive Recordインスタンスと、GraphQLのレスポンスに渡すActive Recordのオブジェクトは、想定しないクエリキャッシュが効かないように注意して使おうという教訓を得た思いです」

「クエリキャッシュについては、たとえば個別にreloadすることで対応できます: このreloadも書き忘れがちなんですが、RailsのコードならActive Recordでhas_manyしているものを取り出すときなどにreloadを書き忘れたら即結果が変わるので、その場で気づけるんですよ」「なるほど」「でもGraphQLの場合は#resolveが返す結果だけ見ても一見問題がないのに、Query側でネステッドなデータを取り出すときにはじめて影響することが分かるので気づきにくい」「あ〜そうか!」

Rails: N+1クエリを「バッチング」で解決するBatchLoader gem(翻訳)

🔗 Ruby 3のJITとRails


つっつきボイス:「こちらはk0kubunさんの記事で、RailsがRuby 3のJITで遅くならないようにできたそうです」「お〜それは凄い!」

「記事の読ませ方がうまいですね: “MJITでRailsが速くならない”から始まってOptcarrotやGCCなどこれまでの話題や経緯をたどってから本題に入るという構成」「さすがですね」「Ruby 2.7ではベンチマーク対象のトップ100メソッドだけをコンパイルしていたのを、すべてのメソッドをコンパイルするように変えてみたら少し速くなったそうです」

「JITでSinatraが11%速くなって、Railsの2つのベンチマークもVMと比べて1.04倍と1.03倍速くなったんですね」「DiscourceのRailsアプリもJITで速くなったのが凄い↓: 増加は1.03倍でも、Discourseのように実際に使われている大規模RailsアプリがJITで遅くならなかったことが大事」「この結果は頼もしいですね」「RailsがJITで遅くならなければJITをオンにすることも増えると思います」「これはいい記事👍」「後で読もうっと」


同記事より

discourse/discourse - GitHub

🔗 その他Rails

つっつきボイス:「はてブでバズっていたGist集記事です」「true/falseを厳密にバリデーションするのか、なるほど↓」

「空白を自動でstripするStrippedString型↓も面白いですね」

「見出しを見ているとこういうニーズがあるというのがよくわかる」「よさげなGistがいろいろ載ってますね: 使っちゃおうかな」「使うならちゃんと内部ロジックも読んだ上で理解して使っていきたいですね」「はい!」


前編は以上です。

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

週刊Railsウォッチ(20210525後編)Rubyのオブジェクトアロケーション改善、RubyKaigi Takeout 2021開催日発表、AWS App Runnerほか

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

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

Rails公式ニュース

Ruby Weekly

The post 週刊Railsウォッチ(20210531前編)RailsConf 2021の動画が公開、GraphQLのN+1を自動回避、Ruby 3のJITとRailsほか first appeared on TechRacho.

週刊Railsウォッチ(20210601後編)Python使いから見たRuby、MySQLのインデックス解説、GitHubが採用したOpenTelemetryほか

$
0
0

こんにちは、hachi8833です。

週刊Railsウォッチについて

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

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

🔗Ruby

🔗 書籍『Webで使えるmrubyシステムプログラミング入門』


つっつきボイス:「前回の銀座Rails#33でこの本の著者がmrubyのプレゼン↓をやってたのを見て、面白そうな本なのでその場で注文かけました」「私もこの本買ってました(読まなきゃ😅)」

🔗 Python使いから見たRuby


つっつきボイス:「これはいい記事でしたね」「私も楽しく読みました」

「この記事ではPythonですが、他の言語(PHP、Java、JavaScriptなど)をやっている人もこの記事で解説されていることを押さえておけば “ある程度まで” Rubyを読めるようになれるでしょうね」

「見方を変えると、他の言語を知っている人が予備知識なしでRubyのコードを読もうとすると、なかなかフィーリングだけでは読めないということでもあると思います」「言われてみるとRubyの構文は少し独特かも」「たとえばJavaをやっている人なら、JavaScriptやPythonを初めて読むときにもある程度までフィーリングで読めると思うんですけど、そういう人がいきなりRubyを読むと、まずeachの読み方がわからなかったりするんですよ」「あ、そうかも」「わかる気がします」

「Rubyには他にもシンボルの概念や%記号、変数名や関数名は小文字で始めてクラス名は大文字で始めるといった縛りなど、他の言語にないものも多いんですよ」「Rubyの例外処理にbeginがなくて、rescueensureのインデントが飛び出ている↓のも、Javaから入った自分は最初戸惑いました」「Rubyを使っているとそれが普通なんですけどね」

# 同記事より
def hoge
  p "main process"
  raise "just for test"
rescue => e
  p "error occurred: #{e.message}"
ensure
  p "this line is always called"
end

「この記事はそういう他の言語から来る人たちにとって役立つと思います👍」「ボリュームがそんなに多くないのもありがたいですね」

🔗 Cの側からRubyを覗く(Ruby Weeklyより)


つっつきボイス:「こちらはCRubyを記述するC言語の側からRubyを覗くという企画のシリーズ記事だそうです」「CRubyの内部実装を解説する記事ですね」「第3回はメソッド呼び出しや可変長引数、キーワード引数、ブロック付きメソッド呼び出しが解説されてる」「最後に練習問題も付いてますね」「CRubyのコードを追う機会は普段なかなかないな〜」

🔗 hashdiff: ハッシュ同士を比較するgem

liufengyun/hashdiff - GitHub


つっつきボイス:「ハッシュのdiff?」「あ〜、こういうふうに差分を配列で取り出せるのか↓」「+-が追加と削除で、~が変更かな」「GitHubとかのdiff viewの感覚で差分を出せる感じ」「巨大なarrayの比較には使わないでくれと書かれてました」

# 同リポジトリより
a = {a:{x:2, y:3, z:4}, b:{x:3, z:45}}
b = {a:{y:3}, b:{y:3, z:30}}

diff = Hashdiff.diff(a, b)
diff.should == [['-', 'a.x', 2], ['-', 'a.z', 4], ['-', 'b.x', 3], ['~', 'b.z', 45, 30], ['+', 'b.y', 3]]

「ところで、こういうdiff viewの挙動ってどこでもだいたい同じだけど、共通のdiffライブラリがあるのかな?🤔」「ググってみるとGoogleのdiff-match-patchというライブラリが出てきた↓」「C++、C#、Python、Objective-C、JavaScriptなどいろんな言語に対応してるんですね」「Rubyはないのか〜」「Luaはあるのに」

google/diff-match-patch - GitHub

🔗 その他Ruby

# 同PRより
$ irb
irb(main)[01:0]> ls ERB.new('test')
ERB#methods:
  def_class         def_method        def_module        encoding          filename          filename=         lineno
  lineno=           location=         make_compiler     result            result_with_hash  run               set_eoutvar
  src
instance variables: @_init  @encoding  @filename  @frozen_string  @lineno  @src

つっつきボイス:「小ネタですけど、RubyのIRBにpryと同じようなlsコマンドが入っていたそうです」「k0kubunさんのお仕事」「最近のIRBの使い勝手、本当によくなったと思います」「自分もpryをすっかり使わなくなりましたね」「私も」「本体にbundleされたライブラリの機能が向上するのはいい👍


「ついに教科書進出!」「よく見たら地理の教科書でした」「Rubyって地域の特産物なのかしら?」「焼き物とか漆器みたいな趣」

🔗DB

🔗 スライド『MySQLとインデックスと私』


つっつきボイス:「そうそう、このスライドはよかった: 令和時代のMySQLインデックスのお話」

「インデックスを図示する方法がうまい↓」「たしかに見やすいですね」「実際のMySQL内部のデータ構造がこうなっているというわけではないと思いますが、RDBMSが与えられたクエリからデータを検索していく順番がうまく図示されているのがとても良いと思います👍」「なるほど」「データがフィルタされる過程はだいたいこのスライドのとおりです」

「複合インデックスのしくみやインデックスマージの話などいろいろ解説されていますね」「結局このあたりは自力でデータベースをチューニングするようにならないとなかなか身につかないんですよ」「データベースとみっちりお付き合いしないとだめか…」

「入っているデータによってクエリプランが変わるというのも割と大事なポイントですね」「はい」「これはPostgreSQLの話ですが、以前stagingではmerge joinしてるのにproductionではフルスキャンする、みたいなケースがありました: 原因は恐らくproductionのレコード数が多すぎて、merge joinするためのメモリ容量が足りなくなったんじゃないかと結論づけました」「お〜」「こういうのを追いかけるにはRDBMS自体の知識がそれなりに必要ですね」

🔗 RDB内の実行順序

「RDBを学ぶときは、RDBの中でどのような順序で処理が進められるかという流れが大事で、これを最初に理解しておかないと今のインデックスの話などもなかなかしっくりこないと思います」「なるほど」「これまでも引き合いに出した(ウォッチ20190416)、そーだいさんの『失敗から学ぶRDBの正しい歩き方』にもそうした知見が豊富にあります↓」「そうそう、これ買いました」「買いました〜」

「同書は現場で実際によくある事例がたくさん紹介されていてとてもよい本です👍」「この本すごくわかりやすかった」「特に同書の『6.2 リレーショナルモデルとソートのしくみ』図6.1『RDBMSとエクゼキュータ』はとても大事: この実行順序を理解しておかないと、最適化するポイントを間違えてしまったりして最適化の意味がなくなる」「この図大事ですね」「この図を理解するためだけにこの本を買ってもいいぐらい」

Julian Evansさんの以下の記事に、同書よりもう少し簡単な図が掲載されています↓。

参考: SQL queries don’t start with SELECT

「今回取り上げたスライドも、そーだいさんのツイートで知ったと思います」

つっつきの後で探してみました↓。

🔗 Active RecordとRDB

「ところでActive RecordのメソッドチェーンもRDBの実行順序を意識しないといけないんでしょうか?」「SQLの最終的な実行順序はRDBの中の話なので、Active Recordのメソッドチェーンの順序は基本的に影響しません」「なるほど!」

「その代わりActive Recordのメソッドには、典型的なデータと典型的なクエリの場合にはクエリをこう書き換えると速くなるといったクエリ効率化のノウハウがいろいろ盛り込まれているので、 単純かつ頻繁に使われるようなユースケースであれば、RDBのことを気にしなくてもある程度速度を出せます」「お〜」「クエリが複雑になって典型的なパターンでなくなってくる場合には、to_sqlで取り出した生SQLのクエリプランを確認したり、想定しているインデックスが使われてるかなどを確認して最適化する必要があるでしょう」

「あとActive Recordのscopeが複雑になってくると、これなら速いだろうと思っていた処理が実際にはやたら遅くて、よく調べたらscopeの中で複雑な条件を付けていてそれが遅かった、みたいなことはあります」「そうそう」「実際の業務アプリでは典型から外れることもよくありますね」

🔗クラウド/コンテナ/インフラ/Serverless

🔗 App Runner続報


つっつきボイス:「先週(ウォッチ20210525)に続いてAWS App Runnerの話題です」

「記事を見てみると、サービス設定にいろいろ制約があるみたい」「大規模なアプリケーションは対象にしてない感じでしょうか?」「たぶんサービスが立ち上がって間もないだけでしょうね」「それもそうか」「AWS Lambdaも初期は数秒程度のexecution timeしか許されていなかったので、それと同じように最初は大規模にしないだろうと思います」

「KMSは使える」「AWS Copilotは使ったことなかった」「Copilotは今使ってて、ちょっとGoogle App Engineに似た雰囲気を感じました」「Systems Manager パラメータストアが使えないのはちょっとしんどそうだけど、必要とされる機能だし、このあたりは今後連携できるようになっていくんじゃないかなという気がする」「そうなって欲しいですね」

参考: AWS Key Management Service(マネージド型の暗号化キー作成と管理)| AWS
参考: AWS Copilot のご紹介 | Amazon Web Services ブログ
参考: AWS Systems Manager パラメータストア - AWS Systems Manager

「早くもApp RunnerでRailsを動かすスクリーンキャストが公開されてました↓」「今のAWSでRailsを動かすのはなかなか大変なので、App Runnerでもっと楽にやれるといいでしょうね」

🔗 GitHubがOpenTelemetryを採用(Hacklinesより)


つっつきボイス:「OpenTelemetryとは?」「下の日本語記事によると、OpenCensusOpenTracingというオープンなサービス監視標準が最近統合されてOpenTelemetryができたそうです」「お、どこかで見たと思ったら、銀座Rails#24で発表されてたのをこの図↓で思い出した」「お〜」


同記事より

参考: OpenTelemetry とは  |  Google Cloud
参考: OpenTelemetry: 計装器を長く使い続けるために - New Relic公式ブログ
参考: OpenCensus(OpenTelemetry)とは | フューチャー技術ブログ

「現代のサービスはユーザーからリクエストを受けてレスポンスを返すまでにたくさんのサービスを経由しますよね: たとえばAWS API Gatewayを通り、Lambdaを呼んで、LambdaがRDBにアクセスし、RDBはSQLを発行するとか」「ですね」「そうしたすべてのサービスがモニタリング用の共通のリクエストIDを共有して、各処理内での所要時間などを付与するフォーマットが標準化されていることで、異なるサービス群を複数使っていてもトレーシングを共通化できるというメリットがあります」「ふむふむ」「そのための規格がいくつかあって、OpenTelemetryはそのひとつだと理解しました」「お〜、そういうのがあったとは」

「この記事に載っているRailsのInstrumentation機能↓は、Railsのあらゆるステータスログを出力できるんですけど、これをOpenTelemetryに対応させられれば、OpenTelemetryをサポートするあらゆる監視サービス(DataDogとか)やビジュアライズツールを使えるようになります」「それでNewRelicも上の日本語記事でもOpenTelemetryに協力していると書いていたんですね」

# 同記事より
# in an initializer, or config/application.rb
OpenTelemetry::SDK.configure do |c|
  c.use 'OpenTelemetry::Instrumentation::Rails'
  c.use 'OpenTelemetry::Instrumentation::PG', enable_sql_obfuscation: true
  c.use 'OpenTelemetry::Instrumentation::ActiveJob'
  # This application makes a variety of outbound HTTP calls, with a variety of underlying
  # HTTP client libraries - you may not need this many!
  c.use 'OpenTelemetry::Instrumentation::Faraday'
  c.use 'OpenTelemetry::Instrumentation::Net::HTTP'
  c.use 'OpenTelemetry::Instrumentation::RestClient'
end

参考: Active Support の Instrumentation 機能 - Railsガイド
参考: クラウド時代のサーバー監視&分析サービス | Datadog

「最初の記事に戻ると、GitHubがOpenTelemetryを採用したというのは、どうやらGitHub内部のRailsでOpenTelemetryを利用しているということみたい」「外部サービスかと思ったらユーザーには直接関係なさそう」「GitHubはOpenTelemetryを使いつつOpenTelemetryを支援しているという記事のようですね」

🔗JavaScript

🔗 SICPのJavaScript版

参考: 計算機プログラムの構造と解釈 - Wikipedia


つっつきボイス:「SICPって何だっけと思ったら、紫色の表紙を見て思い出した」「コンピュータサイエンスをScheme言語(LISP言語の方言)で学ぶ有名な教科書でしたね」

「昔大学でScheme動かしながら学んだ覚えがありますけど、ある時期からはPython版も出てますよ」「え、知りませんでした」「米国だと最近の学生はPythonでないとやってられないとか何とか」「オリジナルのSICPはMITから出版されたんだったかな」

参考: Scheme - Wikipedia
参考: Introduction | SICP in Python

「SICP(Structure and Interpretation of Computer Programs)は日本語だと『計算機プログラムの構造と解釈』」「日本語版を出していたピアソンも今はないんだな…」「そのJavaScript版が出たということですね」「JavaScriptでコンピュータサイエンス学びたい人にはよさそう」「いい本だと思いますけど、ゼミで集まってやるならともかく、ひとりで学ぶのは相当つらいんじゃないかな〜」


後編は以上です。

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

週刊Railsウォッチ(20210531前編)RailsConf 2021の動画が公開、GraphQLのN+1を自動回避、Ruby 3のJITとRailsほか

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

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

Ruby Weekly

Hacklines

Hacklines

The post 週刊Railsウォッチ(20210601後編)Python使いから見たRuby、MySQLのインデックス解説、GitHubが採用したOpenTelemetryほか first appeared on TechRacho.

Railsの技: Active Recordバリデーションをコンテキストに応じて実行する(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

Railsの技: Active Recordバリデーションをコンテキストに応じて実行する(翻訳)

データベースモデルで特定のバリデーションをスキップしたい場合があります。マルチ画面で構成されるウィザードを作る場合や、管理者がデータを自由に変更したい場合などが考えられます。

こういうときは特定のフォームでバリデーションをスキップしたくなるかもしれませんが、もっと良い方法があります。

Railsでは、レコードの保存やバリデーションの際にcontextを渡せます。contexton:オプションを組み合わせれば、ActiveRecordの特定のバリデーションだけを実行できるようになります。

利用法

たとえば、求人情報サイトでマルチステップのワークフローを構築したいとします。求人情報を作成してデータを入力している間は、求人情報を公開するまで以下のように項目の一部をバリデーションしないようにできます。

class Listing < ApplicationRecord
  belongs_to :company
  belongs_to :user

  has_rich_text :requirements

  validates :title, presence: true, length: { maximum: 50 }

  validates :salary_range, presence: true, on: :publish
  validates :application_instructions, presence: true, on: :publish

  def publish!
    self.published_at = Time.current
    save(context: :publish)
  end
end

この場合、titleはこのレコードの作成および編集時には常に省略不可(最大50文字)ですが、バリデーションのコンテキストに:publishを渡した場合にのみsalary_rangeapplication_instructionsをバリデーションします。

このワークフローはコントローラのアクションで以下のように実装できます。

class ListingsController < ApplicationController

  def create
    @listing = Listing.new(listing_params)

    if @listing.save
      redirect_to @listing, notice: "Listing created"
    else
      render :new, status: :unprocessable_entity
    end
  end

  def publish
    @listing = Listing.find(params[:id])

    if @listing.publish!
      redirect_to @listing, notice: "Listing published"
    else
      render :edit, status: :unprocessable_entity
    end
  end
end

また、変更を行うユーザーに応じて異なるバリデーションを加えることもできます(管理者が友人に特別な短いユーザ名を与えることを許可したい場合など)。

以下では、ユーザー名は6文字以上を必須とするルールを:createコンテキストに設定しています(Railsはレコード作成時にデフォルトでこの設定を含めます)。次に、:adminコンテキストにユーザー名を3文字以上とするルールを追加します。

class Account < ApplicationRecord
  validates :username, length: { minimum: 6 }, on: :create
  validates :username, length: { minimum: 3 }, on: :admin
end

Account.new(username: "swanson").valid? # => true
Account.new(username: "swanson").valid?(:admin) # => true

Account.new(username: "mds").valid? # => false
Account.new(username: "mds").valid?(:admin) # => true

Account.new(username: "a").valid? # => false
Account.new(username: "a").valid?(:admin) # => false

Railsのバリデーションでコンテキストを指定する場合、データベースレベルのバリデーションが行えなくなるというデメリットがあります。レコードの一部が無効でも永続化できることや、ルールに条件を設定できるのはコンテキストの強力な機能ですが、代償を伴うことになります。

バリデーションをデータベースレベルの制約レベルからアプリケーションに移すことについては慎重にお考えください。

参考資料

関連記事

The post Railsの技: Active Recordバリデーションをコンテキストに応じて実行する(翻訳) first appeared on TechRacho.

Railsコントローラのアクションがrenderで終わるとは限らない(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

Railsコントローラのアクションがrenderで終わるとは限らない(翻訳)

今回はRailsのちょっとした小技をご紹介します。こんなライブラリを使っているところを想像してみてください。そのライブラリはコントローラー内でいくつかのロジックを実行し、必要な指定が不足している場合は、それに応じていくつかのテンプレートをレンダリングします。このライブラリは古くから存在する、JSON APIを使わずにHTMLテンプレートのみをレンダリングしていた時代に作成されたものだとしましょう。さて、APIコントローラに同様のロジックを追加する必要が生じました。JSONレスポンスを処理するようにライブラリを変更すべきだと思うかもしれませんが、解決方法は他にもあるのです。

コントローラのrenderメソッドを実行しても、アクションの処理を終了して制御フローを返すわけではないことを思い出しましょう。したがって、renderを呼び出した後でも、レスポンスの変更などの操作は引き続き可能です。ただし、1つのアクション内でrenderを複数回呼び出すことはできません(DoubleRenderErrorという例外が発生します)。

つまり、そのライブラリにまったく触らずにアクションを拡張できるということです。そのライブラリが、例外発生時にテンプレートをレンダリングするメソッドを持つモジュールを公開しているとしましょう(これは単なる例なので、ライブラリの出来はここでは問題にしません)。

module ActivationCheck

  def check_active(id)
    ActivationChecker.new.call(id) # 指定のidを持つプロジェクトがアクティブならtrue、それ以外ならエラーをraise
  rescue ActivationCheck::Error => exc
    render("activation_check/error", message: exc.message)
    false
  end
end

そしてAPIコントローラが以下のようになっているとしましょう。

class ProjectsController
  include ActivationCheck

  def show
    project = Project.find(params[:id])
    if check_active(project.id)
      render json: project
    end
  end
end

このライブラリをコントローラで使うときの問題は、check_activeメソッドがfalseを返したときにレスポンスでステータス200 OKのHTMLテンプレートもレンダリングされてしまうことです。JSONテンプレートを作成して、ライブラリが提供するデフォルトのHTMLテンプレートを上書きすることは可能ですが、それでもステータス200 OKが返ってしまいます(レスポンスが成功していないのに成功したというステータスコードを返すべきではありません)。これに対処するために、フローの後半でレスポンスのステータスを直接変更してみましょう。

class ProjectsController
  include ActivationCheck

  def show
    project = Project.find(params[:id])
    if check_active(project.id)
      render json: project
    else # この分岐で既にActivationCheckがテンプレートをレンダリングしている
      response.status = :bad_request
    end
  end
end

後は、JSONテンプレートを作成してデフォルトのテンプレートを上書きすれば(例: app/views/activation_check/error.json.erb)、元のライブラリに手を加えずに、コントローラで適切なステータスコードのJSONレスポンスを返せるようになります。

お知らせ

ARKADEMY.DEVに参加してArkencyのトップクラス教育プログラムコースにアクセスしましょう!「Railsアーキテクトマスタークラス」「アンチ”IF”コース」「忙しいプログラマーのためのブログ執筆コース」「Async Remoteコース」「TDD動画クラス」「ドメイン駆動Rails動画コース」以外にもさまざまなコースが新設中です。

関連記事

ソフトウェアパターンを闇雲に適用しないこと(翻訳)

The post Railsコントローラのアクションがrenderで終わるとは限らない(翻訳) first appeared on TechRacho.

週刊Railsウォッチ(20210607前編)ActiveRecord::Relationのone?とmany?が高速化、RubyKaigi Takeout 2021登壇者募集開始ほか

$
0
0

こんにちは、hachi8833です。RubyKaigi Takeout 2021の登壇者募集が始まりましたね。

週刊Railsウォッチについて

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

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

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

Rails公式ニュースが久しぶりに更新されたので、その中からこれまで取り上げていなかったものを中心に拾いました。

🔗 ActiveRecord::Relationone?many?を高速化

# activerecord/lib/active_record/relation.rb#L296
    def one?
      return super if block_given?
-     limit_value ? records.one? : size == 1
+     return records.one? if limit_value || loaded?
+     limited_count == 1
    end

    def many?
      return super if block_given?
-     limit_value ? records.many? : size > 1
+     return records.many? if limit_value || loaded?
+     limited_count > 1
    end

つっつきボイス:「SELECT COUNT(*)をやめてLIMIT 2を使って高速化したようですね: 巨大テーブルのCOUNTは遅いですけど、SELECT 1したうえでLIMIT 2にすると速いですよ」「お〜、ベンチマークも1300倍以上になったとありますね」「100M行ならこのぐらい差が出ますよ」「速度が欲しい人たちはこれまで同様の独自メソッドを書いていたかもしれませんね」

LIMIT 2で速くなるかどうかは状況にもよるかも: たとえばMySQLでSELECT COUNT(*) FROM tableのようにテーブル内の行をすべて取得するようなクエリを投げると、MySQLのinformation schemaに既にある値を使うので、LIMIT 2よりも速くなるかもしれませんね」「それは速そう!」「LIMIT 2より速くできる方法は他にもありそうですが、Active Recordでscoped chainされることを考えればこのPRの方法がよさそう👍

LIMIT 2に加えてSELECT 1も速くするのによく使われますね」「1ですか?」「1というスタティックな値だけを返すという意味ですね: 定数であればよいので1でなくても構いません」「あ〜なるほど」「行データが不要で件数だけ欲しいようなときに使うことがあります」

参考: sql server 2008 - What does “select 1 from” do? - Stack Overflow


ActiveRecord::Relationone?メソッドやmany?メソッドは、背後でSELECT COUNT(*) FROM postsのようなクエリを生成してから、結果が1に等しい(または1より大きい)かどうかを比較している。巨大テーブルや複雑な条件ではCOUNTが非常に遅くなることがあるが、この場合は全レコード件数は完全に不要。それならLIMITを追加してSELECT COUNT(count_column) FROM (SELECT 1 AS count_column FROM posts LIMIT 2)のようにするだけでずっと高速になる。実際any?などのメソッドではLIMITを使っている。
なおこれはすべてのバージョンのRailsで発生している。
100M行のテーブルでいくつかベンチマークを取ってみると、スピードが1300倍以上増加した。
同PRより大意

🔗 ネストしたsecretsにメソッド名でアクセスできるようになった


つっつきボイス:「ネストしたsecrets自体は前からサポートされていた覚えがありますけど、今回はそれにメソッド名でもアクセスできるようにしたようですね」「なるほど」

参考: Rails 5.2 credentials cheat cheat — ネストした値をフェッチする例が記載されています

  • Rails.application.credentialsのキーにネステッドアクセスできるようになった

従来はcredentials.yml.encのトップレベルにあるキーにしかメソッド呼び出しでアクセスできなかったが、任意のキーに対してできるようになった。
たとえば以下のsecretsがあるとする。

aws:
   access_key_id: 123
   secret_access_key: 345

この改修で、Rails.application.credentials.aws.access_key_idRails.application.credentials.aws[:access_key_id]と同じものを返すようになった。
Alex Ghiculescu
Changelogより大意

🔗 ActionController::Live#send_streamが追加


つっつきボイス:「send_streamも見覚えある: 最近はsend_dataよりもなるべくsend_streamを使おうみたいな話があったような気がしますね」

「既に生成されてファイルになったものを送信するのであればsend_streamでもsend_dataでも変わらないんですが、以下のようにeachで処理を回すものを送信するのであればsend_streamの方が送信開始を早められます」「あ、send_dataだと処理が完了するまで送信できないのか」「そういうことですね: どちらも最終的に同じことをやれるのであれば基本的にsend_streamを使う方がいいかなと思いました」「なるほど」

生成済みのストリームをより送信しやすくするActionController::Live#send_streamを追加。

   send_stream(filename: "subscribers.csv") do |stream|
     stream.write "email_address,updated_at\n"

     @subscribers.find_each do |subscriber|
       stream.write "#{subscriber.email_address},#{subscriber.updated_at}\n"
     end
   end

同PRより大意

Rails 7でActiveStorage::Streamingサポートが追加(翻訳)

🔗 ActiveStorage::Streamingが切り出された


つっつきボイス:「DHHによる改修です」「streaming.rbがActive Storageから切り出された↓」「リファクタリングっぽいですね」「send_blob_streamというメソッドもできた」

# streaming.rb
# frozen_string_literal: true

module ActiveStorage::Streaming
  DEFAULT_BLOB_STREAMING_DISPOSITION = "inline"

  include ActionController::Live

  private
    # Stream the blob from storage directly to the response. The disposition can be controlled by setting +disposition+.
    # The content type and filename is set directly from the +blob+.
    def send_blob_stream(blob, disposition: nil) #:doc:
      send_stream(
          filename: blob.filename.sanitized,
          disposition: blob.forced_disposition_for_serving || disposition || DEFAULT_BLOB_STREAMING_DISPOSITION,
          type: blob.content_type_for_serving) do |stream|
        blob.download do |chunk|
          stream.write chunk
        end
      end
    end
end

Active Storageからストリーミングするコントローラを独自に作りたいのであれば、ストリーミングを適切に行えるメソッドがあると便利。そういうものがActiveStorage::BaseControllerに切り出されていないまま既に存在していた。
同PRより大意

🔗 fresh_whenstale?Cache-Controlヘッダーを上書きできるよう修正

stale(形容詞)新鮮でない、気の抜けた


つっつきボイス:「Cache-Controlヘッダーにまた修正が入った(ウォッチ20201012ウォッチ20210412)」

「コントローラのアクションでCache-Controlヘッダーに積極的に情報を渡す手段が追加されたということみたいですね↓: Cache-Controlを直接書き変えずにfresh_whenメソッドやstaleメソッドのオプション経由で変えられるようにした」「なるほど」

    # When overwriting Cache-Control header:
    #
    #   def show
    #     @article = Article.find(params[:id])
    #     fresh_when(@article, public: true, cache_control: { no_cache: true })
    #   end
    #
    # This will set in the response Cache-Control = public, no-cache.
  • fresh_whenstale?cache_control: {}オプションを追加。
    これらのメソッドでresponse.cache_controlを設定するショートカットとして使える。
    Jacopo Beschi
    同Changelogより大意

「クライアントキャッシュの制御はコントローラがやるものと考えれば、そのためのヘルパーメソッドがあるのは理にかなっていそうな気もする」「あ、Cache-Controlヘッダーが制御するのは一瞬Railsサーバーのページキャッシュのような気がしたけど、クライアントキャッシュの方だったか」

「コントローラでCache-Controlヘッダーを制御できるようになるのはいいことだと思う一方で、コントローラではサーバーのページキャッシュとクライアントキャッシュのどちらについても気にする場面があるので、どちらを制御しているかに気をつけないといけないかも」「それもそうですね」

参考: Cache-Control - HTTP | MDN
参考: RailsにおけるCacheの概念と使い方 - Qiita

🔗Rails

🔗 PostgreSQLのRow-Level SecurityをRailsで使う(Ruby Weeklyより)


つっつきボイス:「PostgreSQLに行レベルのセキュリティ機能があるとは」「略してRLS」「ENABLE ROW LEVEL SECURITY;で有効にできるのか」「どういうふうに使うのかな?」「なるほど、こんな感じでCREATE POLICYでポリシーを記述するんですね↓」

-- 同記事より
CREATE POLICY transactions_app_user
  ON transactions
  TO app_user
  USING (customer_id = NULLIF(current_setting('rls.customer_id', TRUE), '')::bigint);

参考: PostgreSQL: Documentation: 13: 5.8. Row Security Policies

「記事ではこれをRailsで使ってる↓」「やるな〜」

-- 同記事より
class CreateTransactions < ActiveRecord::Migration[6.1]
  def change
    create_table :transactions, id: :uuid do |t|
      t.bigint :customer_id
      t.text :description
      t.bigint :amount_cents
      t.timestamptz :created_at
    end

    # Grant application user permissions on the table (this migration should run as the admin user)
    reversible do |dir|
      dir.up do
        execute 'GRANT SELECT, INSERT, UPDATE, DELETE ON transactions TO app_user'
      end
      dir.down do
        execute 'REVOKE SELECT, INSERT, UPDATE, DELETE ON transactions FROM app_user'
      end
    end

    # Define RLS policy
    reversible do |dir|
      dir.up do
        execute 'ALTER TABLE transactions ENABLE ROW LEVEL SECURITY'
        execute "CREATE POLICY transactions_app_user ON transactions TO app_user USING (customer_id = NULLIF(current_setting('rls.customer_id', TRUE), '')::bigint)"
      end
      dir.down do
        execute 'DROP POLICY transactions_app_user ON transactions'
        execute 'ALTER TABLE transactions DISABLE ROW LEVEL SECURITY'
      end
    end
  end
end

「モデルのコードを見ると、コネクションの部分で'SET rls.customer_id = %s'のようにRLSを設定してますね↓: PostgreSQLコネクションのセッション変数的な部分にこれを設定することで、クエリが適切かどうかをポリシーでチェックできるようになる感じかな」「複雑になるけどその分安心できそう」

# 同記事より
class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true

  SET_CUSTOMER_ID_SQL = 'SET rls.customer_id = %s'.freeze
  RESET_CUSTOMER_ID_SQL = 'RESET rls.customer_id'.freeze
  def self.with_customer_id(customer_id, &block)
    begin
      connection.execute format(SET_CUSTOMER_ID_SQL, connection.quote(customer_id))
      block.call
    ensure
      connection.execute RESET_CUSTOMER_ID_SQL
    end
  end
end

「RLSをセキュリティ上の防衛という観点から見た場合、PostgreSQLのRLSはあくまで生SQLの中で設定しているので、仮にセッションを乗っ取られて生SQLを実行されたら回避できないでしょうね」「それもそうか」

「自分なら、たとえば今度Rails 7に入る予定のモデル暗号化機能(ウォッチ20210412)みたいな暗号化用gemを使って、暗号化の鍵はアプリケーション側だけで持ち、PostgreSQLには暗号化済みのデータを保存する方が、生SQLされたときの対策としては有効かなと思いました」「あ〜なるほど」「もちろんPostgreSQLのRLSも、有効性の範囲を理解したうえでポリシー設定に使う分にはよいものだろうと思います👍

「面白い機能ですね」「ぽすぐれならこういう機能があっても不思議じゃない」「記事の最後には、RLSを使う場合はパフォーマンスにも注意しようと書かれてますね」

🔗 Active Recordのnewにはブロックも渡せる(Ruby Weeklyより)


つっつきボイス:「Active Recordのnewcreateのような作成系メソッドは以前からこういうふうにブロックも渡せますね↓」「あ、そうでしたか」

# 同記事より
u = User.new do |user|
  user.first_name = "Jordan"
  user.last_name = "Knight"
end

「Rubyだと『こういう書き方、やってみたら動くかな?』と思いついてドキュメントも読まずにやってみると本当に動くことがちょくちょくありますけど、そういうノリでnewにブロック渡ししてみたらできたという感じでしょうね」「たしかにRubyだとそれよくありますよね」「Ruby名物の『やったらできた』」


追いかけボイス:「同記事の後半では、Rubyのtapメソッドを使った場合にブロック渡しの挙動が異なることについても触れられていますね↓」

# 同記事より
# tapにブロックを渡す場合
new_user = User.create(first_name: "Jordan", last_name: "Knight").tap do |u|
  u.first_name = "Jonathan"
end

new_user.first_name        #=> "Jonathan"
new_user.reload.first_name #=> "Jordan"
# createに直接ブロックを渡す場合
new_user = User.create(first_name: "Jordan", last_name: "Knight") do |u|
  u.first_name = "Jonathan"
end

new_user.first_name        #=> "Jonathan"
new_user.reload.first_name #=> "Jonathan"

参考: Object#tap (Ruby 3.0.0 リファレンスマニュアル)

🔗 Hotwire記事2本


つっつきボイス:「1本目は今翻訳中のEvil Martiansの記事で、2本目は少し前のですがwillnetさんのHotwire記事です」「もしかすると自分が本当に欲しかったのはHotwireだったのかもしれない、という気持ちもある一方で、Turboがね…Turboを使うかどうかまだ悩み中なんですよ」「そこですよね」「Evil Martiansの記事は、いい意味で古き良き時代のWeb開発に戻れるよと言いつつ、既存Railsアプリに導入するとそこそこ直しが必要だったようです」「HotwireとRails本体の相性についてはあまり心配していませんが、むしろRails以外のものとHotwireとのインテグレーションが気になっています」「あ、そうか!」「いずれにしろHotwireについてはもう少し調べておかないといけないでしょうね」「たしかに」


速報: Basecampがリリースした「Hotwire」の概要

🔗 その他Rails


つっつきボイス:「小ネタですが、過去のRails gemがインストールされていれば、こんなふうにバージョン番号の前後にアンダースコア_を付けるとバージョン指定できるそうです↓」「そうそう、Railsにはこんなオプションが隠れていますね」「たまにとても欲しくなりそうな機能」

# 同記事より
# Create Rails 6.1 project
$ rails _6.1.3_ new rails6_1

# Create Rails 6.0 project
$ rails _6.0.3_ new rails6_0

# Create Rails 5.2 project
$ rails _5.2.3_ new rails5.2

前編は以上です。

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

週刊Railsウォッチ(20210601後編)Python使いから見たRuby、MySQLのインデックス解説、GitHubが採用したOpenTelemetryほか

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

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

Rails公式ニュース

Ruby Weekly

Hacklines

Hacklines

The post 週刊Railsウォッチ(20210607前編)ActiveRecord::Relationのone?とmany?が高速化、RubyKaigi Takeout 2021登壇者募集開始ほか first appeared on TechRacho.


週刊Railsウォッチ(20210608後編)RubyでAppleのLZFSE圧縮データ解凍、AWS Lambda Extensionsが正式リリース、unixgame.ioほか

$
0
0

こんにちは、hachi8833です。

週刊Railsウォッチについて

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

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

🔗Ruby

🔗 object_tracer: 旧TappingDeviceがリネーム(Ruby Weeklyより)

st0012/object_tracer - GitHub


つっつきボイス:「以前@st0012さんが作ったTappingDevice gemがリネームされていたのをRuby Weeklyの記事で知りました」「TappingDeviceという名前でだいぶ違うものを想像していましたけどデバッガーなんですね」「オブジェクトにアタッチして動きをtappingするイメージだからそこから命名していたのかも」

tapping(名詞)傍受、盗聴

「考えてみればこのgemの機能はトレーサーなので、新しい名前の方がいいですね👍」「そうですね」「前の名前はアタッチするための手段が使われていましたけど、新しい名前はgemのユースケースや振る舞いを表していて的確」「なるほど、何をするためなのかという目的がわかる名前ですね」「tappingはあくまで手段のはず」

「ちなみに以下は最近のruby/debugへのマージ回数で、st0012さんが今のところトップです」「この調子でruby/debugに参加する人が増えてさらに使いやすくなるといいですね」

ruby/debug - GitHub

🔗 提案: Rubyのバックトレースに位置情報を含める


つっつきボイス:「@mameさんからの提案は、1行内でHashの添え字アクセスがメソッドチェインで複数回発生するとき、どのオブジェクトへのメソッド呼び出しの時点でエラーが出たのかを特定できるようにThread::BacktraceLocationを拡張するというもの: どこで発生したかがわかりにくいundefined methodメッセージってすごくありがちなので、これが欲しい気持ちわかる」「発生場所がわかるなら教えて欲しいヤツ」「ここだよ、ここですね(古)」

# 同issueより
# どこがundefinedかわかりにくい
data["data"].first["field"] #=> undefined method `[]` for nil:NilClass

# こうやって発生位置を知らせる
$ ruby -r ./sample/no_method_error_ext.rb err1.rb
err1.rb:2:in `<main>': undefined method `[]' for nil:NilClass (NoMethodError)

data["data"].first["field"]
                  ^^^^^^^^^

「Rust言語のエラーメッセージがこういう位置情報を丁寧に出してたのを思い出しました↓」「もしかするとそういうのを意識したのかもしれませんね」

参考: Improved error messages - The Edition Guide

// doc.rust-lang.orgより
error[E0506]: cannot assign to `x` because it is borrowed
 --> foo.rs:4:5
  |
3 |     let y = &x;
  |              - borrow of `x` occurs here
4 |     x += 1;
  |     ^^^^^^ assignment to borrowed `x` occurs here

error: aborting due to previous error

For more information about this error, try `rustc --explain E0506`.

🔗 Arbre: RubyコードでHTML生成(Awesome Rubyより)

activeadmin/arbre - GitHub

# https://activeadmin.github.io/arbre/ より
html {
  head {
    title "Welcome page"
  }
  body {
    para "Hello, world"
  }
}

つっつきボイス:「アーブレ?」「HTML on Rubyとあるので、上のようなDSLからHTMLを生成できるということですね」「一度は作ってみたくなるヤツかも」「ArbreはActive Admin gem↓から切り出されたそうです」

activeadmin/activeadmin - GitHub

🔗 Appleの圧縮フォーマットで遊んでみた(Hacklinesより)

woodruffw/lzfse.rb - GitHub


つっつきボイス:「マニアックそうな記事です」「なるほど、LZFSE圧縮されているシステムファイルをRubyで解凍したりしてる」「著者が作ったruby-machoというgemもありますけど、マッチョかと思ったらマークオーでした」「Mach-Oバイナリを追いかけるとは強い」「使い慣れたRubyでやってみたということでしょうね」

# 同記事より
require "lzfse"

# LZFSE (de)compression
LZFSE.lzfse_compress
LZFSE.lzfse_decompress

# LZNV (de)compression
LZFSE.lznv_compress
LZFSE.lznv_decompress

参考: Mach-O - Wikipedia
参考: LZFSE - Wikipedia

Homebrew/ruby-macho - GitHub

🔗 Rubyの文字列削除メソッドの速度比較


つっつきボイス:「Railsのパフォーマンス最適化でもこの種の情報を見かけますね」「同記事では文字列のサイズも変えて試してるようです」「実装にも影響されるでしょうね」

「用途が限定的なほど速く汎用的なほど遅い、そうそう↓」「特定の用途に特化したメソッドほど速くなる傾向はありますね」

測定その 1 の考察
メソッドを速い順に並べると String#delete, String#tr, String#sub, String#gsub, String#remove の順番でした。用途がより限定的なメソッドは速く、より汎用的なメソッドは遅いという結果となりました。便利な String#gsub をいつも使うのではなく、用途に合わせて適切なメソッドを使うことが良いということですね。
同記事より

「測定その2では、文字列が長くなると結果が変わるのね」「正規表現を使うと遅くなる、ごもっとも」「正規表現好きですけどホントそのとおりです」

🔗DB

🔗 書籍『Production Ready GraphQL』


つっつきボイス:「GraphQLの本!」「GraphQL API側の設計に関する書籍みたい: GraphQLが登場してからだいぶ経ってきたこともあってベストプラクティスがだいたい固まってきた感じはありますね」

目次を見た感じではGraphQLの一般的なスキーマ設計を扱っていて、graphql-rubyとかには言及してなさそうかな」

rmosolgo/graphql-ruby - GitHub

「graphql-rubyを詳しいノウハウを解説している本があったら欲しいと思っているんですよ: GraphQL Rubyの公式サイト↓にリファレンスも一応あるんですけど、現場で使おうとするとまだまだ大変」

「どの辺がつらいんでしょうか?」「テストの書き方などはGraphQL Rubyサイトにひととおり載っていますけど、作り始めてみると、自分が欲しいスキーマを得るにはRuby側でどのように書けばいいのかという部分が意外に難しい」「あ〜」「GraphQL単体の情報はApollo↓とかに割とあるんですけどね」

「サーバー側の実装言語に依存せずにGraphQLスキーマを設計するという視点であれば良さそうな本だと思います👍


「探しているうちにGraphQL::Proというのを見つけた↓」「上のGraphQL Rubyの関連サイトみたいですね」「CanCanCanやPunditとのインテグレーションもできるらしい」「年900ドルか」

🔗クラウド/コンテナ/インフラ/Serverless

🔗 AWS Lambda Extensions(Publickeyより)


つっつきボイス:「お、Lambda Extensionsが正式リリースされた」「前からあったかと思ったら去年からなんですね」「以前ウォッチで扱ったのをこの図↓で思い出しました(ウォッチ20201021)」


同記事より

「Lambda Extensionsは、コンテナのライフタイムに合わせたAPIを持っていて、Lambdaと違うのはLambdaの関数が起動する前や終了後などにもアタッチして監視などを行える点ですね」「そういえばそうだったかも」「記事にDatadogやNew Relicといった監視サービスが名を連ねているのも、これができるから: 先週話したようなOpenTelemetry(20210601)のような監視を行うと、コンテナのライフサイクルも含めて監視したくなるものなんですよ」「なるほど」

「Lambda Extensionsがないとできないこともあるので、正式版になったことで安心して使えるようになったのはいいですね👍

🔗CSS/HTML/フロントエンド/テスト/デザイン

🔗 QUICがRFC 9000に


つっつきボイス:「ついにQUICにRFC番号が付与された🎉

参考: HTTP/3の基盤となる「QUICプロトコル」の標準化プロセスが完了、IETFの「RFC 9000」として - Publickey

「ところで以下の図↓を見ていて、前方互換性さえ保てれば本当はUDPもQUICで上書きしたかったんじゃないかなと想像しました」「😆」「さすがにそれをやると、IP層より上のプロトコル番号をチェックするファイアウォールのようなL4機器を通れなくなってしまいますけどね」


www.publickey1.jpより

「SCTPが流行らなかったのもそれと同じ理由じゃないかな〜」「SはセキュアのSでしょうか?」「ストリームのSですね: SCTPは、HTTP/2のようなマルチストリーム、つまり1個のSCTP接続の中に複数のストリームを同時に流せるプロトコルで、しかもネットワーク経路の異なる複数の接続を一つのSCTPストリームとして扱えます(いわゆるマルチホーム)」「へ〜」

参考: Stream Control Transmission Protocol - Wikipedia
参考: HTTP/2 - Wikipedia

「HTTP/3はまだ詳しく追っていませんが、QUICを取り込みつつ他にも機能があるようですね」「HTTP/3勉強しないといかんな〜」「よほどエッジなWebサービスとかでないとHTTP/3になったときの影響はすぐには見えてこないかもしれませんね」

参考: HTTP/3はどうやってWebを加速するか? TCP、TLS、HTTP/2の問題とHTTP/3での解決策~Fastly奥氏が解説(前編) - Publickey
参考: HTTP/3はどうやってWebを加速するか? TCP、TLS、HTTP/2の問題とHTTP/3での解決策~Fastly奥氏が解説(後編) - Publickey

🔗 Chromeの10080番ポートが使えなくなっていた


つっつきボイス:「これびっくりしましたね」「10080番、普通に使ってるのに😢

「どうして使えなくしたんでしょうか?」「記事によればNAT Slipstreaming v2という攻撃手法で10080番ポートがよく使われるかららしいですね(1191)」

参考: NAT Slipstreaming v2 攻撃とブラウザ側の対策 - ASnoKaze blog

「こんな理由でブラウザのポートが塞がれるのはちょっと不本意」「この調子で8080番や3000番もダメということになったらたまりませんよね」「気軽にfetch APIにアクセスできなくなる」「ポートを変えればいいのはわかっているんですけど」

「443->10443みたいに本来のwell-knownポートの番号を10000台に変えて使うことはよくありますよね」「私も使ってます」「この問題を踏んだときにブラウザに表示してくれるならいいけど、そうでなかったら知らない人が踏んだときになかなか原因がわからないでしょうね」「こういうことが起きる可能性もあるんだなと改めて感じました」

「このissueが掲載されているfetchって、WHATWGのリポジトリのようですけど、fetchって何でしょうか?」「fetchはWeb API標準のひとつですね: MDNのドキュメントを見れば↓、Web APIの中にFetch APIも含まれています」「なるほど」

参考: Fetch API - Web API | MDN

whatwg/fetch - GitHub

参考: Fetch Standard
参考: Web Hypertext Application Technology Working Group - Wikipedia — WHATWG

🔗言語/ツール/OS/CPU

🔗 Mouse without Borders


つっつきボイス:「Windowsで複数PCの間でマウスが共有できるんですね」「Mouse without Borders、私は割と前から使ってますよ」「サードパーティ製品ではすごく昔からありますけど、標準で入ってるんですか?」「自分でダウンロードして追加インストールする必要がありますけど、ちゃんとしたマイクロソフトのソフトウェアですね」「複数のブラウザにまたがってチェックしたいときとかに便利そう」


その後、ちょうど本日macOS Montereyが発表されましたが、それに入っている「Universal Control」がちょっとMouse without Bordersを思わせますね。

参考: Mac向けの最新OS「macOS Monterey」が発表、Macの隣にiPadを置くだけで1つのマウスですべてを操作可能に - GIGAZINE

🔗 組版処理システムTwight


つっつきボイス:「リポジトリは公開されてないようですが、TeXのようなテキストベースの組版処理システムというのが目を惹きました」「印刷業界の中の人ではなく筑波大に在籍してるんですね」「よく作ったな〜」「以下と同じ人だそうです↓」

参考: 筑波大の授業DB代替ツールを作った学生、「未踏」のスーパークリエータに認定 オープンソースの組版処理システム開発で(ITmedia NEWS) - Yahoo!ニュース

「Twightは弊社の超シリーズと方向性近いのかな?Slackで聞いてみよう」「(Slackで返信)見た感じTwightは”制作ツール”のようです: 弊社の超シリーズで作っているのはビューアが中心なのでターゲット層は違うでしょうね」「なるほど、ありがとうございます!」

参考: EPUBで縦書きや組版を綺麗に表示する電子書籍ソリューション | BPS株式会社

業界随一の仕様準拠性:EPUB3 ビューア「超縦書」Windows版 無料公開のお知らせ

🔗 unixgame.io


つっつきボイス:「BPS社内Slackにも貼りましたけど、Nokiaのベル研が出しているunixgame.ioはなかなか楽しめました」「お〜、ScratchみたいなGUIでUnixシェルワンライナーを解くんですね」「やってみるとわかりますけど、GUIなので使えるコマンドが限定されているので、普段使い慣れたコマンドを使わずに解くしかないところがパズルとして面白い」「なるほど、シェルの縛りプレイ」「おかげで普段使わないコマンドを知ることができました: 正規表現を使わずにtrコマンドで大文字小文字変換するとか」「そうそう」「シェルを普通に使いこなしている人でも楽しめるのがいいですね👍

「このGUIはどんなライブラリを使っているのかな?」「ソースを見てみるとBlockly↓というのを使ってる」「Googleのライブラリなんですね」「Blockly昔使ったことあります!」

参考: Blockly  |  Google Developers


後編は以上です。

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

週刊Railsウォッチ(20210607前編)ActiveRecord::Relationのone?とmany?が高速化、RubyKaigi Takeout 2021登壇者募集開始ほか

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

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

Ruby Weekly

Awesome Ruby

Hacklines

Hacklines

StatusCode Weekly

statuscode_weekly_banner

Publickey

publickey_banner_captured

The post 週刊Railsウォッチ(20210608後編)RubyでAppleのLZFSE圧縮データ解凍、AWS Lambda Extensionsが正式リリース、unixgame.ioほか first appeared on TechRacho.

HotwireはRailsを「ゼロJavaScript」でリアクティブにできるか?前編(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

長文につき前編と後編に分割しました。

HotwireはRailsを「ゼロJavaScript」でリアクティブにできるか?(翻訳)

サマリー

DHHたちによる新しいマジックを投入して、長らく待ち望んでいたHotwireを5分間チュートリアルよりも高いレベルで使うときがやってまいりました。Hotwireは、JavaScriptなしでモダンなWebインターフェイスを楽に構築できるライブラリ群の総称であり、今年大々的に発表されて以来大きな話題となっています。Hotwireの「HTML over-the-wire」アプローチはRailsの世界に波紋を広げていて、私が今年のRailsConfでお話ししたことも含め、これまでブログ記事やreddit書き込みやスクリーンキャストが山ほど公開されています。本記事ではコード例やテスト戦略を交えながら、Hotwireを徹底的に解説したいと思います。私の好きな某ロックバンドのアルバムタイトル「Hardwire: to self-destruct」をもじれば、さしずめ「新しい技を学ぶためにHotwireしよう(Hotwired: to learn a new tricks.)」となるでしょう。

参考: ハードワイアード…トゥ・セルフディストラクト - Wikipedia

忙しい人へ

Rails 6でHotwireする方法を手っ取り早く知りたい方は、以下のプルリクをご自由にお調べください(お知らせ不要です)。

本記事では、上のコードを詳しく解説します。本記事は、私がRailsConf 2021でお披露目した『“Frontendless Rails frontend”』を元に加筆したものです。既にRailsConfの全登壇者のスライドと動画がネットで公開されています。チケットをお持ちでない方もご覧いただけますのでご心配なく(訳注: その後スライドと動画が公開されたので以下に貼りました)。

Hotwireならこうやれる

ここ5年間の私は、ほぼバックエンド開発のみを手掛けてきました。「REST」「GraphQL API」「WebSocket」「gRPC」「データベース」「キャッシュ」など、ブラウザ画面の向こうにあるものすべてです。

フロントエンドの進化の波がことごとくビッグウェーブのように通り過ぎていきましたが、未だにWebアプリにReactだのwebpackだのを詰め込む理由が腑に落ちていません。伝統あるHTMLファーストのRails way(つまりハイウェイ😉)こそがこれまでの私のやり方でした。かつて、アプリケーションでJavaScriptを動かすためにそれ専用のMVCなりMVVMを使わなくてもよかった時代があったことを覚えていますか?懐かしいですね。その時代が、静かに復活しつつあるのです。

今や私たちは、「HTML-over-the-wire」(ちゃんとした実際の用語です)が台頭しつつあるところを目の当たりにしています。「バックエンドでレンダリングしたテンプレートをWebSocket経由ですべてのクライアントにプッシュする」アプローチはPhoenix LiveViewから始まり、StimulusReflexにある一連のgemのおかげでRailsコミュニティで支持を広げました。そして今年初めにDHHがHotwireを世に送り出したことで、ついにDHHみずから「HTML-over-the-wire」を祝福してくれました。

果たしてWeb開発は巨大なパラダイムシフトを迎えようとしているのでしょうか?「サーバーでレンダリングしたテンプレート」というシンプルなメンタルモデルに立ち返って、今度はあらゆるリアクティブなインターフェイスをきわめて手軽に扱えるようになるのでしょうか?そう願いたいところですが、希望的観測であることも承知しています。ビッグ・テックはクライアントでレンダリングするアプリケーションにさんざん投資して引き返せなくなっています。2020年代のフロントエンド開発は、求められる資格も業界も参入条件も別物です。今さら「フルスタック」に戻ることはありえません。

しかしHOTWire(HTML-over-the-wireをこんなふうに略したBasecampはなかなかですね)は、複雑化した、いやむしろ「こじれにこじれた」今どきのブラウザ向けクライアントサイドプログラミングのロケットサイエンスに代わる手段を提供してくれるのです。


Hotwireは、Web表示を制御できないAPIのみのアプリに疲れてしまったRails開発者や、週に40時間もSQLやJSONを処理することから逃れてユーザーエクスペリエンスを作りたいと思っているRails開発者が長らく待ち望んでいた、Web開発を再び楽しくする新しい風なのです。


本記事では、Hotwireを用いて既存のRailsアプリケーションに「HTML-over-the-wire」哲学を適用する方法を紹介したいと思います。私の最近の記事と同様、自作のAnyCableデモアプリケーションを実験材料に使うことにします。

このアプリはインタラクティブかつリアクティブで、Turbolinksと若干のカスタム(Java)Scriptで駆動され、直近のシステムテストのカバレッジも十分確保できている(つまり安全にリファクタリングできる)点がデモにうってつけです。このアプリの「Hotwire化」は、以下の簡単な4つのステップを踏むだけで完了します。

🔗 TurbolinksをTurbo Driveに置き換える

Turbolinksは古くからRails世界で知れ渡っていて、最初のメジャーリリースは2013年でした。しかし当時のRails開発者の間では「フロントエンドがおかしくなったらとりあえずTurbolinksを切ってみる」という経験則が広まっていました。Turbolinkのフェイクナビゲーション(pushState + AJAX) とサードパーティのJSコードの互換性を保つのは並大抵ではなかったのです。

StimulusJSが登場したとき、私はついにTurbolinksをオフにするのをやめました。StimulusJSはモダンなDOMミューテーションAPIに依存したことで、「JavaScriptスプリンクル1」との接続や切断の問題を劇的に解決したのです。コード編成やDOM操作のためにStimulusをTurbolinksと組み合わせることで、ReactやAngularの開発コストの数分の一のコストで「SPA」エクスペリエンスを楽に構築できるようになりました。

その古き良きTurbolinksが「Turbo Drive」と名を改めました。Turbo Driveは文字どおりTurboを「ドライブ」するものであり、Hotwireパッケージの中核を占めています。


(私のアプリがそうだったように)Turbolinksを既にお使いであれば、名前を変更するだけで簡単にTurbo Driveへの乗り換えられます。


package.jsonファイルでturbolinks@hotwired/turbo-railsに置き換え、Gemfile内のturbolinksturbo-railsに置き換えるだけで、作業は完了します。

初期化コードも少し変わり、以下のように1行で書きます。

- import Turbolinks from 'turbolinks';

- Turbolinks.start();
+ import "@hotwired/turbo"

Turbo Driveは手動で起動する必要がありません(止めることもできませんが)。

HTMLのdata-属性data-turbolinksをすべてdata-turboに検索置換しておく必要もあります。

唯一、変更方法を見つけるのに時間がかかったのは「フォーム」と「リダイレクト」の扱いでした。Turbolinksのときはリモートフォーム(remote: true)とリダイレクト用concernを用いてJavaScriptテンプレートに応答できました。Turbo Driveにはフォームを「ハイジャックする」機能が組み込まれているので、もうremote: trueは不要です。しかしリダイレクトのコード(正確にはリダイレクションステータスのコード)については更新が必要であることがわかりました。

- redirect_to workspace
+ redirect_to workspace, status: :see_other

HTTPレスポンスコードに303 See Otherを使うのが賢い選択です。こうすることでTurboがネイティブのフェッチAPIのredirect: "follow"オプションに依存できるようになるので、フォーム送信後に新しいコンテンツをフェッチするためのリクエストを別途開始する必要がなくなります。HTTP-redirect fetchの仕様によると、「ステータスが303で、かつリクエストメソッドがGETでもHEADでもない場合」は、自動でGETリクエストが実行されなければなりません。「ステータスが301または302で、かつリクエストメソッドがPOSTの場合」との違いがおわかりでしょうか?

その他の3xxステータスはPOSTリクエストのみに対応していますが、Railsで普段使われるのはPOSTPATCHPUTDELETEです。

🔗 Turbo Framesでフレーム化する

次は本当の意味で新しそうな機能、Turbo Framesです。

Turbo Framesによって、ページの一部分をシームレスに更新できるようになります(Turbo Driveのようにページ全体を更新するのではありません)。この振る舞いは<iframe>と非常に似ていますが、別ウィンドウを開いたりDOMツリーを作成することもなく、それらに伴うセキュリティ上の悪夢もありません。

それでは利用例を見てみることにしましょう。

AnyCableのデモアプリケーション(名前はAnyWork)は、複数のToDoリストとチャットを備えたダッシュボードを作成できます。ユーザーは、異なるToDoリスト上にある項目に対して追加や削除を行うことも、完了マークを付けることもできます。


Turbo Framesの利用例: 各項目が独自のフレーム内に置かれている

項目の完了と削除は、元々AJAXリクエストとカスタムStimulusコントローラで作り込んでいましたが、この機能をTurbo Framesで書き換えてHTMLオールインワンにすることを決意しました。

ToDoリストを分解して、項目ごとの更新を扱えるようにするにはどうすればよいでしょうか。以下のように個別の項目をフレームに変えてみましょう。

<!-- _item.html.rb -->
<%= turbo_frame_tag dom_id(item) do %>
  <div class="any-list--item<%= item.completed? ? " checked" : ""%>">
    <%= form_for item do |f| %>
      <!-- ... -->
    <% end %>
    <%= button_to item_path(item), method: :delete %>
      <!-- ... -->
    <% end %>
  </div>
<% end %>

重要な点は以下の3つです。

  • ヘルパーを用いて項目のコンテナを<turbo-frame>タグでラップし、一意のidを渡したこと(Action Viewの便利なdom_idメソッドをチェックしてください)
  • フォーム送信とフレーム内容の更新をTurboがインターセプトするためにHTMLフォームを追加したこと
  • method: :deleteオプション付きのbutton_toヘルパーを追加したこと(HTMLフォームも背後で作成されます)。

これで、フレーム内でフォームが送信されるとTurboが送信にインターセプトしてAJAXリクエストを代行し、同じidを持つフレームをそのレスポンスHTMLから抽出してフレームの内容を置き換えるようになります。


ここまでJavaScriptを一行たりとも手書きせずに動きます。


次は更新されたコントローラのコードを見てみましょう。

class ItemsController < ApplicationController
  def update
    item.update!(item_params)

    render partial: "item", locals: { item }
  end

  def destroy
    item.destroy!

    render partial: "item", locals: { item }
  end
end

項目を削除するときは、同じパーシャルからレスポンスを返す点にご注意ください。しかし削除の場合は、項目のHTMLノードを更新するのではなく削除する必要があります。どうすればよいでしょうか。空のフレームを用いてレスポンスを返せばよいのです。以下のようにパーシャルを更新してみましょう。

<!-- _item.html.rb -->
<%= turbo_frame_tag dom_id(item) do %>
  <% unless item.destroyed? %>
    <div class="any-list--item<%= item.completed? ? " checked" : ""%>">
      <!-- ... -->
    </div>
  <% end %>
<% end %>

ここで「項目に完了マークが付けられたらどうやってフォームを送信すればよいのか?」という疑問が持ち上がるかもしれません。言い換えれば、チェックボックスの状態が変わったときにフォーム送信をトリガーする方法があるかということです。これは以下のように「インライン」イベントリスナーを定義することで行なえます。

<%= f.check_box :completed, onchange: "this.form.requestSubmit();" %>

原注

submit()ではなくrequestSubmit()を使うことが重要です。requestSubmit()がトリガーする送信イベントはTurboからインターセプトできますが、submit()はそうではありません。

ただし、requestSubmit()はまだモダンなブラウザの一部でサポートされていません(caniuse.com)。polyfillの利用をご検討ください。

要するに、HTMLテンプレートをちょっぴり変更して、コントローラをシンプルなコードに書き換えるという手間をかければ、従来の機能はそのままにカスタムJavaScriptを完全に消し去れるということです。ワクワクしてきますね。

さらに一歩進めて、ToDoリストもフレームに変換できます。こうすることで、項目を追加したときにTurbo Driveページの更新から特定のノード更新に切り替わるようになります。ぜひご自宅でお試しください。

また、項目が完了または削除されたときに、ユーザーにフラッシュで「正常に削除されました」のような通知を表示したいとします。これをTurbo Frameで実現できるでしょうか。フレームでフラッシュメッセージのコンテナをラップして、更新されたHTMLを項目のマークアップと一緒にプッシュする必要がありそうです。これは当初の思いつきでしたが、うまくいきませんでした。フレーム更新のスコープはイニシエータフレームに限定されるので、フレーム外にあるものを更新できません。

あるフレームの中からページ全体の更新や別のフレームの更新をトリガーすることも一応可能ですが(Hotwireドキュメントを参照)、独立した2つのフレームを更新する方法は今のところありません。

少し調べてみると、実はTurbo Streamsを使えばできることがわかりました。

(後編に続く)

関連記事

速報: Basecampがリリースした「Hotwire」の概要


  1. 訳注: スプリンクルはアイスクリームやドーナツに振りまかれるトッピングの一種ですが、JavaScript sprinklesは主にビューで用いるスニペット的な小さいJavaScriptコードを指すと考えられます。JavaScript sprinklesという言い回しは、2015年にDHHがポッドキャストでちょっとおどけたニュアンスで使ったのがきっかけで流行ったようです。 

The post HotwireはRailsを「ゼロJavaScript」でリアクティブにできるか?前編(翻訳) first appeared on TechRacho.

HotwireはRailsを「ゼロJavaScript」でリアクティブにできるか?後編(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

長文につき前編と後編に分割しました。今回は後編です。

HotwireはRailsを「ゼロJavaScript」でリアクティブにできるか?前編(翻訳)

🔗 Turbo Streamsでストリーミングする

Turbo DriveやTurbo Framesと比べると、Turbo Streamsはまったく新しい技術です。Turbo DriveやTurbo Framesと異なり、Turbo Stremsは「明示的」です。つまり、Turbo Stremsがあっても何かが「自動的に」動くことはなく、ページ上で何をいつ更新するかは開発者が決めます。Turbo Streamsを使うには、<turbo-stream>要素が必要です。

<turbo-stream>要素の例を見てみましょう。

<turbo-stream action="replace" target="flash-alerts">
  <template>
    <div class="flash-alerts--container" id="flash-alerts">
      <!--  -->
    </div>
  </template>
</turbo-stream>

<turbo-stream>要素は、DOM IDがflash-alertsのノードを、<template>タグ内に渡された新しいHTMLコンテンツに置き換える(action="replace")操作を担当します。<turbo-stream>要素をページの好きな場所に置くと、ただちに指定のアクションを実行し、自分自身を削除します。背後ではHTMLの「Custom Elements API」が用いられています。これも、開発者の幸せのため(要するにJavaScriptを減らすため🙂)に最新のWeb APIを用いた例のひとつです。

<turbo-stream>要素について詳しく知りたい方は、stream_element.tsのソースコードをご覧ください。

Turbo Streamsは、JavaScriptテンプレートでやっていたことを宣言的に記述できるものと言えるでしょう。

// destroy.js.erb
$("#<%= dom_id(item) %>").remove();

2010年代には上のように書いていたものを、以下のように書けるのです。

<!--  destroy.html.erb -->
<%= turbo_stream.remove dom_id(item) %>

現時点でサポートされているアクションはappendprependreplaceremoveupdate(ノードのテキストコンテンツのみを置き換える)の5つのみです。こうした制約やそれを乗り越える方法については後述します。

それでは最初の問題に立ち返るとしましょう。ToDoリストの項目を完了または削除したときにフラッシュで通知を表示する方法です。

ここでやりたいことは、<turbo-frame><turbo-stream>の両方について更新を通知することです。どうすればできるでしょうか。試しに、それ用のパーシャルテンプレートを追加してみましょう。

<!-- _item_update.html.erb -->
<%= render item %>

<%= turbo_stream.replace "flash-alerts" do %>
  <%= render "shared/alerts" %>
<% end %>

ItemsControllerを以下のように少し変更します。

+    flash.now[:notice] = "Item has been updated"

-    render partial: "item", locals: { item }
+    render partial: "item_update", locals: { item }

しかし残念ながら、上の方法では期待どおりに動きません(フラッシュメッセージがまったく表示されません)。HotwireのTurboのドキュメントを詳しく読んでみると、ストリーム要素を有効にするにはHTTPレスポンスのContent-Typeヘッダーをtext/vnd.turbo-stream.htmlに設定する必要があることがわかりました。なるほど、早速やってみましょう。

-    render partial: "item_update", locals: { item }
+    render partial: "item_update", locals: { item }, content_type: "text/vnd.turbo-stream.html"

これでフラッシュメッセージが表示されるようになりました。しかし今度は項目のコンテンツが更新されません😞。Hotwireに多くを期待しすぎたのでしょうか?Turboのソースコードをみっちり読んでみると、ここでやろうとしたようなストリームとフレームのミックスはできないことがわかりました。

そして、この機能は以下の2とおりの方法で実装できることがわかりました。

  • すべてをストリームにする
  • <turbo-stream><turbo-frame>の内側に置く

2番目の方法は、HTMLパーシャルを通常のページ読み込みやTurboの更新で再利用するというアイデアに反していると個人的に思えたので、1番目の方法でやることにしました。

<!-- _item_update.html.erb -->
<%= turbo_stream.replace dom_id(item) do %>
  <%= render item %>
<% end %>

<%= turbo_stream.replace "flash-alerts" do %>
  <%= render "shared/alerts" %>
<% end %>

任務完了です!しかしこのユースケースでは、これと引き換えに新しいテンプレートを追加しなければなりませんでした。現実には、アプリケーションが育つにつれて、この手の「アドホックな」パーシャルが増えてしまいそうです。

原注: 2021-04-13の更新情報

Alex Takitaniが上のツイートでもっとエレガントな方法を教えてくれました↓。フラッシュコンテンツを更新するレイアウトを使うというものです。

この場合、Turbo Streamsのレスポンスに使う以下のアプリケーションレイアウトを定義できます。

<!-- layouts/application.turbo_stream.erb -->
<%= turbo_stream.replace "flash-alerts" do %>
  <%= render "shared/alerts" %>
<% end %>

<%= yield %>

続いて、コントローラから明示的なrenderを削除する必要があります(さもないとレイアウトが使われません: #25)。

   def update
     item.update!(item_params)

     flash.now[:notice] = "Item has been updated"
-    render partial: "item_update", locals: { item }, content_type: "text/vnd.turbo-stream.html"
   end

注意: テストコードで暗黙のレンダリングが行われるようにするために、上に対応するコントローラspecやリクエストspecにformat: :turbo_streamを必ず追加しておくこと。

続いて、_item_updateパーシャルを書き換えて Turbo Streamのupdateテンプレートにします。

<!-- update.turbo_stream.erb -->
<%= turbo_stream.replace dom_id(item) do %>
  <%= render item %>
<% end %>

クールですね。これこそRails wayです!

それでは現実の(リアルタイム)ストリーミングについてのお話に移りましょう。

Turbo Streamsは、いわゆる「リアルタイム更新」の文脈でよく引き合いに出されます(StimulusReflexともよく比較されます)。

それでは、リストの同期機能をTurbo Streams上に構築する方法を見ていくことにしましょう。


Turbo Streamsが、接続されている全クライアントでページ更新を同期する様子

Turbo登場以前は、ブロードキャストを扱うためにAction CableのカスタムチャネルとStiumulusコントローラを追加しなければならず、項目の削除と完了を取り違えないようメッセージのフォーマットにも気を配る必要がありました。つまり、メンテナンスするコードが山ほどあったのです。

Turbo Streamsなら、それらのほとんどについて面倒を見てくれます。turbo-rails gemに含まれる一般的なTurbo::StreamsChannelクラスと#turbo_stream_fromヘルパーを使うと、以下のHTMLでサブスクリプションを作成できます。

<!-- worspaces/show.html.erb -->
<div>
  <%= turbo_stream_from workspace %>
  <!-- ... -->
</div>

ここではAction Cableを用いていますが、Turbo Streamsはトランスポート方法に依存しません。他のなんちゃらケーブルを使いたければ、それ用のアダプタを書くだけで更新をクライアントにプッシュできるようになります。

既にコントローラでは更新のブロードキャストを担当する#broadcast_new_item#broadcast_changesという「afterアクション」を使っていたので、必要なのはそれらをTurbo::StreamsChannelに切り替えることだけです。

 def broadcast_changes
   return if item.errors.any?
   if item.destroyed?
-    ListChannel.broadcast_to list, type: "deleted", id: item.id
+    Turbo::StreamsChannel.broadcast_remove_to workspace, target: item
   else
-    ListChannel.broadcast_to list, type: "updated", id: item.id, desc: item.desc, completed: item.completed
+    Turbo::StreamsChannel.broadcast_replace_to workspace, target: item, partial: "items/item", locals: { item }
   end
 end

Turbo::StreamsChannelへの乗り換えはスムーズにやれましたが、惜しくもブロードキャストを検証するコントローラ用の単体テストがすべて失敗してしまいました。

残念ながら現時点のTurbo Railsはテストツールを提供していないので、よくある話ですがテストツールを自分で書かなければなりませんでした(#33659)。

module Turbo::HaveBroadcastedToTurboMatcher
  include Turbo::Streams::StreamName

  def have_broadcasted_turbo_stream_to(*streamables, action:, target:) # rubocop:disable Naming/PredicateName
    target = target.respond_to?(:to_key) ? ActionView::RecordIdentifier.dom_id(target) : target
    have_broadcasted_to(stream_name_from(streamables))
      .with(a_string_matching(%(turbo-stream action="#{action}" target="#{target}")))
  end
end

RSpec.configure do |config|
  config.include Turbo::HaveBroadcastedToTurboMatcher
end

上の新しいマッチャーを用いてテストを以下のように書き換えました。

 it "broadcasts a deleted message" do
-  expect { subject }.to have_broadcasted_to(ListChannel.broadcasting_for(list))
-    .with(type: "deleted", id: item.id)
+  expect { subject }.to have_broadcasted_turbo_stream_to(
+    workspace, action: :remove, target: item
+  )
 end

Turboによるリアルタイム更新はここまでうまく動き、多くのコードが不要になりました。


ここまで一行たりともJavaScriptコードを書いていません。これは果たして現実なのでしょうか?


それともただの幻でしょうか?この夢はいつ覚めるのでしょうか?さっそく現実に戻ることにしましょう。

🔗 Turboの向こう側へ: Stimulusとカスタム要素を使う

Turboへの乗り換え中に、既存のAPIだけでは機能が足りなくなるユースケースがいくつも立ちはだかったので、とうとうJavaScriptを書かなければならなくなるときがやって来ました。

  • ユースケース1: ダッシュボードで新しいリストをリアルタイムに追加する

上の章でやったような項目リストの例と違う部分は、マークアップです。以下のダッシュボードレイアウトを見てみましょう。

<div id="workspace_1">
  <div id="list_1">...</div>
  <div id="list_2">...</div>
  <div id="new_list">
    <form>...</form>
  </div>
</div>

末尾の要素は常に新しいリストフォームのコンテナとし、リストを追加すると#new_listノードの直前に挿入されるようにしたいと思います。先ほど、Turbo Streamsがサポートするアクションがたった5つと申し上げたのを思い出してください。以下は移行前のコードですが、どこが問題かおわかりですか?

この部分については、StimulusReflexファミリーのCableReadyが圧勝しています。CableReadyはinsert_adjacent_htmlを含む30個以上のアクションをサポートしているのです。

handleUpdate(data) {
  this.formTarget.insertAdjacentHTML("beforebegin", data.html);
}

上と同じような振る舞いをTurbo Streamsで実装するには、リストをストリームで追加した直後にリストを適切な位置に移動するハックを追加しなければなりません。それでは独自の「JavaScriptスプリンクル」を追加することにしましょう。

最初に、このタスクに形式的な定義を与えます。「ワークスペースのコンテナに新しいリスト項目が追加されるときは、新しいフォーム要素の直前に置かれるべきである」。この定義の「ワークスペースのコンテナに新しいリスト項目が追加される」タイミングで、DOMを観測(observe)して変更(change)にリアクティブに対応する必要が生じます。どこかで見覚えがありませんか?そう、前編でStimulusの話をしたときに言及したMutationObserver APIです。これを使うことにしましょう。

運のよいことに、この機能を使うために高度なJavaScriptを書く必要はありません。”使う”のはStimulus Useというライブラリだけです(トートロジーですみません)。Stimulus Useは、Stimulusコントローラで使える便利な「振る舞い」のコレクションであり、複雑な問題を解決できるシンプルなスニペット群です。ここで必要なのはuseMutationという振る舞いです。

コントローラで用いるコードはかなり簡潔で、内容も一目瞭然です。

import { Controller } from "stimulus";
import { useMutation } from "stimulus-use";

export default class extends Controller {
  static targets = ["lists", "newForm"];

  connect() {
    [this.observeLists, this.unobserveLists] = useMutation(this, {
      element: this.listsTarget,
      childList: true,
    });
  }

  mutate(entries) {
    // ストリーム経由で新しいリストを1個追加するときはエントリを1個だけにすべき
    const entry = entries[0];

    if (!entry.addedNodes.length) return;

    // childListの変更中はobserverを無効にする
    this.unobserveLists();
    // newFormをchildListの末尾に移動する
    this.listsTarget.append(this.newFormTarget);
    this.observeLists();
  }
}

ユースケース1の問題は解決しました。次に進みましょう。

  • ユースケース2: チャット機能の実装

私たちのアプリには、ダッシュボードごとにごくシンプルなチャット機能が付いていました。ユーザーが送信するメッセージはどこにも保存されない短命なかたちになっていて、リアルタイムで受信できます。

メッセージの外観はコンテキストに応じて変化させます。ユーザー自身が送信したメッセージは緑の枠で囲まれて左側に表示され、他のユーザーのメッセージは灰色の枠で囲まれて右側に表示されます。しかし、チャットに接続しているユーザー全員にブロードキャストされるHTMLはまったく同じです。

ユーザーに合わせてチャットの表示を変えるにはどうすればよいでしょうか?これはチャット的なアプリにありがちな問題で、一般には「ユーザーごとに異なるHTMLを送信する」か「受信したHTMLをクライアント側で加工する」かのどちらかで解決します。私の好みは後者なので、その方法で実装してみましょう。

カレントユーザーの情報をJavaScriptにわたすために、以下のmetaタグを使います。

<!-- layouts/application.html.erb -->
<head>
  <% if logged_in? %>
    <meta name="current-user-name" content="<%= current_user.name %>" data-turbo-track="reload">
    <meta name="current-user-id" content="<%= current_user.id %>" data-turbo-track="reload">
  <% end %>
  <!-- ... -->
</head>

次に、これらの値にアクセスする小さなJSヘルパーを書きます。

let user;

export const currentUser = () => {
  if (user) return user;

  const id = getMeta("id");
  const name = getMeta("name");

  user = { id, name };
  return user;
};

function getMeta(name) {
  const element = document.head.querySelector(
    `meta[name='current-user-${name}']`
  );
  if (element) {
    return element.getAttribute("content");
  }
}

チャットメッセージのブロードキャストにはTurbo::StreamChannelを使います。

def create
  Turbo::StreamsChannel.broadcast_append_to(
    workspace,
    target: ActionView::RecordIdentifier.dom_id(workspace, :chat_messages),
    partial: "chats/message",
    locals: { message: params[:message], name: current_user.name, user_id: current_user.id }
  )
  # ...
end

なお、以下はオリジナルのchat/messageテンプレートです。

<div class="chat--msg">
  <%= message %>
  <span data-role="author" class="chat--msg--author"><%= name %></span>
</div>

以下はカレントユーザーに応じてスタイルを変更する既存のJSコードですが、近日中に削除する予定です。

// これは使わないこと
appendMessage(html, mine) {
  this.messagesTarget.insertAdjacentHTML("beforeend", html);
  const el = this.messagesTarget.lastElementChild;
  el.classList.add(mine ? "mine" : "theirs");

  if (mine) {
    const authorElement = el.querySelector('[data-role="author"]');
    if (authorElement) authorElement.innerText = "You";
  }
}

ところで、HTMLをTurboで更新する場合はこれを別の方法で行う必要があります。もちろんuseMutationをここで使う手もあります。「現実の」プロジェクトではおそらくそうするでしょう。しかし本記事の目的は、問題を解決するさまざまな方法を紹介することです。

本セクションのタイトルに「カスタム要素」という言葉があったのを思い出してください(ここまでの話がすっかり長くなってしまい恐縮です)。カスタム要素はTurboをパワーアップするWeb APIです。これを使ってみてはどうでしょう?

手始めに、HTMLを以下のように更新します。

<any-chat-message class="chat--msg" data-author-id="<%= user_id %>>
  <%= message %>
  <span data-role="author" class="chat--msg--author"><%= name %></span>
</any-chat-message>

変更点は、data-author-id属性を追加したことと、<div>タグを<any-chat-message>というカスタムタグに置き換えたことだけです。

では、このカスタム要素を登録しましょう。

import { currentUser } from "../utils/current_user";

// これが、モダンなAPIでカスタムHTML要素を作成する方法です
export class ChatMessageElement extends HTMLElement {
  connectedCallback() {
    const mine = currentUser().id == this.dataset.authorId;

    this.classList.add(mine ? "mine" : "theirs");

    const authorElement = this.querySelector('[data-role="author"]');

    if (authorElement && mine) authorElement.innerText = "You";
  }
}

customElements.define("any-chat-message", ChatMessageElement);

完成です!これで、ページに<any-chat-message>要素を追加しておけば、カレントユーザーからのメッセージが自動的に更新されるようになりました。このコードではStimulusすら不要です。

本記事で用いた完全なソースコードについては以下のプルリクをどうぞ。

さて、本記事のトピックである「ゼロJavaScriptのリアクティブなRails」は果たして存在するかについてですが、実際にはそうではありませんでした。JavaScriptコードを大幅に削減できたものの、最終的には新しいJavaScriptコードに置き換える必要があります。しかし新しいJavaScriptコードは、従来よりも実用性が高いと言えるでしょう。機能がより高度な分、JavaScriptと最新のブラウザAPIに関する十分な知識も求められます。ここがトレードオフであることは間違いありません。

追伸: CableReadyとStimulusReflexについても同様のプルリク↓を投げていますので、よろしければHotwireと比較してお気づきになった点をEvil MartiansのTwitterまでお知らせいただければ幸いです。


火星からやってきたバックエンド(フロントエンド)スペシャリストたちの力で御社のデジタルプロダクトを強化したいとお考えのお方は、ぜひEvil Martiansのフォームまでご相談をお寄せください。

本記事の翻訳や転載についてのご相談は、まずメールにてお願いします。

関連記事

速報: Basecampがリリースした「Hotwire」の概要

The post HotwireはRailsを「ゼロJavaScript」でリアクティブにできるか?後編(翻訳) first appeared on TechRacho.

週刊Railsウォッチ(20210614前編)Pumaのgraceful restart、partial_writesコンフィグが非推奨化、Active Recordの楽観的ロックほか

$
0
0

こんにちは、hachi8833です。

週刊Railsウォッチについて

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

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

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

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

🔗 ActiveModel::AttributeSet#values_for_databaseActiveRecord::Base#attributes_for_databaseが追加


つっつきボイス:「追加されたattributes_for_databaseメソッドは、instantiateしてattributesしたものが元オブジェクトのattributesに完全に等しくなる形で取るのに使えるようですね: シリアライズ <-> デシリアライズの等価変換保証のための機能という感じ」「お〜」「before_type_castだと生値かつreaderなので触れないから、シリアライズしたもので取れるメソッドを用意したということだと思います」

参考: APIドキュメント instantiateActiveRecord::Persistence::ClassMethods


要約: レコードをinstantiateで再生成するときに使えるような、レコードの属性を返すメソッドが欲しかった。知っている限りではそういうメソッドがなかったので追加した。
今やっているシリアライズでは、レコードの属性を取り出してレコードの再作成に使えるような形でシリアライズを行おうとしている。その基準は、シリアライザを外部から見たときにMarshalと完全に同じように振舞うこと。
当初はfoo.attributes_before_type_castでシリアライズし、Foo.instantiate(attributes_before_type_cast)でデシリアライズしてみるとjson属性で渡すキーがシンボル値のときに動かないことがわかった。foo.attributes_before_type_castは属性をシンボルキーで返すが、Marshal.load(Marshal.dump(...))はjsonカラムがstring値のキーを持つインスタンスを返す。

ここで問題なのはattributes_before_type_castで、名前からもわかるように型キャストする前の属性を返しているが、自分たちはデータベース内にある属性をそのままinstantiateに渡したい。

このプルリクはそれ用のattributes_for_databaseを追加する。このプロパティは以下のように一意の形で元に戻せる。

Foo.instantiate(foo.attributes_for_database).attributes == foo.attributes

言い換えれば、このような属性を使えば元のレコードを完全に再作成できる。これはシリアライズで使うのに理想的。
それと合わせてActiveModel::AttributeSet#values_for_databaseも追加した。こちらは値を実際に変換するときに適しているだろう。
同PRより大意

🔗 PostgreSQLのactive?メソッドのSELECT 1を空クエリに変更

# activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb#L293
      def active?
        @lock.synchronize do
-         @connection.query "SELECT 1"
+         @connection.query ";"
        end
        true
      rescue PG::Error
        false
      end

つっつきボイス:「SELECT 1を空のクエリ(;)に変えるとちょっとだけ速くなったそうです」「なるほど、active?はPostgreSQLのコネクションが生きているかどうかを確認するためだけのメソッドだから、クエリを投げなくてもいいですよね」「言われてみればなるほど」「疎通確認ならこれでイケる👍」「コメントなしで;だけ見ると一瞬バグかと思っちゃいそう」

「参考に張られているGitLab issueに一般的なベンチマークが載ってる↓」「たしかに速くなってますね」

# 同issueより抜粋
# SELECT 1の場合
progress: 30.0 s, 53157.6 tps, lat 0.150 ms stddev 1.418
progress: 60.0 s, 56619.2 tps, lat 0.141 ms stddev 0.022
progress: 90.0 s, 56882.5 tps, lat 0.141 ms stddev 0.010
progress: 120.0 s, 56631.8 tps, lat 0.141 ms stddev 0.027

# 空クエリの場合
progress: 30.0 s, 66476.7 tps, lat 0.120 ms stddev 0.010
progress: 60.0 s, 66723.3 tps, lat 0.120 ms stddev 0.024
progress: 90.0 s, 66661.8 tps, lat 0.120 ms stddev 0.010
progress: 120.0 s, 66596.9 tps, lat 0.120 ms stddev 0.024

🔗 partial_writesコンフィグが非推奨化


つっつきボイス:「今後はpartial_insertspartial_updatesを使うようにとのことです」「partial_writesってそもそも何を行うのかな?」「部分書き込みに関連してそう」「stackoverflowを見るとpartial_updatesメソッドはRails 4.1で削除されたとあるな」(しばらく探す)「メソッドがないなと思ったら、partial_writespartial_updatespartial_insertsはメソッドじゃなくてRailsのコンフィグなのか」「あ、コンフィグでしたか」「insertとupdateコンフィグでパーシャル書き込みの挙動を変えられるようにするためにpartial_writesコンフィグを非推奨化にしたということのようですね」


これによってupdateとcreateで振舞いを変えられるようになる。
たとえば、コンカレントなマイグレーション実行によってカラムのデフォルト値が削除され、それによって古いスキーマを用いているプロセスがinsertに失敗するといった可能性を防ぐために、partial insertを無効にするのが望ましい状況が考えられる。
自分たちはまさにこの問題に遭遇したため、2015年から同様のパッチを走らせている。
partial_insertsをデフォルトで無効にしておきたい気もするが、何か自分の知らないメリットがあるだろうか?
同PRより大意

参考: 3.7 Active Recordを設定する — Rails アプリケーションを設定する - Railsガイド

config.active_record.partial_writes: 部分書き込みを行なうかどうか(「dirty」とマークされた属性だけを更新するか)を指定する論理値です。データベースで部分書き込みを使う場合は、config.active_record.lock_optimisticallyで楽観的ロックも使う必要がある点にご注意ください。これは、更新がコンカレントに行われた場合に、読み出しの状態が古い情報に基づいて属性に書き込まれる可能性があるためです。デフォルト値はtrueです。
railsguides.jpより

🔗 音声チャネル関連の改修


つっつきボイス:「動画音声がらみの改修が2つありました」「動画に音声が入っているかどうかのmetadataと↓Analyzer::AudioAnalyzerクラスが追加されたのね: 音声なしの動画もあるので、これを入れたのはわかる」

# activestorage/lib/active_storage/analyzer/video_analyzer.rb#L26
    def metadata
-     { width: width, height: height, duration: duration, angle: angle, display_aspect_ratio: display_aspect_ratio }.compact
+     { width: width, height: height, duration: duration, angle: angle, display_aspect_ratio: display_aspect_ratio, audio: audio? }.compact
    end

🔗 Active Storageのsigned_idexpires_in:キーワード引数を渡せるようになった

# activestorage/app/models/active_storage/blob.rb#L154
- def signed_id(purpose: :blob_id)
+ def signed_id(purpose: :blob_id, expires_in: nil)
    super
  end

参考: signed_idActiveStorage::Blob


つっつきボイス:「expires_in:を指定できるようになったんですね: find_signedで期限切れの日時を絞れるようになってる↓」「お〜、これうれしいかも」「いい改修だと思います👍

# activestorage/test/models/attachment_test.rb#103
  test "getting a signed blob ID from an attachment with a expires_in" do
    blob = create_blob
    @user.avatar.attach(blob)

    signed_id = @user.avatar.signed_id(expires_in: 1.minute)
    assert_equal blob, ActiveStorage::Blob.find_signed!(signed_id)
  end

  test "fail to find blob within expiration date" do
    blob = create_blob
    @user.avatar.attach(blob)

    signed_id = @user.avatar.signed_id(expires_in: 1.minute)
    travel 2.minutes
    assert_nil ActiveStorage::Blob.find_signed(signed_id)
  end

🔗Rails

🔗 Railsアプリのディレクトリ編成(Ruby Weeklyより)


つっつきボイス:「Railsを10年やってきた結果こういうディレクトリ構成に落ち着いたという感じの記事みたいです」「ざっと眺めた感じでは、Railsガイドとだいたい同じようなことを形を変えて書いているように見えますね」「特に変わったことはやってなさそうかも」

参考: Ruby on Rails ガイド:体系的に Rails を学ぼう


  • concernは使っている。悪く言われることもあるけど、concernそのものではなく「よくないconcern」が問題なのだと思う(関連記事)。
  • バックグラウンドジョブ: ジョブワーカーもコントローラ同様できるかぎり薄くしている(関連記事)。ワーカーにいろんなことをさせるのではなく、何かを呼ぶだけにとどめておくべきと信じている。
  • JavaScriptはできるだけ使わないようにしている。使うときはStimulusでPOJO(plain old JavaScript Object)にすることが多い。
  • テスト: RSpecを使っている。TDDの学習には随分時間をかけたが全然使ってない。テストのほとんどはmodel specとsystem spec。
  • Service Object: 人気はあるようだけど、自分は使わない。普通にOOPでやっている。多くの人はService Objectを手続き的なコードとしてモデリングしているが、自分は宣言的なオブジェクトでモデリングしている(関連記事)。
    同記事後半より大意

🔗 Pumaをgraceful restartする(Ruby Weeklyより)


つっつきボイス:「Pumaのgraceful restartにもいろいろあるよという記事のようですね」

  • regular restarts: connections are lost, and new ones are established after booting the new application process
  • hot restarts: connections are not lost but remain to wait for the new application server workers to boot
  • phased restarts: current connections finish with old workers and new workers handle new ones
    同記事より

「3番目のphased restartは、自分たちが一般的によく使う意味でのgraceful restartに相当するでしょうね: コネクションがないワーカーは即リスタートするけど、コネクションが生きているワーカーはコネクションが終了するまでリスタートしない」「そうそう、終わるまで待ってくれるリスタート」

「1番目のregular restartはコネクションが切れるタイプで、問答無用でリスタートする: これはgracefulではないでしょうね」「そう思います」「記事でも2番めと3番目をgracefulと呼んでました」

「2番目のhot restartは何だろう?」「アプリケーションサーバーの新しいワーカーが起動するまではコネクションを残す、現在のリクエストは終了させて新しいワーカーで同じものを再度動かそうとする、という感じみたい」「phased restartに近そう」「それぞれにシグナルがあるのね↓」

# 同記事より
$ kill -SIGUSR2 25197 # hot restartの場合
$ kill -SIGUSR1 25197 # phased restartの場合

pumactlでリスタート時にhotやphasedを指定できるともありますね」「へ〜」「他にも、tcp/9293をLISTENしてHTTPで叩けるcontrol-urlを使うと、/restart/phased-restartにHTTPリクエストを送信してリスタートできる」「APIっぽい使い方もできるんですね」「Puma 1.2.2から--controlで同じことができたようですが、puma 5から--control-urlに変わったんですね」「最近のPumaに詳しくなれそうな記事👍

puma/puma - GitHub

「Webサーバー再起動の考え方は、制御の仕方こそ変わっても、ここ10年ほどアーキテクチャレベルでは大きくは変わっていない感じがしますね」「そうそう、Apacheが2.0だった頃を思い出します」「ApacheのMPMでも同じようなことやってた」

参考: マルチプロセッシングモジュール (MPM) - Apache HTTP サーバ バージョン 2.2

🔗 Hotwireスライド


つっつきボイス:「先週のウォッチ20210607で紹介した@willnetさんのHotwire記事↓に、元になったスライドがあったことに今頃気づきました😅」「これと同じかどうかわかりませんが、イベントでも見たような覚えありますね」

参考: まるでフロントエンドの“Rails” Hotwireを使ってJavaScriptの量を最低限に - ログミーTech
参考: クライアント側のJavaScriptを最小限にするHotwire - ログミーTech

「今さらですけどHotwireってJSのフレームワークなんですね」「そうですね、TurboはRailsと強く結合している感じはあります」

HotwireはRailsを「ゼロJavaScript」でリアクティブにできるか?前編(翻訳)

🔗 Active Recordのoptimistic lock(RubyFlowより)


つっつきボイス:「optimistic locが楽観的ロックで、pessimistic lockが悲観的ロックだったかな」「日本語だとわかった😅」「楽観的ロックは、ロックを取得できてもトランザクションが成功するとは限らない(実行開始後に競合して失敗する可能性がある)、悲観的ロックは、ロックを取得できればデッドロックやタイムアウトにならない限りトランザクションが成功する(ロックの取得に失敗する可能性はある)」「そうそう」

参考: 排他制御(楽観ロック・悲観ロック)の基礎  - Qiita
参考: APIドキュメント ActiveRecord::Locking::Optimistic


# 同記事より: ActiveRecord::StaleObjectErrorでエラーを出すようにした例
# PATCH/PUT /products/1 or /products/1.json
def update
  respond_to do |format|
    if @product.update(product_params)
      format.html { redirect_to @product, notice: "Product was successfully updated." }
      format.json { render :show, status: :ok, location: @product }
    else
      format.html { render :edit, status: :unprocessable_entity }
      format.json { render json: @product.errors, status: :unprocessable_entity }
    end
  rescue ActiveRecord::StaleObjectError => _error
    @product.errors.add(:base, "Oops. Looks like the product has changed since you last opened it. Please refresh the page")
    format.html { render :edit, status: :unprocessable_entity }
    format.json { render json: @product.errors, status: :unprocessable_entity }
  end
end

参考: APIドキュメント ActiveRecord::StaleObjectError


前編は以上です。

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

週刊Railsウォッチ(20210608後編)RubyでAppleのLZFSE圧縮データ解凍、AWS Lambda Extensionsが正式リリース、unixgame.ioほか

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

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

Rails公式ニュース

Ruby Weekly

RubyFlow

160928_1638_XvIP4h

The post 週刊Railsウォッチ(20210614前編)Pumaのgraceful restart、partial_writesコンフィグが非推奨化、Active Recordの楽観的ロックほか first appeared on TechRacho.

週刊Railsウォッチ(20210615後編)RubyのRBSを理解する、シンボルがGCされないとき、Terraform 1.0リリースほか

$
0
0

こんにちは、hachi8833です。

週刊Railsウォッチについて

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

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

🔗Ruby

🔗 Rubyの新しい型注釈システムRBSを理解する(Ruby Weeklyより)


つっつきボイス:「記事はRBSとは何かから始めて詳しく解説している感じですね」「JetBrains IDEもRBSに対応するようになりましたし、RBSの記事が増えるのはいいですね👍

ruby/rbs - GitHub

🔗 Codewarsで見かけたRBS

「ところで、今日のWebチーム内ミーティングの発表で取り上げられていたCodewars↓という最近のプログラミング練習サイトはUIも含めてなかなかよくできていて、そこのRubyの練習問題の中にRBSが書かれているものもあったんですよ」「お〜、RBSがそんなところに!」

「全部の問題というわけではなさそうでしたが、たまたま開いたRubyの問題文でRBSを説明に使っていました: RBSのこういう使い方を見たのは初めて」「面白そう〜」「Rubyのバージョンも 2.5.0と3.0.0を選べるんですね」「CodewarsはブラウザエディタのキーバインドをVimやEmacsにもできるところが個人的にポイント高い: そのままだとブラウザのショートカットとぶつかることもあるようですけど」「あるあるですね」


CodewarsのGitHubリポジトリにissue trackerも見つけました↓。

codewars/codewars.com - GitHub

以下はつっつき後に見つけたツイートです。

🔗 YJITの現在(Ruby Weeklyより)

Shopify/yjit - GitHub


つっつきボイス:「Shopifyが作っているもうひとつのRuby JITであるYJITは以前もウォッチで取り上げましたね(ウォッチ20210420)」「Yはyet anotherのY」「MJITの後で始まったプロジェクトだったと思いますが盛んに活動してますね」


同記事より

「この記事の途中でEuruko 2021でのMatzが引用されていて↓、しばらくはRuby言語仕様への追加を控えるそうです」「なるほど、言語仕様を変更するとJITなどのソース自体を解析して処理するような実装にも影響が出てしまうので、そういう方針も大事なのかもしれませんね」

Matz has stated in his recent talk at Euruko 2021 that Ruby would remain conservative with language additions in the near future. We believe that this is a wise decision as rapid language changes can make it difficult for JIT implementations to get off the ground and stay up to date. It makes some sense, in our opinion, for Ruby to focus on internal changes that will make the language more robust and deliver competitive performance in the future.
同記事より


以下のEuroko 2021の動画↓(1:16:50で頭出し)で、Ruby 3.1の言語仕様変更は極力控えめにするとMatzが話しています。

🔗 死なないシンボル(Hacklinesより)


つっつきボイス:「メタプロでシンボルがGCされないことがあるという記事のようです」「mortalが『死すべき定めの』で、タイトルで使われているのがその反対のimmortal(不死身の)ですね」

# 同記事より
$ ruby inmortal.rb
{:T_SYMBOL=>28, :T_STRING=>10136}
{:T_SYMBOL=>38, :T_STRING=>10228}
{:T_SYMBOL=>38, :T_STRING=>7793}  # GC後もシンボルの個数が減っていない

respond_to_missingmethod_missingの中でメソッド新規作成を回避すると、不死身のシンボルが発生しなくなり、インスタンスで必要なものだけになります。
同記事より大意

🔗 その他Ruby

🔗DB

🔗 pg_query: PostgreSQLのSQL文字列からPostgreSQL準拠のパーサーツリーを取り出すgem(Ruby Weeklyより)

pganalyze/pg_query - GitHub


つっつきボイス:「pganalyze.comというサービスが出しているgemだそうです」「こういうパーサーツリー↓を出力できるということは、PostgreSQL内部の挙動を調べるのに使うのかな」

# 同リポジトリより
#=> #<PgQuery::ParserResult:0x00007fb69a958820
  @query="SELECT 1",
  @tree=<PgQuery::ParseResult:
    version: 130002,
    stmts: [
      <PgQuery::RawStmt:
        stmt: <PgQuery::Node:
          select_stmt: <PgQuery::SelectStmt:
            distinct_clause: [],
            target_list: [
              <PgQuery::Node:
                res_target: <PgQuery::ResTarget:
                  name: "",
                  indirection: [],
                  val: <PgQuery::Node:
                    a_const: <PgQuery::A_Const:
                      val: <PgQuery::Node:
                        integer: <PgQuery::Integer: ival: 1>
                      >,
                      location: 7
                    >
                  >,
                  location: 7
                >
              >
            ],
            from_clause: [],
            group_clause: [],
            window_clause: [],
            values_lists: [],
            sort_clause: [],
            limit_option: :LIMIT_OPTION_DEFAULT,
            locking_clause: [],
            op: :SETOP_NONE,
            all: false
          >
        >,
        stmt_location: 0,
        stmt_len: 0
      >
    ]
  >,
  @warnings=[]>

includeフォルダにあるヘッダーファイルは同じくpganalyzeのlibpg_queryのものみたい: これでPostgreSQLのライブラリを参照してクエリパーサーの内部を解析して正規化したツリーを返すネイティブ拡張のように見えますね」「サンプルコード↓にもコネクションの記述がないので、PostgreSQLサーバーがなくても動きそう」

pganalyze/libpg_query - GitHub

# 同リポジトリより
parsed_query = PgQuery.parse("SELECT * FROM users")

# Modify the parse tree in some way
parsed_query.tree.stmts[0].stmt.select_stmt.from_clause[0].range_var.relname = 'other_users'

# Turn it into SQL again
parsed_query.deparse
#=> "SELECT * FROM other_users"

「以下の1番目と2番目のクエリのfingerprintが一致しているのは、SQL文字列では差異があっても、パーサーレベルでは値部分が抽象化されたり、コメントなどSQL実行と無関係なものが取り除かれたりして一致するということでしょうね↓」

# 同リポジトリより
PgQuery.parse("SELECT 1").fingerprint

=> "50fde20626009aba"

PgQuery.parse("SELECT 2; --- comment").fingerprint

=> "50fde20626009aba"

# Faster fingerprint method that is implemented inside the native C library
PgQuery.fingerprint("SELECT ?")

=> "50fde20626009aba"

「このツールを使えばお手製パーサーを作らずにPostgreSQL内部のクエリパーサーの挙動を確実に追えるのがよさそう👍」「こんなツールを作る人たちがいるのか〜」「Ruby以外にもGoやNodeやPython版もありますね」

🔗クラウド/コンテナ/インフラ/Serverless

🔗 Fastlyの障害(Publickeyより)


つっつきボイス:「Fastlyの障害はニュースでも取り上げられていました」FastlyのようなCDNサービスはこれまであまり一般に知られていませんでしたけど、これをきっかけにCDNの認知度が高まるかも」

「今回のFastlyの障害でAmazonのECサイトの画像が配信されなくなっていたことで(購入とかは普通にできていたようです)、Amazon自身のECサイトがFastlyを使っていることがわかったのが個人的に発見でした」「あら、自社でCloudFront持っているのに使ってないのか〜」「でもAmazonは今回の障害でCloudFrontを使って復旧させてましたけどね」「最初からそうすればよさそうですけど」「もしかするとFastlyの方が安いなどの事情があって、CloudFrontをバックアップ用にしているのかもしれないと想像してみました」

参考: Amazon CloudFront(グローバルなコンテンツ配信ネットワーク)| AWS

🔗 VarnishのVCL

「FastlyはVarnishをホスティングしてエッジ配信しているサービスですが、自分はほとんど使ったことがなかったので、この機会にちょっと調べてみると、ひとつ驚いたのがVarnishのVCLという設定ファイルをFastlyのユーザーが書けるという点」「え、なかなか大胆ですね」

参考: Varnish (ソフトウェア) - Wikipedia
参考: Fastly VCL 入門 - Qiita

「VCLはnginxのコンフィグと似たような感じで宣言型言語っぽく書けるんですが、ifなどの条件式や正規表現なども書けるのでコンフィグできる範囲が結構大きい」「ユーザーが設定間違えたらまた何か起きたりして」「Fastlyの顧客側の設定変更がきっかけでバグが顕在化したそうですが、VCLの記述自由度が高いことによるかどうかは推測の域を出ません」

参考: CDNのFastly、世界的な障害の原因は「ソフトウェアのバグ」 - CNET Japan

「コンフィグに正規表現が使えるということは、もしかすると最近流行りのCatastrophic Backtrackingが起きたのかも」「VCLの正規表現はそれほど強力ではなかったと思ったんですが、今見るとPerl互換正規表現(PCRE)構文を使っていると書かれているので↓、バックトラックを使うタイプだとしたらその可能性もあるかもしれませんね」

参考: VCL 正規表現早見表 | Fastly ヘルプガイド

はじめての正規表現とベストプラクティス10: 危険な「Catastrophic Backtracking」前編

🔗 CDNを冗長化できるか

「今後こういう事故に備えるにはどうしたらいいんでしょうね」「WebサービスでCDNを使おうとする場合、CDNサービスを複数使って冗長化しようとするととても複雑になりますし、常にスタンバイさせておかないといけないのでコストもかさみますね」「それもそうか」「マルチCDNでディザスタリカバリ的なことをやるのはあまり現実的ではないかなと思います: FastlyのVCLと同じ設定を、他のCDNでも別の言語で書くのも大変ですし」

「どうしてもマルチCDN構成にしたいなら、エッジ側でのコンピューティングを諦めてコンテンツキャッシュだけ行うようにすれば、DNSを切り替えるだけでCDNを切り替えられるでしょうね」「なるほど」「たとえばAWSのRoute 53にはヘルスチェック機能があるので、理論上はマルチCDNをヘルスチェックして失敗したら切り離すことはできそうかなと思いました」

参考: Amazon Route 53(スケーラブルなドメインネームシステム (DNS))| AWS

🔗 Terraform 1.0がリリース(StatusCode Weeklyより)


つっつきボイス:「Terraformって今まで1.0じゃなかったんですか」「リリースノートによると0.15.5から1.0のバージョンアップで、特に大きな変更はなさそうなので、節目の意味で記念にリリースしたのかも」「とにかくおめでとう🎉」「以前のRuboCopもなかなか1.0が出ませんでしたけど、それでもみんな使ってるという意味ではTerraformもそうですね」

🔗 Terraformよもやま話

「ちなみにTerraformコマンドは実行する際のTerraformバージョンをtfstateファイルのバージョンと合わせる必要があります: HashCorpのTerraform Cloudを使うか、事情があってローカルで実行するならtfenvなどの任意バージョンを切り替えられるツールを使うと便利です」「なるほど」

tfutils/tfenv - GitHub

参考: Infrastructure as code with Terraform and GitLab | GitLab

「Terraformでしくじると大変なので、特にtfstateファイルをS3などに保存する設定にしておくのが重要: Terraformを最後に実行した人のtfstateファイルが残っていないと後でつらいことになります」「お〜」「なお、使ったことはありませんが、GitLabにもTerraformのインテグレーションがあります↓」

参考: Terraformのtfstateをざっと理解する - Qiita
参考: Infrastructure as code with Terraform and GitLab | GitLab


後編は以上です。

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

週刊Railsウォッチ(20210608後編)RubyでAppleのLZFSE圧縮データ解凍、AWS Lambda Extensionsが正式リリース、unixgame.ioほか

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

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

Ruby Weekly

Hacklines

Hacklines

StatusCode Weekly

statuscode_weekly_banner

Publickey

publickey_banner_captured

The post 週刊Railsウォッチ(20210615後編)RubyのRBSを理解する、シンボルがGCされないとき、Terraform 1.0リリースほか first appeared on TechRacho.

Viewing all 1405 articles
Browse latest View live