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

週刊Railsウォッチ(20200114前編)config_forのbreaking change、Active Storage variantをDBでトラッキング、SprocketsとWebpackの違いほか

こんにちは、hachi8833です。すっかり遅ればせながらあけましておめでとうございます🌅。2020年代も週刊Railsウォッチをよろしくお願いします🙇。


つっつきボイス:「お〜リニューアルへの反応が、と思ったら今日のツイートでしたか😆: 一応昨年12月26日にリニューアルしたんですけどね☺️」「😆」

TechRachoのサイトデザインをリニューアルしました

  • 各記事冒頭には⚓でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • 毎月第一木曜日に「公開つっつき会」を開催しています: お気軽にご応募ください

今回は第18回公開つっつき会を元にお送りいたします。ご参加いただいた皆さん、ありがとうございます!

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

昨年末の公式情報を中心に見繕いました。

新機能: Active Storage variantをデータベースでトラッキングするようになった

現在はvariantをリクエストされるとそれがサービス内に存在するかどうかをチェックし、ない場合はその場で生成して保存し、再利用に備えている。
この存在チェックによるvariant提供の遅延は容認できなくなることがある。サードパーティのサービスでは通常の環境で最大500msに達する。しかも、アップロード前に特定のキーでオブジェクトが存在するかどうかをチェックすると、S3の最終consistencyがトリガーされる。

Amazon S3では、全リージョンのS3バケットで新しいオブジェクトをPUTSするときのread-after-write consistencyを提供していますが、1つ注意点があります。オブジェクト作成前に(オブジェクトが存在するかどうかの確認のために)キー名をHEADやGETでリクエストすると、S3はread-after-writeの最終consistencyを提供します。

つまり、あるvariantを最初にリクエストして生成しS3に保存すると、以後そのvariantのサービスURLへのリダイレクトが失敗し続ける可能性がある。これによって画像が散発的に破損する。
このPRは存在チェックを行わないことで上の懸念を修正する。あるvariantが初めてリクエストされれば生成して保存し、以後はアプリケーションのデータベースでトラッキングする。これにより、ストレージサービスへのリモート呼び出しを行わずに、variantが生成済みかどうかを確認できるようになった。
同PRより大意


つっつきボイス:「ここで言ってるvariantは、画像をアップロードしたときに複数のサイズを生成して指定できるようにしているアレのことかなと」「サムネイル画像とかに使うヤツですね😋」「Active Storageでvariantが使えるようになってる?🤔」「Rails 6で入ってたと思います(ウォッチ20191111)」「たしかにvariant機能がないと画像アップロードとしてはとてもつらくなるでしょうし😆」

「結構差分大きいな😳」「17ファイルも更新されてますね」「改修内容もわかりみ: hoge_bigみたいな画像があるかどうかを毎回S3にAPI経由で問い合わせて、なかったら作るとかやってるとそのチェックが重くなるでしょうね☺️」「ふむふむ」「チェック先がファイルシステムとかだったら一瞬でしょうけど、API経由では遅くなるからデータベースに存在情報を持たせておけば、どんなストレージを使っていてもまずデータベースを参照することになるから高速化が期待できる」「なるほど!」「よくあるやり方☺️」

「ただこれをやると、今度はデータベースとの不整合が発生したときに面倒なことになりますけどっ😆」「S3にあるvariantを誰かが削除しちゃったりとか」「今日出席できなかったkazzさんは、データベースよりもメモリ上に置きたいな〜って」「それは無理でしょ😆: データベースが複数台構成で動かなくなると思います」「あ、そうか😳」「置き場所としてはRedisやmemcachedあたりか、さもなければデータベース上ぐらいでしょうね☺️」

参考: Redis
参考: memcached - Wikipedia

「あとサービスによってはvariantの生成がめちゃ重くなることがありますし、Pixivみたいな画像中心のアプリだとvariantが膨大になるでしょうし」「想像つきます😆」「Active StorageもそうですしCarrerWaveでもShrineでもPaperclipでもそうなんですけど、variantをいつ生成するかというのが共通のテーマになってますね: アップロード時にvariantをプリレンダリングするのか、あるいはここでやってるみたいにオンデマンドで生成するのかはだいたい悩ましいポイントです😭」「たしかに」「画像をバンバン扱うようなシステムではこの辺が大事🧐」

「プリレンダリングならいったん生成が終われば後は速いんですけど、たとえば後でvariantの定義が変わると全部再生成しないといけなくなるのが厄介😇」「あ〜」「bigとsmallに後からmiddleを足すとか😆: 画像の数やサイズが大きいとめっちゃ重くなりますし😆」「それ用のrakeタスク的なものはだいたい必要になるので探せばたぶん何かしらあると思います」

