- Ruby / Rails関連
週刊Railsウォッチ: GitHubによるdisable_joins解説、MemoWise gemでメモ化、RailsのDDoS攻撃対策ほか(20210719前編)
こんにちは、hachi8833です。
お知らせ: 今週木金は祝日のため、来週は週刊Railsウォッチの代わりに通常記事を公開します。
🔗Rails: 先週の改修(Rails公式ニュースより)
以下の公式更新情報から見繕いました。今回は小粒でわかりやすい改修が多い印象です。
🔗 config_for
でyamlのshared
ルート要素が配列を受け取れるよう修正
# railties/test/application/configuration_test.rb#2060
test "config_for works with only a shared root array" do
set_custom_config <<~RUBY
shared:
- foo
- bar
RUBY
app "development"
assert_equal %w( foo bar ), Rails.application.config.my_custom_config
end
test "config_for returns only the env array when shared is an array" do
set_custom_config <<~RUBY
development:
- baz
shared:
- foo
- bar
RUBY
app "development"
assert_equal %w( baz ), Rails.application.config.my_custom_config
end
つっつきボイス:「yamlファイル内の各環境設定のルート要素にハッシュではなく配列を指定するとエラーになる問題が修正されたようですね」「なるほど」「通常はハッシュを渡すと思うので、これを踏むことは少なそうかな」
# railties/lib/rails/application.rb#L249
if shared
- config = {} if config.nil?
- if config.is_a?(Hash)
+ config = {} if config.nil? && shared.is_a?(Hash)
+ if config.is_a?(Hash) && shared.is_a?(Hash)
config = shared.deep_merge(config)
+ elsif config.nil?
+ config = shared
end
end
🔗 filepath
にファイルがない場合のエラー出力を改善
- PR: Handle error when file does not exist at filepath by tywhang · Pull Request #41283 · rails/rails
つっつきボイス:「ActionView::TemplateRenderer
で出すエラーの種類を増やして、絶対パスでない場合に適切なエラーを出すようにしたんですね」
# actionview/lib/action_view/renderer/template_renderer.rb#L14
private
# Determine the template to be rendered using the given options.
def determine_template(options)
keys = options.has_key?(:locals) ? options[:locals].keys : []
if options.key?(:body)
Template::Text.new(options[:body])
elsif options.key?(:plain)
Template::Text.new(options[:plain])
elsif options.key?(:html)
Template::HTML.new(options[:html], formats.first)
elsif options.key?(:file)
if File.exist?(options[:file])
Template::RawFile.new(options[:file])
else
- raise ArgumentError, "`render file:` should be given the absolute path to a file. '#{options[:file]}' was given instead"
+ if Pathname.new(options[:file]).absolute?
+ raise ArgumentError, "File #{options[:file]} does not exist"
+ else
+ raise ArgumentError, "`render file:` should be given the absolute path to a file. '#{options[:file]}' was given instead"
+ end
end
elsif options.key?(:inline)
handler = Template.handler_for_extension(options[:type] || "erb")
format = if handler.respond_to?(:default_format)
handler.default_format
else
@lookup_context.formats.first
end
Template::Inline.new(options[:inline], "inline template", handler, locals: keys, format: format)
elsif options.key?(:renderable)
Template::Renderable.new(options[:renderable])
elsif options.key?(:template)
if options[:template].respond_to?(:render)
options[:template]
else
@lookup_context.find_template(options[:template], options[:prefixes], false, keys, @details)
end
else
raise ArgumentError, "You invoked render but did not give any of :partial, :template, :inline, :file, :plain, :html or :body option."
end
end
🔗 credentialエディタ呼び出し時にファイルパスに含まれるスペースを正しく扱えるよう修正
- PR: Handle paths with spaces when editing credentials by ghiculescu · Pull Request #42728 · rails/rails
つっつきボイス:「credentialへのパスがエスケープされていなかったのか: これは修正すべきでしょうね」「修正で追加されたShellwordsは、そういえば以前も取り上げましたね(ウォッチ20200225)」「ShellwordsはRubyのデフォルトgemで、Rubyで実行するsystem
コマンドに渡す文字列に変数を埋め込む必要がある場合は必ずShellwordsでエスケープしないといけません: 今回はたまたまcredentialのパスにスペースが含まれているとcredentialエディタの起動でエラーになったことで見つかったんでしょうね」
# railties/lib/rails/commands/credentials/credentials_command.rb#L95
def change_credentials_in_system_editor
credentials.change do |tmp_path|
- system("#{ENV["EDITOR"]} #{tmp_path}")
+ system("#{ENV["EDITOR"]} #{Shellwords.escape(tmp_path)}"
end
end
参考: Shellwords.#shellescape (Ruby 3.0.0 リファレンスマニュアル)
参考: Rubyから外部コマンドを実行するときはShellwordsモジュールが便利 - ブログのおんがえし
🔗 Action Cableのbroadcast
でログ出力を300文字までに変更
つっつきボイス:「なるほど、ログが溢れないようにtruncate(300)
を追加したんですね」「気持ちわかります」「Action Cableのやりとりに画像のような大きなファイルが含まれているとログが大量に発生するのはあるある」
# actioncable/lib/action_cable/server/broadcasting.rb#L42
def broadcast(message)
- server.logger.debug { "[ActionCable] Broadcasting to #{broadcasting}: #{message.inspect}" }
+ server.logger.debug { "[ActionCable] Broadcasting to #{broadcasting}: #{message.inspect.truncate(300)}" }
payload = { broadcasting: broadcasting, message: message, coder: coder }
ActiveSupport::Notifications.instrument("broadcast.action_cable", payload) do
encoded = coder ? coder.encode(message) : message
server.pubsub.broadcast broadcasting, encoded
end
end
🔗 fixtureの読み込み後に外部キーをverifyするよう変更
つっつきボイス:「Railsのfixture機能では参照の外部キーチェックを行わないんですよ: 以下で言うとpirate: redbeard
はpirates.ymlに存在しないんですがバリデーションされないので通る」「ふむふむ」「この改修では外部キーのバリデーションを行うようにしたようですね」
# 同PRより
# test/fixtures/parrots.yml
george:
name: "Curious George"
pirate: redbeard
# test/fixtures/pirates.yml
blackbeard:
name: "Blackbeard"
「修正後はバリデーションに失敗するとエラーをraiseするようになっている↓」
# activerecord/lib/active_record/fixtures.rb#L640
if ActiveRecord.verify_foreign_keys_for_fixtures && !conn.all_foreign_keys_valid?
raise "Foreign key violations found in your fixture data. Ensure you aren't referring to labels that don't exist on associations."
end
🔗 fixtureとfactory_bot
「ただfixtureが外部キーでバリデーションエラーを出すようになると少し面倒になるんですよ: 複雑な依存関係を持つデータをfixtureで扱う場合、バリデーションエラーにならないためにfixtureの適切な読み込み順序なども考えないといけなくなってしまう」「あ〜そうか!」
「おそらくfixtureの使い所は基本的に単体モデルのデータのようなあまり複雑でないものだと思うので、それもあって従来はfixtureで外部キーをバリデーションしていなかったんじゃないかなと想像しています」「ふ〜む」「自分もこれまでfixtureは比較的シンプルなデータのセットアップに使うことが多くて、リレーションが複雑になってきたらfixtureをやめてfactory_botにしていましたね」
「このバリデーション機能はfixtureで基本的にやりたい人にはありがたいと思います: 個人的には上のように外部キーを使うデータはあまりfixture向きではないかなという気もしていますが」「なるほど」
🔗 存在しないミドルウェアをdelete
した場合にエラーを出すよう修正
つっつきボイス:「config.middleware.delete
で削除するミドルウェアが存在していない場合にエラーを出すようにしたんですね: 何も出さないよりは正しそう」
# actionpack/lib/action_dispatch/middleware/stack.rb# 132
def delete(target)
- middlewares.delete_if { |m| m.name == target.name }
+ middlewares.reject! { |m| m.name == target.name } || (raise "No such middleware to delete: #{target.inspect}")
end
🔗 rails g
で指定するインデックス種別が無効な場合にエラーを出すようにした
つっつきボイス:「従来は以下の:indxe
みたいなスペルミスが無視されていたのを、エラーを出すようにしたそうです」
# 同PRより
bin/rails g model post title:string:indxe
「Railsのジェネレータを普段使っていないから、post title:string:index
と書くとインデックスを付けられるとは知らなかった」「そういえばRailsを長くやっている人はscaffoldをあまり使わない印象がありますね」
参考: Rails ジェネレータとテンプレート入門 - Railsガイド
🔗Rails
🔗 GitHubがRails 7に追加したdisable_joins
の解説記事
つっつきボイス:「GitHub主席ソフトウェアエンジニアのEileenさんの記事です」「お、どこかで見たと思ったらRails 7に最近入ったdisable_joins
ですね(ウォッチ20210426): マルチプルデータベース間であたかもjoinsしているかのように関連付けを取り出そうとするとSQLではJOINできないので、Active Recordが代わりに分割してクエリを発行してくれる機能」「あ、そうでした」「EileenさんがこうやってRailsコミット記事を出すことで、Railsに貢献する意義をGitHub社内にアピールできるという側面もありそうですね: よさそうな記事👍」
# 同記事より
class Dog < AnimalsRecord
has_many: treats, through: :humans, disable_joins: true
has_many :humans
end
-- 同記事より
SELECT "humans"."id" FROM "humans" WHERE "humans"."dog_id" = ? [["dog_id", 1]]
SELECT "treats".* FROM "treats" WHERE "treats"."human_id" IN (?, ?, ?) [["human_id", 1], ["human_id", 2], ["human_id", 3]]
Rails 7: has_many :through関連付けにdisable_joins: trueオプションが追加(翻訳)
🔗 Rails 7のモデル暗号化導入の経緯
つっつきボイス:「BasecampがやっているHEYの中の人が書いた、Rails 7のモデル暗号化機能のいきさつ記事だそうです」「そういえば暗号化機能はもともとHEYで使っていたものを切り出したという話がありましたね」「HEYの中で第三者によるセキュリティ監査も受けたそうです」「この記事もなかなかよさそう👍」
「モデル属性の暗号化はRails 7の新機能の中では比較的大きな位置を占めそうなので、Action Mailboxとかよりも使う人は多いんじゃないかな」「そんな気がしますね」
🔗 MemoWise gem: メモ化支援gem(Ruby Weeklyより)
つっつきボイス:「MemoWiseは、Rubyのメモ化(memoisation)を以下のようにmemo_wise
などで書けるgemなんですね: どこかで見たかも」
# 同記事より: 通常のメモ化
class Example
def slow_value
@slow_value ||= begin
...
end
end
end
# 同記事より: MemoWiseの場合
class Example
prepend MemoWise
def slow_value
...
end
memo_wise :slow_value
end
参考: メモ化 - Wikipedia
「インスタンス変数とメソッド名を取り違えそうになると記事に書かれていました」「そうそう、アクセサメソッドをインスタンス変数と同じ名前にするとそうなりがちなので注意が必要ですね」
「一見メモ化のためだけのgemを作るほどでもなさそうにも見えますが、単なる利便性よりも、ここでメモ化が行われることをmemo_wise
で明示的に示すことでコードの可読性を高める効果が期待できそうかなと思いました👍」「なるほど」
つっつきの後で以下の記事も見つけました。
参考: Optimizing MemoWise Performance @ ja.cob.land
🔗 RailsへのDDoS攻撃の影響を最小化する(Hacklinesより)
つっつきボイス:「記事ではrack-attackミドルウェアによるスロットリングが取り上げられていますね: ちょうど最近使いました」「私も使ってます」
「記事ではCloudflareなどのサービスを用いたDNSレベルの防御の話もしている」「ふむふむ」
「DDoS対策は、まずRailsサーバーにいかにDDoSを届かせないかが重要だと思います: rack-attackのスロットリングについても、記事にもあるようにfail2banと組み合わせられればより防御を固められるでしょうね」
参考: Fail2ban
参考: 不正アクセスからサーバを守るfail2ban。さくらのクラウド、VPSで使ってみよう! | さくらのナレッジ
「fail2banって初めて知りました」「fail2banはrack-attackと別にかなり昔からある不正アクセス遮断用のソフトウェアで、iptablesと統合されることもよくありますし、いろんなところで使われています」「なるほど」「たしかにfail2banのページのつくりが昔っぽいかも」
「たとえばiptablesにfail2banを組み合わせると、一定時間内に何リクエスト以上来ると指定の秒数だけ遮断するといった設定を書けます: ただしfail2banはシステムに入っていないと使えません」「なるほど」「もちろんrack-attackでもスロットリングはできますが、DDoSがRackに届くことはRailsに届くことでもあるので、ガチのDDoS相手だときつい」「あ〜」「元記事にも書いてあるように、可能ならいわゆるWAF(Web Application Firewall)的なものも使いたいですね」
参考: Web Application Firewall - Wikipedia
同記事によると、rack-attackはfail2banなどのシステムレベルのツールが利用できない環境(Herokuなど)で便利だそうです。
前編は以上です。
バックナンバー(2021年度第3四半期)
週刊Railsウォッチ: ruby-spacyで自然言語処理、Ruby製x86-64アセンブラ、『タイムゾーン呪いの書』ほか(20210713後編)
- 20210712前編 AR::Relation#destroy_allがバッチ分割に変更、Active Record暗号化解説、sidekiq-unique-jobsほか
- 20210706後編 GitHub CopilotのAI補完、Pure Ruby実装のRuby JIT rhizome、PostgreSQLのPG-Strom拡張ほか
- 20210705前編 DI的な書き方が必要なとき、脆弱性学習用アプリRailsGoat、brakemanは優秀ほか
今週の主なニュースソース
ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。
週刊Railsウォッチについて
TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)