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

週刊Railsウォッチ: productionのforce_ssl=trueがデフォルトで有効に、rakeタスクをthorで書くほか(20230704前編)

こんにちは、hachi8833です。

週刊Railsウォッチについて

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

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

お知らせ: 来週の週刊Railsウォッチはお休みをいただき、通常記事を公開します 🙇

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

🔗 production環境でforce_ssl=trueをデフォルトで有効にする

production環境ではforce_ssl=trueをデフォルトで有効にするようになった。これはアプリへのアクセスをすべて強制的にSSL(TLS)経由にし、Strict-Transport-Security(HSTS)やsecure cookieを使うようにする。
Justin SearlsAaron PattersonGuillermo IguaranVinícius Bispo
同Changelogより


production環境にアプリをデプロイ後、認証済みトラフィックがセキュアでないHTTPで転送されていることに気づくまで数週間放置してしまったことがある。production環境ではconfig.force_sslがデフォルトでtrueになるという誤った前提で新しいアプリを運用してしまった。

この変更で皆の関心を集めて議論のきっかけとしたい。このオプションが導入されて以来、Let's Encryptの証明書によってWebの状況はずいぶん変わり、今やHTTPSはほとんどのホスティングサービスで必須要件となっている。新しいアプリではStrict-Transport-Securityをデフォルトで有効にしてもいい時期が来たと思う。
同PRより

参考: Strict-Transport-Security - HTTP | MDN


つっつきボイス:「今は無料で証明書を発行してくれるLet's Encryptもあるし、production環境をデフォルトでforce_ssl=trueするのはこれはこれでいいんじゃないかと思います」

参考: Let's Encrypt - フリーな SSL/TLS 証明書

「ちなみに、productionやstaging環境以外のローカル開発環境でも、使おうとしているAPIによってはHTTPSを有効にしないとアクセスできないものがあったりしますね」「OAuthコールバックとかは最近HTTPSじゃないと弾かれますね」

参考: OAuth - Wikipedia

🔗 (PostgreSQL向け)マイグレーションでenumのリネーム、値の追加、値のリネームが可能になった

rename_enumおよびrename_enum_valueはリバース可能である。add_enum_valueはPostgreSQLの制約(enum値を削除できない)によりリバースできない。代替手段として、enum全体を削除してから再作成すること。

rename_enum :article_status, to: :article_state
add_enum_value :article_state, "archived" # will be at the end of existing values
add_enum_value :article_state, "in review", before: "published"
add_enum_value :article_state, "approved", after: "in review"
rename_enum_value :article_state, from: "archived", to: "deleted"

Ray Faddis
同Changelogより


つっつきボイス:「お〜、ぽすぐれでenumのリネームやenum値の追加/リネームができるようになった🎉」「2022年4月からのプルリクなんですね」「そういえば#44898は以下の記事でも前からマージが望まれていました↓」

Rails: Evil Martiansが使って選び抜いた夢のgem(翻訳)

「PostgreSQLにはALTER TYPEというものがあるのか↓」

# activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb#L553
+     # Rename an existing enum type to something else.
+     def rename_enum(name, options = {})
+       to = options.fetch(:to) { raise ArgumentError, ":to is required" }
+
+       exec_query("ALTER TYPE #{quote_table_name(name)} RENAME TO #{to}").tap { reload_type_map }
+     end

参考: PostgreSQL 15.0ドキュメント: ALTER TYPE

add_enum_valueがリバースできないのは仕様上仕方がない」「ここで言うリバースは、マイグレーションのupdownに書かなくてもchangeに書けばロールバック可能になるということですね」「そうそう」

参考: §3.9 changeメソッドを使う -- Active Record マイグレーション - Railsガイド

🔗 複合主キー関連

🔗 複合主キーの逆方向関連付けの設定を修正

動機/背景
複合主キーの関連付けでレコードに外部キーが存在するかどうかをチェックするには、外部キーのすべての部分が存在しているかどうかのチェックが必要になる。さもないと逆方向の関連付けが設定されない。

詳細
#foreign_key_for?を決定する外部キーのすべての部分をチェックするようになる。

追加情報
より詳細なテストをinverse_associations_test.rbに追加して、子の複合主キー関連付けが適切に読み込まれるかどうかを検証している。また、より一般的なテストをbelongs_to_associations_test.rbに追加して、逆方向の関連付けが設定された場合にbelongs_to関連付けが正しく保存されるようにしている。
同PRより