shrine gem

「shrineって、これも画像アップロード用のgemでしょうか?」「はい: 今だと一番いい感じに動いてくれるアップローダでしょうね」「おぉ〜😂」「Shrineは評判もいいですね」「欲しい機能はひととおりあるしメンテもされてますし☺️: 去年バージョン3が出たんだったかな」


shrinerb.comより


「ちなみにBPSでやっているRails製の漫画のサービス↓では、漫画の画像サイズが大きいのでたしかプリレンダリングでやってたと思います: オンデマンドでやると最初の人のアクセスが激重になるので」「へ〜」(以下PDFやepubの話で盛り上がる)

TagBuilderに条件値をハッシュで渡せるようになった

このPRはTagBuilderに条件値を渡すサポートを追加する。https://github.com/JedWatson/classnamesにヒントを得て、GitHubアプリケーションで使っているロジックを切り出して使っている。
RailsのビューでCSSクラスの適用に条件をつけるのはよくある手法だが、以下のように三項演算子の式展開だらけになりがち。

content_tag(
  "My username",
  class: "always #{'sometimes' if current_user.special?} another"
)

TagBuilderにハッシュのサポートを追加したことで、今後は以下のように書ける。

content_tag(
  "My username",
  class: ["always", "another", { 'sometimes' => current_user.special? }]
)

同PRより大意


つっつきボイス:「この書き方はどこかで見たような🤔: あ、去年見かけたVue.jsだかReactっぽい書き方できるヤツ?(ウォッチ20191216)」「それっぽいですね」「従来は条件付けした文字列を無理やり式展開で押し込んでたけど、配列の中で条件をハッシュで渡せるようになったと」「あ、そういうことですか😳」「たしかに論理的にはこっちで書ける方がいいですよね😋: 式展開と違ってコードのトラッキングもやりやすいですし👍」「下の書き方の方が好き❤️」「rubocopのチェックとかも効きやすいでしょうし☺️」

Rails::Application#config_forが共有設定をネスト含みでマージするようになった

自分は環境固有の設定のためにgemをいくつか使っているが、これをRailsの機能に統合できればと思う。しかし自分の設定はネストが深く、現在のRails::Application#config_forでは設定をマージできない。

# config/application.yml
shared:
  foo:
    bar:
      baz: 1
development:
  foo:
    bar:
      qux: 2

# 現在
config_for(:application)[:foo][:bar] #=> { qux: 2 }
# こうしたい
config_for(:application)[:foo][:bar] #=> { baz: 1, qux: 2 }

これがbreaking changeであることは承知している。デフォルトの振る舞い変更が難しいとかあれば、オプションとして制御できるようにするのはどうだろう。
同PRより大意


つっつきボイス:「これはRails標準のconfigの変更か: 書いてあるとおり超breaking changeですね😇」「おぉ?」

「以前からsettingslogicというgemやrails_configというgemがあるんですけど、それでも同じようなことをやれますね」「昔そのあたりをブログに書きましたけど探さないでください🤣」「🤣」

編集部注: rails_config gemは現在configに名前が変わりましたが、紛らわしいので本記事では旧名のrails_configで表記します。

Rails設定用gemの挙動

「settingslogicってたしかこんな挙動(メモ書きを始める)」

# default.yml
- production
  - categories
    - hoge
      - piyo
    - huga
    - foo

# production.yml
- production
  - categories
    - hoge

「settingslogicは、キーがかぶるとそれ以下を全部消して上書きする: rails_configの場合はキーがかぶるとマージ(と言っていいかどうかですけど)的に扱う」「まあマージでいいんじゃないでしょうか☺️」「そしてsettingslogicはこの挙動が事故りやすいという😆」「そうそうっ😤」「default.ymlを更新するとproductionで上書きされて反映されなかったりするというのがありがちなパターン😇: 追加したはずのキーがなくてproductionでエラーを吐くとか😆」「地獄感ありあり☠️」

# settingslogicを使う場合
- production
  - categories
    - hoge

# rails_configを使う場合
- production
  - categories
    - hoge
      - piyo
    - huga
    - foo

「rails_configならdefaultの方で少なくともエラーにならないように設定しておいてからproductionの値を設定するんですけど、settingslogicはproductionに入れ忘れると死ぬ😇: そして従来のconfig_forも上書きする挙動なのでそれと同じことが起きます」

breaking changeの影響は?

