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

週刊Railsウォッチ: GitHubによるdisable_joins解説、MemoWise gemでメモ化、RailsのDDoS攻撃対策ほか(20210719前編)

こんにちは、hachi8833です。

週刊Railsウォッチについて

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

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

お知らせ: 今週木金は祝日のため、来週は週刊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

参考: プログラマーのための YAML 入門 (初級編)

🔗 filepathにファイルがない場合のエラー出力を改善


つっつきボイス:「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エディタ呼び出し時にファイルパスに含まれるスペースを正しく扱えるよう修正


つっつきボイス:「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

Rails API: `ActiveRecord::FixtureSet`(翻訳)

🔗 fixtureとfactory_bot

「ただfixtureが外部キーでバリデーションエラーを出すようになると少し面倒になるんですよ: 複雑な依存関係を持つデータをfixtureで扱う場合、バリデーションエラーにならないためにfixtureの適切な読み込み順序なども考えないといけなくなってしまう」「あ〜そうか!」

「おそらくfixtureの使い所は基本的に単体モデルのデータのようなあまり複雑でないものだと思うので、それもあって従来はfixtureで外部キーをバリデーションしていなかったんじゃないかなと想像しています」「ふ〜む」「自分もこれまでfixtureは比較的シンプルなデータのセットアップに使うことが多くて、リレーションが複雑になってきたらfixtureをやめてfactory_botにしていましたね」

thoughtbot/factory_bot - GitHub

「このバリデーション機能は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より)

panorama-ed/memo_wise - GitHub


つっつきボイス:「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

Ruby: インスタンス変数初期化のメモ化`||=`はほとんどの場合不要

🔗 RailsへのDDoS攻撃の影響を最小化する(Hacklinesより)

参考: DoS攻撃 - Wikipedia


つっつきボイス:「記事ではrack-attackミドルウェアによるスロットリングが取り上げられていますね: ちょうど最近使いました」「私も使ってます」

rack/rack-attack - GitHub

「記事ではCloudflareなどのサービスを用いたDNSレベルの防御の話もしている」「ふむふむ」

「DDoS対策は、まずRailsサーバーにいかにDDoSを届かせないかが重要だと思います: rack-attackのスロットリングについても、記事にもあるようにfail2banと組み合わせられればより防御を固められるでしょうね」

参考: Fail2ban
参考: 不正アクセスからサーバを守るfail2ban。さくらのクラウド、VPSで使ってみよう! | さくらのナレッジ

「fail2banって初めて知りました」「fail2banはrack-attackと別にかなり昔からある不正アクセス遮断用のソフトウェアで、iptablesと統合されることもよくありますし、いろんなところで使われています」「なるほど」「たしかにfail2banのページのつくりが昔っぽいかも」

参考: iptables - Wikipedia

「たとえば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後編)

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

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

Rails公式ニュース

Ruby Weekly

Hacklines

Hacklines


CONTACT

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