つっつきボイス:「今回の複合主キー関連改修は2つとも修正でした」「複合主キーの場合は外部キーにすべてのキーが揃っていなければ正常に動かなくなりますね↓: 複合主キーの改修が進むにつれて、こういうふうに修正の必要な部分が見えてきた感じ」

# activerecord/lib/active_record/associations/association.rb#L338
        def foreign_key_for?(record)
-         record._has_attribute?(reflection.foreign_key)
+         foreign_key = Array(reflection.foreign_key)
+         foreign_key.all? { |key| record._has_attribute?(key) }
        end

🔗 複合主キーモデルを指すhas_many through:関連付けのdestroy_allを修正

このプルリクは、「複合主キーがあるモデルを指すが、関連付け自身は参照のビルドにidカラムだけを使う」has_many through:関連付けにおけるdestroy_allの振る舞いを修正する。

primary_key: :idという関連付けの定義は「このidカラムは外部主キーとして使うこと」という意味であり、そのためRailsはpublic_sendでこのカラムの値にアクセスすることは避けるべき(そうすると特殊な意味を持つidメソッドが呼び出されてしまうため)。このメソッドは識別子すなわち「主キーカラムの背後にある値」を指すが、複合主キーの文脈ではidカラムと同じものではないので、代わりに _read_attributeで直接idカラムの値を取得する。
同PRより


つっつきボイス:「これも複合主キーを正しく参照していなかった箇所がまだ残っていたので修正したということですね」「public_sendで呼び出されると違うidメソッドにアクセスしてしまうので、privateの_read_attributeメソッドで取り出すように変更したんですね」

# activerecord/lib/active_record/associations/through_association.rb#L57
        def construct_join_attributes(*records)
          ensure_mutable
          association_primary_key = source_reflection.association_primary_key(reflection.klass)
          if Array(association_primary_key) == reflection.klass.composite_query_constraints_list && !options[:source_type]
            join_attributes = { source_reflection.name => records }
          else
-           join_attributes = {
-             source_reflection.foreign_key => records.map(&association_primary_key.to_sym)
-           }
+           assoc_pk_values = records.map { |record| record._read_attribute(association_primary_key) }
+           join_attributes = { source_reflection.foreign_key => assoc_pk_values }
          end

          if options[:source_type]
            join_attributes[source_reflection.foreign_type] = [ options[:source_type] ]
          end
          if records.count == 1
            join_attributes.transform_values!(&:first)
          else
            join_attributes
          end
        end

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

🔗 secrets関連

🔗 Rails.application.secrets呼び出しの非推奨化

Railsのsecrets#47801非推奨化されてcredentialsに置き換わった。Rails.application.secretsの呼び出しは非推奨警告を表示すべき。
そのために、最も深い場所にあるRails.application.secretsへの呼び出しを削除する#48470が必要。
同PRより


つっつきボイス:「これは次の#48470と連携しているそうです」「既に#47801bin/rails secrets:setupを実行しても無効になってたのね↓」

bin/rails secrets:setupはRails 5.2以降非推奨化されていて、実行するとコマンドは実行されずに非推奨化警告を表示する。そろそろ全部削除しても安全だろう。
secrets:setupが非推奨化されてから随分経ったので、secrets:showコマンドとsecrets:editコマンドも非推奨化してもよさそう。
Remove deprecated secrets:setup and deprecate secrets:edit/show by p8 · Pull Request #47801 · rails/railsより

「そういえばRailsがcredentialに切り替わったのが5.2だったからだいぶ経ちますね↓」

参考: §2.4 credential管理 -- Ruby on Rails 5.2 リリースノート - Railsガイド

config/credentials.yml.encファイルが追加され、productionアプリケーションの秘密情報(secret)をここに保存できるようになりました。これによって、外部サービスのあらゆる認証credentialを、config/master.keyファイルまたはRAILS_MASTER_KEY環境変数にあるキーで暗号化した形で直接リポジトリに保存できます。Rails.application.secretsやRails 5.1で導入された暗号化済み秘密情報は、最終的にこれによって置き換えられます。 さらに、Rails 5.2ではcredentialを支えるAPIが用意され、その他の暗号化済み設定/キー/ファイルも簡単に扱えます。 詳しくは、Rails セキュリティガイドを参照してください。
§2.4 credential管理 -- Ruby on Rails 5.2 リリースノート - Railsガイドより