「たぶん今回の変更は同じキーがあったときにマージしたいということなので、config_forをrails_config風味にしたいということかなと😆: 思いっきりbreaking change」「どのぐらい影響出るんでしょう?🤔」「う〜ん、設定にもよるので予測難しいけどこれで死ぬことが割とあるかもしれませんね👹: こういうconfigはgemとかでカスタマイズされてることもよくあるので」「う〜む😅」

「このPRはもうmasterにマージされちゃってますし😇」「設定を上書きじゃなくてマージする挙動になるならそんなに事故らないかな?」「でもたとえばdevelopmentとproductionで異なるSMTPサーバーを指定していた場合に、マージされるとdevelopment用のSMTPサーバーの設定もproductionに残ることになるから、これはこれで事故るんじゃないかな〜🤔」「あ〜ごめんなさい事故りますねたぶん😅」「知らないうちに設定が変わるのコワイ😱」「こんな変更をあっさりマージしちゃって大丈夫?っていう気持ちにちょっとなりますね...」「ちょっとドキドキする😓」「ドキドキする😇」

「settingslogicとかを使ってる人にも影響あるんでしょうか?」「いえ、settingslogicなんかはRails本体のconfigとは違うところで設定するので関係ないですね」「そうでしたか😳」「Rails本体とはクラスの空間が違うので両者を共存することはできます☺️」

「このPRがmasterにマージされたのは昨年12月9日か〜」「6.0.2.1には入ったんだろうか?😅」

後で調べると6-0-stableにはまだ入っていませんでした↓。

参考: rails/CHANGELOG.md at 6-0-stable · rails/rails

config_forではたとえばsharedの設定をdevelopmentで上書きできなくなるということですよね?」「ということでしょうね〜」「まあビジネスアプリの重要な設定をRailアプリ本体のconfigに直接書くことは普通しないと思いますし、自分はそういうときはrails_config gemとかを使うので、少なくとも自分のアプリは踏まないと信じる🤣」「🤣」「ただ他のgemが踏むかもしれませんけどっ😆」

Active SupportのRange#include?をdateやtimeの値に利用することが非推奨化された

dateやtimeのrangeに値が含まれているかどうかのチェックにRange#include?を利用することは非推奨化される。今後はRange#cover?を使うことが推奨される。
CHANGELOGより大意

参考: cover? -- ActiveSupport::CompareWithRange


つっつきボイス:「今後はTimeWithZoneではcover?を使えと」

# activesupport/lib/active_support/core_ext/range/include_time_with_zone.rb#L11
    def include?(value)
-     if self.begin.is_a?(TimeWithZone)
-       cover?(value)
-     elsif self.end.is_a?(TimeWithZone)
+     if self.begin.is_a?(TimeWithZone) || self.end.is_a?(TimeWithZone)
+       ActiveSupport::Deprecation.warn(<<-MSG.squish)
+         Using `Range#include?` to check the inclusion of a value in
+         a date time range is deprecated.
+         It is recommended to use `Range#cover?` instead of `Range#include?` to
+         check the inclusion of a value in a date time range.
+       MSG
        cover?(value)
      else
        super
      end
    end
  end
end

「deprecatedの理由は何だろう?🤔」「issueに書いてありそう↓」「そっちでしたか😅」「include?だと実装の問題があるから使って欲しくないということみたい」

この拡張はどちらかというと削除したい。include?は、Range.to_aを返さない値で動くべきではない。現在の拡張を非推奨にして代わりにcover?を使うよう周知をお願いしてもよい?
同issueコメントより

ActiveSupport::NumberHelper::RoundingHelper:round_modeが追加

    ```ruby
    number_to_currency(1234567890.50, precision: 0, round_mode: :half_down) # => "$1,234,567,890"
    number_to_percentage(302.24398923423, precision: 5, round_mode: :down) # => "302.24398%"
    number_to_rounded(389.32314, precision: 0, round_mode: :ceil) # => "390"
    number_to_human_size(483989, precision: 2, round_mode: :up) # => "480 KB"
    number_to_human(489939, precision: 2, round_mode: :floor) # => "480 Thousand"
    485000.to_s(:human, precision: 2, round_mode: :half_even) # => "480 Thousand"
    ```

つっつきボイス:「数値ヘルパーに:round_modeオプション、たしかに欲しいヤツ😋」「切り上げ切り捨て四捨五入ですね」「Ruby自体にそういう機能はありますけど😆」「その機能をnumber_to_なんちゃらヘルパーのオプションとして使えるようにしたんですね」「ビューの責務を考えたら、ヘルパーのオプションによる丸め処理はあくまでビューの表示のためのものだというのを示すのにはいいのかもしれませんね: 数値自体を丸めてるんじゃなくて表示の書式を整えてるだけだよ、みたいな☺️」「このオプション、後で思い出せるかな😆」

