- Ruby / Rails関連
週刊Railsウォッチ(20200525前編)2020年のRailsマストgem 19個、スライド『Fat Modelの倒し方』、AR mergeのrewhereオプションを変更ほか
こんにちは、hachi8833です。JavaScriptが25歳の誕生日を迎えたそうです🎉。10日そこそこで最初のプロトタイプを作ったとは😳。Rubyはちょっとだけ年上なんですね。
25 years ago this month the first prototype of JavaScript was created over ten days. Most likely May 6-15, 1995.
Read about how it happened in “JavaScript: The First 20 Years” https://t.co/aCMFx28GX0@BrendanEich
— Allen Wirfs-Brock (@awbjs) May 14, 2020
- 各記事冒頭には⚓でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
- 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
今回のつっつき会は日中の社内勉強会枠で行いました。
⚓Rails: 先週の改修(Rails公式ニュースより)
今週は以下のコミットリストから見繕いました。Changelogのdiffも見るのが効率よいとやっと気づきました😅。
⚓Active Recordで署名付きidをサポート
署名付きidベースでのレコード検索サポートを追加した。署名付きidは不正防止の照合済みidのことで、
expires_in
で期限を設定したりpurpose
でスコープを指定したりできる。これが特に有用なのは、パスワードリセットやメール認証などでレコードとやりとりできる署名付きidの所有者が欲しいが、特定の期限を設けたい場合。
同PRより大意
# 同PRより
signed_id = User.first.signed_id expires_in: 15.minutes, purpose: :password_reset
User.find_signed signed_id # => nil, since the purpose does not match
travel 16.minutes
User.find_signed signed_id # => nil, since the signed id has expired
travel_back
User.find_signed signed_id, purpose: :password_reset # => User.first
User.find_signed! "bad data" # => ActiveSupport::MessageVerifier::InvalidSignature
つっつきボイス:「DHH自らのプルリクです」「サインドid?」「purpose: :password_reset
みたいに書けるのね」「いろんなidにマッピングされる、有効期限付きのidを発行できる機能がサポートされたように見える: インターフェイスが便利そう👍」
「この署名付きidって何でしょう?」「生のidを外部にさらさないために一時的に使えるidですね🧐」「有効期限を付けられるし、そんな感じかな」「そうそう😋」
「このfind_signed
↓なんか、署名付きidを渡すと生のidが取れるし😋」「単に取り直してるだけという😆」
# activerecord/lib/active_record/signed_id.rb#42
def find_signed(signed_id, purpose: nil)
if id = signed_id_verifier.verified(signed_id, purpose: combine_signed_id_purposes(purpose))
find_by id: id
end
end
「SHA256使ってる〜」「他の部分はこの署名付きidの管理周りでしょうね」「idや期限はどこに保存してるんだろう?🤔」「復号したら時刻が入っててアプリケーションでそれをチェックしたりして、って何も見ないで言ってますけど😆」「まあやろうと思えばできますね☺️」
参考: SHA-256(Secure Hash Algorithm 256-bit)とは - IT用語辞典 e-Words
「テストも1分で期限切れにしてtravel
で2分進めたらnilが返るようになってる↓」
# activerecord/test/cases/signed_id_test.rb#32
test "fail to find signed record within expiration date" do
signed_id = @account.signed_id(expires_in: 1.minute)
travel 2.minutes
assert_nil Account.find_signed(signed_id)
end
「このプルリク、いいね🎉がいっぱい付いてますね」「こういう一時的なid発行はユースケースとしてよくあるヤツ🧐」「今までだとgem使ったり自力で実装したりという感じだったんでしょうね」「こういう機能があることを知ってれば使うかも😋」「自力でやらずに済む😂」
「Deviseを使うとこういうのが入ってきますね: インターフェイスはちょっと違いますけど😆」「なるほど」「Deviseだと何とかtokenというカラムがあった覚えがあります」
⚓each_pair
やeach_value
にブロックを渡さない場合にEnumeratorを返すようになった
# actionpack/lib/action_controller/metal/strong_parameters.rb#L363
def each_pair(&block)
+ return to_enum(__callee__) unless block_given?
@parameters.each_pair do |key, value|
yield [key, convert_hashes_to_parameters(key, value)]
end
end
...
def each_value(&block)
+ return to_enum(:each_value) unless block_given?
@parameters.each_pair do |key, value|
yield convert_hashes_to_parameters(key, value)
end
end
つっつきボイス:「改修前はブロックを渡さないとエラーになってたんですね😳」「ハッシュの場合とインターフェイスを合わせたということですね👍」「なるほど!」「普通のハッシュならこうやって呼べますので☺️」「こういうのが直っていくのはいいですね〜😋」
irb(main):001:0> ActionController::Parameters.new(foo: "bar").each_pair
=> #<Enumerator: <ActionController::Parameters {"foo"=>"bar"} permitted: false>:each_pair>
irb(main):002:0> ActionController::Parameters.new(foo: "bar").each_value
=> #<Enumerator: <ActionController::Parameters {"foo"=>"bar"} permitted: false>:each_value>
「この間社内でもちょっと話題になったんですけど、RailsのActive Recordとかって、インターフェイスはたしかにEnumerable
と同じなのに、実際にはEnumerable
モジュールをインクルードしてないものがちょくちょくあったりするんですよね😅」「あ〜わかります😆」「それと似た感じで、Enumerable
モジュールはインクルードしてないけど、あたかもしているかのような挙動に近づけたんでしょうね☺️」
⚓raise_on_missing_translations
設定をビューとコントローラで統一
# actionview/lib/action_view/railtie.rb#L43
config.after_initialize do |app|
ActiveSupport.on_load(:action_view) do
app.config.action_view.each do |k, v|
+ if k == :raise_on_missing_translations
+ ActiveSupport::Deprecation.warn \
+ "action_view.raise_on_missing_translations is deprecated and will be removed in Rails 6.2. " \
+ "Set i18n.raise_on_missing_translations instead. " \
+ "Note that this new setting also affects how missing translations are handled in controllers."
+ end
send "#{k}=", v
end
end
end
つっつきボイス:「今までconfig.action_view
にあったi18n(国際化)設定をconfig.i18n
にまとめたのね↓」「あ、設定の話なのか😳」
# actiontext/test/dummy/config/environments/development.rb#L57
# Raises error for missing translations
- # config.action_view.raise_on_missing_translations = true
+ # config.i18n.raise_on_missing_translations = true
「訳文をフェッチして取れなかった場合の振る舞いなんでしょうね☺️」「まあi18nの設定をビューとコントローラで別々にすることはまずないから、まとめちゃってもいいと思いま〜す😋」「普通に考えて、コントローラではi18nエラーを出さないけどビューでは出すなんてしませんし😆」「一緒にしたいですよね😆」
⚓:rewhere
オプションとmerge
を組み合わせたときの挙動を変更
merge
のオプション:rewhere
で、merge
される側の条件をそのまま置換するようサポートされた。
Changelogより
つっつきボイス:「rewhere
って何これ?🤣」「スゴい名前🤣」「へ〜、rewhere
自体は前からあったみたい」「あ、そうなんですね😅」「名前からしてreorder
と同じ頃にrewhere
が実装されたのかも🤔」「この改修は、Active Recordのmerge
でrewhere
を使ったときの挙動をあるべき姿に戻したということでしょうね☺️」
「まあunscope
よりはrewhere
の方が直感的ではありますし😋」「たしかに😋」「条件が既に設定されてしまっているwhere
とかorder
とかをやり直したいときって割とよくありますけど、unscope.where
って書くよりはrewhere
って書く方が早いし😆」「where
を付け直したいときあるといえばある😆」
「ま、自分はActive Recordのインスタンスを取り直す方がいいんじゃないかと思いますけど、どうしても付け直したい人がいるんでしょうし🤣」「😆」
関連PR: #39236
relation.merge
メソッドは、merge
される側の条件を置き換えることもあるが、relation.rewhere
を使わない場合はmerge
する側とされる側の条件が両方残ることもある。
このままでは、merge
される側の条件が置き換えられるのかどうかをmerge
の結果で予測するのがきわめて難しい。
既にある方法のひとつは、merge
する側のリレーションでrelation.rewhere
を使うことだが、これもmerge
でリレーションが使われるかどうかを事前に知るのが難しい(merge
のワンタイムリレーションを除く)。
この問題を修正するために、merge
の:rewhere
オプションで、merge
される側の条件をそのまま置換することをサポートするよう提案したい。
このオプションは、rewhere
でないリレーションがrewhere
リレーションとして振る舞うようになる。
同PRより大意
david_and_mary = Author.where(id: david.id..mary.id)
# マージする側とされる側の両方に競合条件が存在する
david_and_mary.merge(Author.where(id: bob)) # => []
# マージされる側の条件がrewhereによって置き換えられる
david_and_mary.merge(Author.rewhere(id: bob)) # => [bob]
# マージされる側の条件がrewhereオプションによって置き換えられる
david_and_mary.merge(Author.where(id: bob), rewhere: true) # => [bob]
「where(id: david.id..mary.id)
した状態でwhere(id: bob)
をマージすると、bobがない結果セットからさらに絞り込む形になるので結果は空になると」「それが本来望ましい動作だから、出るはずのないbobがwhere(id: bob)
で取れたら確かにびっくりしますし😳」「でrewhere(id: bob)
だとやり直せるからbobが取れると」
「一番下にrewhere: true
っていうオプションがある」「このオプションが必要になるときって一体😆」「この辺の挙動を理解してないとrewhere: true
って付けるのを思いつかなさそう😆」
「このscope.unscope!
しているあたり↓が改修ポイントかな」「breaking changesにならない範囲で修正したんでしょうね😋」
# activerecord/lib/active_record/relation/query_methods.rb#L704
def rewhere(conditions)
- attrs = []
scope = spawn
-
where_clause = scope.build_where_clause(conditions)
- where_clause.each_attribute do |attr|
- attrs << attr
- end
- scope.unscope!(where: attrs)
+ scope.unscope!(where: where_clause.extract_attributes)
scope.where_clause += where_clause
scope
end
「rewhere
、自分はあんまり使わないかな〜: そもそもrewhere
を使わないといけないようなコードの書き方しませんし🤣」「それやったらスコープのライフサイクル長過ぎ〜🤣」
⚓ENVのBACKTRACE
オプションでバックトレースの削除をオフにできる
つっつきボイス:「Railsフレームワークのバックトレースを消せるようになったのかと思ったら、バックトレースのクリーンアップを環境変数で止められるようになったのね」「二重否定っぽくてややこしい😆」「これはなかなかいい機能😋」
「最初ドキュメントにハウツーを追加したのかなと思ったら、よく見ると下にコードも追加されてました↓😅」「BACKTRACE=1
という書式がちょい微妙😆」「backtrace_cleaner
にもともとremove_silencers
というのがあって、それをオンにできるようになったと」「デフォルトではバックトレースを削除すると😆」「ほとんど隠し機能😆」
# railties/lib/rails/generators/rails/app/templates/config/initializers/backtrace_silencers.rb.tt#L6
-# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code.
-# Rails.backtrace_cleaner.remove_silencers!
+# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code
+# by setting BACKTRACE=1 before calling your invocation, like "BACKTRACE=1 ./bin/rails runner 'MyClass.perform'".
+Rails.backtrace_cleaner.remove_silencers! if ENV["BACKTRACE"]
フレームワークのコードや埋もれたissueをデバッグするときは、一時的にバックトレースの削除をオフにしておきたくなることが多い。これを環境変数で設定できれば楽だ。
同PRより大意
⚓Rails
⚓2020年度のマストgem 19
つっつきボイス:「今年のマストgemどんなんかな?」「RuboCopは入ってませんが、たぶんあって当然なのかなと😆」
「bootstrap-emailがある」「これおいしいヤツでしょうか?😋」「HTMLメールはBootstrapのスタイルを付けられないので、それを効かせられるようにするやつなのかなと😆」「HTMLメールのスタイルは普通style
に入れないといけないですし☺️」
「先週話題にしたlockboxも入ってますね↓(ウォッチ20200519)」
「rolifyって初めて見たかも↓」「いわゆるユーザーの認可(authorization)とか権限周りを管理するgemですね」「cancancanが隣にあるし😆」「has_role?
するヤツ」
# 同リポジトリより
user.has_role?(:moderator, @forum)
#=> false # ユーザーが別のフォーラムのモデレータである場合
「Faradayもいらっしゃる↓」「Faraday今も現役なのね〜」「更新もされてますね」
- リポジトリ: lostisland/faraday: Simple, but flexible HTTP client library, with support for multiple backends.
「まあこれまでとそんなに大きく違うわけではなさそうかな」「bootstrap-emailがマストかどうかはさておき😆」「メール使わなければ要らないですし😆」「突飛なgemはなさそう😋」「Railsもそれだけ枯れてきたというか成熟したんでしょうね☺️」
⚓interactor-rails: RailsでInteractorパターン
- リポジトリ: collectiveidea/interactor-rails: Interactor Rails provides Rails support for the Interactor gem.
# 同リポジトリより
class AuthenticateUser
include Interactor
def call
# 何かする
end
end
つっつきボイス:「interactor-railsは取り上げたことがありそうでなかったので」「interactor gemをRailsですぐ使えるようにした感じみたい」「interactorはさっきのマスト19 gemにも載ってましたし😆」「どうせRailsで使うことが多いでしょうから統合したんでしょうし😆」「使いたいならどうぞ〜😋」
interactor gemは以下の翻訳記事にも載っています。
⚓銀座Railsスライド『Fat Modelの倒し方』
RailsのModelは別にDBと1対1と決まっていないので、普通のクラスをapp/modelsに置いても良いのですよ。app/models/foo_form.rbなど #ginzarails
— 神速 (@sinsoku_listy) May 15, 2020
つっつきボイス:「この間の銀座Railsのスライドですね」「ファットモデルはRailsの『滑らない話』の定番😆」「後で読もっと😋」
「そういえば神速さんはなるべくgemを使わない主義って聞いた覚えありますね」「その辺も含めて、やっぱり設計周りは人によってさまざまだな〜って思いながら聞いてましたし、こういう設計周りの話はいろんな人のを聞いておくのがいいと思います👍」「設計で『これしかない』って思い込んだらコワいですね😅」
以下はウォッチ公開後に見つけたツイートです。
1年前に「Ruby on Railsの正体と向き合い方」というテーマで登壇したときに、時間の関係で言及できなかった「コードレベルの向き合い方」の詳細が綺麗に整理された上でまとまっていて、いたく感動してしまった。おすすめ / Fat Modelの倒し方 / how to deal with fat model https://t.co/0vMubQfiLb
— (やさいち|yasaichi) (@_yasaichi) May 25, 2020
⚓Awesome Code: リポジトリのコードをオートフォーマットするサービス
つっつきボイス:「GitHubだけじゃなくてBitBucketやGitLabにも対応してるようです」「リポジトリCIのフックに挟めるとかそういう感じですね😋」「おいくらかな?」「オープンソースは無料で、デフォルトブランチのみのStartupプランだと月10ドルか〜💰」「サービスでやる方が楽ならやってもいいかも」
「その下の紹介記事によると、Awesome CodeはRuboCopやrufoなどをいいとこ取りして、面倒な部分を考えずにやれるようです」「こういうlintの設定ってプロジェクトによって違いますけどね😅」「そういや最近rufo使ってない😆」「自分も外しました😆」「その辺の設定をIDEのlintとなかなか同じにできないのが面倒なんですよね😢、全員が同じIDE使っていればそれでやれるんですけど」「たしかに」
⚓書籍『The Rails 6 Way』
The Rails 6 Wayhttps://t.co/107dHDyXqI #ginzarails
— シロ (@shiroemons) May 15, 2020
つっつきボイス:「銀座Railsでこの本の話題が出ていたので」「Rails 6のRails Wayね」「と思ったら執筆の進捗20%でした😆」「はよ出さないとRailsが6.1になっちゃう😆」「頑張って欲しいです😂」「でもRails 6はマルチDB周りとかが固まってなかったりしますし」「Active StorageはRails 6より前からですけど、そこそこ定着した感ありますね☺️」
目次より:
- 強力・スケーラブルでRESTに沿ったバックエンドサービスを構築する
- Action Controllerを用いる複雑なフローのプログラミング
- Active Recordによるモデル/リレーションシップ/操作の表現、およびActive Recordの高度なテクニック
- マイグレーションでデータベーススキーマをスムーズに進化させる
- Action Viewでフロントエンドを構築
- アセットパイプラインやWebpackerによるアセットの構築
- キャッシュやTurbolinksによるパフォーマンスやスケーラビリティの最適化
- hamlテンプレートで生産性を向上させる
- SQLインジェクション/XSS/XSRFなどの攻撃からシステムを守る
- Action MailerやAction Mailboxによるメール統合
- Action CableでWebSocketsベースのリアルタイムなブラウザ動作を有効にする
- バックグラウンド処理でレスポンスを改善する
- JSONをやりとりするAPI専用バックエンドプロジェクトを構築する
- Active Storageでファイルをクラウドに保存する
- Action Textで洗練されたWYSIWYGを追加
- マルチプルデータベースの活用
⚓その他Rails
- 元記事: Test TruffleRuby in CI by eregon · Pull Request #2797 · rubygems/rubygems
- 元記事: Happy 10th birthday to Everyday Rails! | Everyday Rails
つっつきボイス:「1つ目はTruffleRubyのCIテストが全部通ったそうです」「あ、Rubygemsのテストね😆」「TruffleRubyでRailsが通ったのかと思った😆」
つっつき後にTruffleRuby 20.1.0がリリースされました。
「2つ目はRailsでRSpecやる人にはお馴染みの『Everyday Rails』が10周年だそうです🎉」「🎉」「そういえば昨日大学の講義で自己紹介するときに自分もRailsやって10年経ってるって気づきましたし😆」「10年!」「もうPHPフレームワークより長かった😆」
「以下はついさっき見つけたjnchitoさんのチュートリアル動画です↓(30分)」
前編は以上です。
おたより発掘
先日の発表『Fat Modelの倒し方』を週刊Railsに取り上げてもらいました〜😉 ウォッチ 週刊Railsウォッチ(20200525前編)2020年のRailsマストgem 19個、スライド『Fat Modelの倒し方』、AR mergeのrewhereオプションを変更ほか https://t.co/0FFvA8gu6o
— toshimaru (@toshimaru_e) May 25, 2020
バックナンバー(2020年度第2四半期)
週刊Railsウォッチ(20200518前編)スライド『令和時代のRails運用』、Ruby 3.0のキーワード引数変更リスケ、Action CableのCLIほか
- 20200512後編 RubyのPStoreライブラリ、Lambda StoreのサーバーレスRedisは有能、Amazon Linux 2のライブパッチほか
- 20200511前編 Rails 6.0.3リリース、rails newに–masterオプションが追加、system specとfeature specの違いほか
- 20200428後編 Rubyのバックトレース順序が戻る、KubernetesでRailsをスケール、セキュリティソフト入れますか?ほか
- 20200427前編 Railsで避けたい8つのミス、ridgepole導入の注意点、RDS ProxyのPostgreSQL対応ほか
- 20200421後編 Ruby 2.4サポート終了、Ruby 3の右代入演算子、GitHubコア機能無料化ほか
- 20200420前編 anyway_config gemでRails環境設定、ShopifyのLiquidテンプレートエンジン、書籍『Beyond the Twelve-Factor App』ほか
- 20200414後編: Ruby 3で”endレス”メソッド定義構文が追加、ECMAScript 2020の新機能、紛失防止デバイスほか
- 20200413前編: 最近macOSでRailsが遅い、トランザクションでのreturnやbreakなどが非推奨化、Rails監視ツールリスト2020年度版ほか
- 20200407後編: RubyのTracePointでデバッグ、Rubyとモナド、Gitノウハウ集、リモートワークほか
- 20200406前編: Ruby 2.7.1セキュリティ修正、RailsビューHTMLにテンプレート名を出力、Action Mailboxテスト用フォーム改良ほか
今週の主なニュースソース
ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。