🔗 ローカル環境のsecret_key_baseRails.configに保存する

Railsのsecretsが非推奨化されてcredentialsに置き換わった。
しかしローカル環境ではsecret_key_baseの保存に引き続きRails.application.secretsが使われている。
そこで、代わりにRails.config.secret_key_basesecret_key_baseを保存する。
この変更によってRails.application.secretsの非推奨化が可能になる。#48472を参照。
同PRより


つっつきボイス:「この#48470が上の#48472より先にマージされたそうです」「プルリクに書かれているようにsecret_key_baseは今のローカルではRails.application.secretsに保存されるんですが、これを非推奨化するためにローカル用の保存場所を変更しておく必要があったということですね」

「ちなみにsecret_key_baseが設定されていないとRailsを起動できません: Dockerコンテナで使うときは基本的にsecret_key_baseを環境変数で指定します」

参考: §3.2.33 config.secret_key_base -- Rails アプリケーションを設定する - Railsガイド
参考: § 10.1 独自のcredential -- Rails セキュリティガイド - Railsガイド

🔗 キャッシュの:message_pack:coderオプションで指定する方法に変更する

#48104で、キャッシュフォーマットのバージョンでサポートされる値として:message_packが追加された(フォーマットのバージョンを指定するということは本質的にデフォルトのコーダーを指定することになる)。
しかしAPIとしては、シリアライズの他の側面に影響を与える目的でフォーマットのバージョンを指定する可能性がありうる。
このコミットは、フォーマットバージョンでサポートされる値としての:message_packを削除し、代わりにcoder: :message_pack:による指定を追加する。

# 改修前
config.active_support.cache_format_version = :message_pack

# 改修後
config.cache_store = :redis_cache_store, { coder: :message_pack }

同PRより


つっつきボイス:「MessagePackが追加されて以来改修が重ねられていますけど(ウォッチ20230502ウォッチ20230530など)、コンフィグでの指定方法が変わったんですね: たしかにキャッシュフォーマットを指定するときはコーダーも同時に指定する方がわかりやすい」「Changelogも変更されていますね」

🔗Rails

🔗 Railsタスクをrakeではなくthorで書くコツ(Ruby Weeklyより)


つっつきボイス:「rakeの代わりにthor gem↓を使ってタスクを書こうという記事はときどき見かけますね」「仕方がないとはいえ、rakeタスクは使いにくいんですよ: 記事にもあるように、rakeは引数の渡し方がいろいろ面倒で、気をつけないと引数がシェルで誤って解釈されてしまったりする」「rakeタスクはテストも面倒と書かれていますね」

rails/thor - GitHub

# 同記事より: thorでタスクを書く例
class UserTasks < Thor:
  desc "create EMAIL", "Create a User record in the database identified by EMAIL"
  def create(email)
    # TODO
  end
end

🔗 Railsコンソールの便利技集(Ruby Weeklyより)


つっつきボイス:「ざっと見た限りでは、Railsコンソールの深掘りというよりはtipsをまとめた感じかな」「ヘルパーメソッドの呼び出し↓は、コンソールをどこで実行するかにもよりますね: ビューのコンテキストなら当然ヘルパーメソッドを呼び出せる」

# 同記事より
irb(main):001:0> helper.number_to_currency(123)
=> "$123.00"

irb(main):002:0> helper.number_to_currency('123', precision: 0)
=> "$123"

irb(main):004:0> helper.number_to_phone('5555555555')
=> "555-555-5555"

irb(main):005:0> helper.number_to_phone('5555555555', area_code: true)
=> "(555) 555-5555"

irb(main):006:0> helper.number_to_phone('5555555555', country_code: 1)
=> "+1-555-555-5555"