Rails

「WebpackをSprocketsのように使うな」


つっつきボイス:「Webpacker周りを追っててこの記事にたどりつきました: ここではお集まりの皆さまだけにこの記事のドラフト翻訳をお見せしますが、近日TechRachoで公開しますのでお楽しみに😋」「SprocketsとWebpackerは別物ですから😆」「今更ではありますけど、そのあたりをまとめて説明してくれてるのがいいなと思いました😋」

参考: アセットパイプライン - Railsガイド
参考: rails/sprockets-rails: Sprockets Rails integration

「記事によると、Sprocketsでないとできないことがあるようです」「元々SprocketsとWebpackは役割が違いますからそうでしょうね: 個人的にはSprocketsとWebpackが両方あると混乱しそうだからやめときたいですけど😆」「Sprocketsを残すかどうかずっとモヤモヤしてたんですけど、これで踏ん切りがつきそうです😆」

「RubyでJSのコードを扱いたい場合、特にRailsのコンテキストで扱いたい場合はたぶんSprocketsの方が向いていますね: WebpackはRailsのコンテキストでは動かないので🧐」「まさにそのあたりの話が記事にありました」「Railsのアプリケーションconfigを使ってJSの自動生成部分に値を埋め込みたい、みたいなのをやりたいときとかありますよね: たとえばアセットのプリコンパイル結果にRailsの動作モードを入れたいなんてのは(やるべきではありませんが)、Sprocketsじゃないとやれませんね☺️」

「そういえばRailsがWebpackに対応して間もない頃にダイジェストとか設定とかをWebpackでどう扱うかが議論になってた覚えがありますね😆: Railsのコンテキストにある値をWebpackでどうやってJSに渡すか、そのためにJSONを作って渡すとか何とか」「ちょっと無理やり感😆」「でもやりたいシチュエーションはありますから☺️」「WebpackだとRailsのconfigの値を渡せないので、Railsのconfigと重複するのを承知でWebpackのconfigにも値を書くのか、とか」「それは気持ち悪いです😆」「でしょ😆」「この記事に『SprocketsをWebpackのように使うな』って書いてあるのはわかります😋」

「RailsガイドにもWebpackerとSprocketsをどう扱うかという情報がほぼなくて困ってました😢」「まあRails 6でやるならもうSprocketsのことは忘れてWebpackしか存在しない世界だと思って進めるのがいいんじゃないでしょうか😆」「それもそうですね😂」「記事の人も、自分は併用を極力避けるけどSprocketsでないとできないことがあるのは知っておく価値はあるという感じでした」「そういう記事をひととおり読んでみれば、併用するのはやめておこうという気持ちになれますよ🤣」


「ついでにyarn autocleanで不要なファイルをクリーンアップできるらしいことを知りました↓」「CIとかデプロイでは普通に使いそうですね☺️」

Zeitwerkへの移行


つっつきボイス:「短い記事です」「これ系の記事はいっぱいありますし☺️」「最後の方にある以下をRailsコンソールで実行するとdevelopmentモードでもZeitwerkの読み込みをチェックできるそうです」「読み込み順序に問題があればこれで確認できますね😋」

# 同記事より
Zeitwerk::Loader.eager_load_all

ネストあり/なしルーティングをRESTfulらしく実装する


つっつきボイス:「なるほどこの辺のお話↓」


同記事より

「ネステッドだと上から4番目の/magazines/:magazine_id/ads/:idみたいになりますよね: 知ってる人はとっくに知ってますが😆」「ふむふむ」「そしてネステッドとそうでないルーティングが両方定義されている場合はネステッドの部分を判別して書かないといけない: 以下のif !params[:landlord_id]はネステッドのときだけ通る↓、とか🧐」「なるほど」「この辺も知ってる人には今更ですが😆」

# 同記事より
def show
  if !params[:landlord_id]
    id = params[:id]
    tenant = Tenant.find(id)
    render json: tenant
  else
    id = params[:id]
    landlord_id = params[:landlord_id]
    tenant = Landlord.find(landlord_id).tenants.find(id)
    render json: tenant
  end
end

「ただ個人的にはネステッドとそうでないルーティングを両方書くのは事故の元なのでやりたくありませんけどっ😆」「😆」「同じリソースに複数のルーティングがあるのってどうかと思いますし😆」

「両方書かないといけなくなるシチュエーションってあるんでしょうか?」「本来は1つのURLでアクセスできるべきなんですけど、まああるとすればSEOの都合でもっと短いURLにしてくれという注文が来るとか😆、あとはフロントエンドのフレームワークが別のURLを要求してくるとかですかね〜」「なるほど!」「そもそも自分はネステッドルート好きじゃありませんし、おすすめもしませんが🤣」

「ネステッドで厄介なのは、たとえば記事にあるこういうURL↓でlandlord_idの部分が今後変わったりしたときですね😇」「おぉ?🤔」「普通であれば、あるtenantのidに紐付けられるlandlord idが変われば上のURLはアクセスできなくなりますけど、下ならパーマネントなURLとしてtenantのidにアクセスできますね...あ、そういうパーマネントURLも用意して欲しいという要件はありそう」「たしかに」「ネステッドとそうでないルーティングってそこまで考えずに設計されてることもよくあったりしますし🤣」

# 同記事より
Nested Path: localhost:3000/landlords/:landlord_id/tenants/:id
Un-nested Path: localhost:3000/tenants/:id

GitHub Actions記事


つっつきボイス:「GitHub Actionsはまだ触れていないけど速くて安くていい感じみたいですね😋」

「記事にもありますけどEOLが切れると致命的なんですよね: 業務システムだと古いRubyを使ってるとか普通によくあるので、ある日突然CIが動かなくなるとか😇」「😇」

初心者向けRailsパフォーマンス最適化のコツ5つ


つっつきボイス:「タイトルにもあるようにパフォーマンス初心者(noobs)向けの記事です」「定番の嵐😆」「せやなの嵐😆」

見出しより大意:

  • 1. 半端に最適化しないこと
  • 2. プロファイリングとベンチマークをやってからにすること
  • 3. すごい技を繰り出すよりも足を引っ張る要素を取り除くのが肝心
  • 4. ほぼほぼデータベース(の使いこなし)でつまずく
  • 5. 速くなったっぽいなら速いのだ
  • 6. ほぼほぼRubyのせいではない

「4.といえば、データベースを使いこなせてなくてmap使って遅くなってるケースをちょくちょく見かけますね🤣」「あ〜ありそう🤣」「データベース側でsumすれば瞬殺なのにわざわざオブジェクトに落としてmapするとか🔰」

「『ほぼほぼRubyのせいではない』も😆」「PHPでもどの言語でもちゃんと書けば速くなりますし🚄」「言語のせいにしてはいかんと☺️」

2020年のベストクロスプラットフォームモバイル開発ツール


つっつきボイス:「あ〜モバイル開発か」「出たC#😆」「他にも懐かしいものがチラホラ」(以下延々)

見出しより:

「なぜか最後はRails😆」「RubyMotionっていうRubyでモバイルアプリを書けるアレかと思ったら違った😆」

その他Rails

つっつきボイス:「Ryan Biggさんは以下の翻訳記事↓でお世話になりました」

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


つっつきボイス:「見えないテキストボックスって、Railsガイドで言うハニーポットフィールド(おとりのフィールド)のことだと思うんですが、だんだん通用しなくなってきたんですね」「この辺のCAPTCHA方面は今いろいろアツいですよ😆」「自前で作るよりGoogleのCAPTCHA入れとけばいいんじゃね?😆」

参考: Rails セキュリティガイド - Railsガイド
参考: CAPTCHA - Wikipedia

「ハニーポットって一般用語?」「invisible_captcha gemにもhoneypot:ってあるな↓」「ここに入力があったら例外吐けばいいんだから実装はそんなに大変じゃなさそうですけど☺️」

# https://github.com/markets/invisible_captchaより
class TopicsController < ApplicationController
  invisible_captcha only: [:create, :update], honeypot: :subtitle
end

「あとはどのぐらい効果があるかですね: ブルートフォース的なのはこれではじけるとしても悪意のあるアクセスをどのぐらい防げるのかな、とか」「ないよりはマシぐらいなのかな🤔」「ブラウザのオートコンプリートで入っちゃったりしますかね?🤔」「hiddenフィールドだったら入らないでしょうけど普通の入力フィールドだと入っちゃうでしょうね: invisible_captchaのREADMEにもオートコンプリートはオフにしろと書いてありますし」「ほんとだ」


つっつきボイス:「同書は初心者よりベテランにおすすめだそうです🎉」「特集1はたしかに初心者向けだけど特集2のWebpack/Sprocketsあたりから急にレベル高くなってる😆」「Rails経験があってもフロントの知見がないと手こずりそう😆」「途中からトップギア🏎」



前編は以上です。

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

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

Rails公式ニュース


CONTACT

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