irb(main):007:0> helper.number_to_phone('5555555555a', raise: true)
/Users/cody/.rbenv/versions/3.2.2/lib/ruby/gems/3.2.0/gems/actionview-7.0.5/lib/action_view/helpers/number_helper.rb:453:in `parse_float': ActionView::Helpers::NumberHelper::InvalidNumberError (ActionView::Helpers::NumberHelper::InvalidNumberError)

「逆にi18nは以下のようにI18nで直接呼び出せる↓」

# 同記事より
irb(main):001:0> I18n.t('users.agreements.show')
=> {:title>"%{agreement} Changed", :last_updated=>"Last updated %{date}", :description=>"Before you can proceed, you must read and accept the new %{agreement}.", :accept=>"I Accept", :decline=>"I Decline"}

irb(main):001:0> I18n.t('are_you_sure')
=> "Are you sure?"

_変数で直前の結果を取り出すのも定番↓」「IRBではないけど、Railsコンソール(bin/rails c)で一番使うのはデータベースをリードオンリーにする--sandboxオプションかな、特にproductionで」「自分も使います」

User.first
=> User stuff

user = _

=> <User ...>

appでルーティングを参照できるのね↓」「自分はあまり使ってなかったけど、source_locationも使えますね」

# 同記事より
irb(main):001:0> app.new_billing_address_path
=> "/billing_address/new"

irb(main):002:0> app.root_path
=> "/"
# 同記事より
irb(main):001:0> User.instance_method(:confirm).source_location
=> ["/Users/cody/.rbenv/versions/3.2.1/lib/ruby/gems/3.2.0/gems/devise-4.8.1/lib/devise/models/confirmable.rb", 79]

🔗 message_bus: RubyアプリやRackアプリで使えるメッセージングバス(Ruby Weeklyより)

discourse/message_bus - GitHub


つっつきボイス:「サンプルコード↓を見ると、サーバーtoサーバーのメッセージングバスを単体で構築できるライブラリのようですね: publishsubscribeがあるところからしてpub/sub的に使えるみたい」

# 同リポジトリより
message_id = MessageBus.publish "/channel", "message"

# in another process / spot

MessageBus.subscribe "/channel" do |msg|
  # block called in a background thread when message is received
end

# subscribe to channel and receive the entire backlog
MessageBus.subscribe "/channel", 0 do |msg|
  # block called in a background thread when message is received
end

# subscribe to channel and receive the backlog starting at message 6
MessageBus.subscribe "/channel", 5 do |msg|
  # block called in a background thread when message is received
end

参考: 出版-購読型モデル - Wikipedia -- pub/sub

「RailsやSinatraなどのRackアプリケーションでも使えるのでRailsアプリにも相乗り可能なのね」「JavaScriptライブラリも入っているのでJavaScriptからでもメッセージングバスを使えるのか: Railsアプリを作るほどでもないようなときにメッセージングバスを構築するのに便利そう👍」

# 同リポジトリより: Railsのミドルウェアスタックに組み込む場合
# config/initializers/message_bus.rb
Rails.application.config do |config|
  # do anything you wish with config.middleware here
end

参考: Message Bus - Enterprise Integration Patterns

「このmessage_busは既存の機能だとどれに近いんでしょうか?」「既存のものだと、相互通信できるAction Cableみたいな感じかな: 要するに相互に共有できるバス(bus)を経由して通信する方法」「いわゆるバス接続のトポロジーなんですね」

参考: Action Cable の概要 - Railsガイド
参: バス (コンピュータ) - Wikipedia

🔗 eyeloupe: Railsのエラー画面をAIアシスタントで強化(Ruby Weeklyより)

alxlion/eyeloupe - GitHub


つっつきボイス:「eyeloupeっていわゆるルーペ(拡大鏡)のことか」「READMEの動画↓を見た感じではAirbrakeとかSentryあたりをちょっと思い出すけど、例外もハンドリングしたりするらしいので、どちらかというとエラー画面を強化するライブラリのようですね」「そこにAIアシスタント機能も搭載しているんですね」「mount Eyeloupe::Engine => "/eyeloupe"でエンジンをマウントすると/eyeloupeでアクセスできるのね: ちょっと面白そうなので★追加してみた👍」

airbrake/airbrake - GitHub

参考: Rails | Sentry Documentation

eyeloupeはLaravel Telescopeにインスパイアされて作ったそうです↓。

参考: Laravel Telescope - Laravel - The PHP Framework For Web Artisans


前編は以上です。

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

週刊Railsウォッチ: ruby_memcheckでネイティブgemのメモリリークを自動検出ほか(20230629後編)

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

Rails公式ニュース

Ruby Weekly


CONTACT

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