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

週刊Railsウォッチ: Arel::Nodes::NodeにAPIドキュメントが追加、rubocop-mdほか(20230405前編)

こんにちは、hachi8833です。RubyKaigi 2023の各種イベントが発表されていますね。

参考: Ruby on Rails | endoflife.date

週刊Railsウォッチについて

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

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

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

🔗 複合キー関連の改修2件

以下のような(query_constraintsによる)複合キー制約を定義した関連付けがあるとする。

BlogPost.has_many :comments, query_constraints: [:blog_id, :blog_post_id]

このとき、以下のようにcommentsオブジェクト全体でブログ記事をクエリできるようになる。

comments = Comment.first(2)
BlogPost.where(comments: comments).to_a

残りの作業
関連付け名を用いて制約をビルド可能になるこの機能は非常に強力なので、少なくとも今後以下のようなサブクエリのユースケースでカバーする関連付けの種類を増やす必要があるだろう(belongs_tohas_onehas_many through、ポリモーフィック関連付け)。

BlogPost.where(comments: Comment.where(id: 1, blog_id: 1))` 

そこで今後は必要に応じてカバレッジと修正を増やしていく予定。ただし他の作業がパラレルに完了するのをブロックしないよう、最も一般的なユースケースについて修正を行いたいと思う。以下のような抽象的な利用法に依存する機能がブロックされないようにする。

.where(association_name => association_records)

今回の例では、association_namecomments:に、association_recordsCommentオブジェクトの配列に相当する。
#47692より


ActiveRecord::FinderMethods#findで、以下のように複合主キーをセットで渡せるようになった。

Cpk::Book.find([1, 1])
Cpk::Book.find([[1, 1]])
Cpk::Book.find([1, 1], [1, 2])
Cpk::Book.find([[1, 1], [1, 2]])

値は複合主キーカラムの値として扱われるが、これはモデルの設定としてprimary_keyクラスインスタンス変数に配列として複数のカラムが指定された場合のみが対象となる。

以前から、findメソッドは以下のように常に"識別子"を受け付けるシグネチャを持っている。

Model.find(ID)
Model.find([ID])
Model.find(ID, ID)
Model.find([ID, ID])

上のIDにはモデルを識別するものが入る。Railsがシングルカラムの識別子だけをサポートする限り、この点はさほど明確ではなかった。しかし複合主キーがサポートされたことで、識別子としてのIDの意味が拡張された。値のセットでモデルを識別可能になったので、それをサポートするためにfindのシグネチャを拡張する必要がある。

実装の詳細

primary_keyを持つモデルについてのみ振る舞いが変更されるので、is_a?(Array)の分岐が複数になり、そのいくつかは重複する。primary_key.zip(values_set)が反復するパターンになったり、.inject(&:or)を使うバッチクエリのビルド方法も反復する。
しかし現時点では、重複を導入してできるだけ多くのユースケースを集めておき、これらを1つの概念に抽象化してリファクタリングしたい。冗長なユースケースについては、当面その概念にふさわしい名前と置き場所を見つけるためにのみ役立てることになるだろう。
#47664より


つっつきボイス:「複合キーに関連する改修が増えてきましたね」「#47692は、query_constraintsを指定した関連付けをwhereで絞り込む際、個別のキーを指定せずに関連名だけで絞り込めるようにした: これはできて欲しいヤツ👍」「今回はhas_many関連付けで、今後はbelongs_to関連付けとかでも順次使えるようにするんですね」「複合主キー関連の改修はもっと大規模になるかと思っていたけど、思ったより変更範囲が小さいのがちょっと驚き」

参考: Edge API query_constraints -- ActiveRecord::Persistence::ClassMethods

「次の#47664はfindメソッドに[1, 1][[1, 1], [1, 2]]のような形で複合主キーを渡せるようになった、なるほど」「findに配列で渡すの何でだろうと思ったら複合主キーの話だったんですね、やっと理解しました」

参考: Rails API ActiveRecord::FinderMethods

「ところで、渡す複合主キーの順序ってどうやって規定するんだろう?」「あ、それもそうか」「関連付けの定義で書いた順に適用されそうに思えるけど、複合主キーを渡すときの順序については気をつけておく必要はあるかも」

🔗 Railsアプリをディレクトリの外から再起動可能になった

bin/rails restartをアプリのディレクトリの外から呼び出せるようになった。
従来は、以下を実行すると"No Rakefile found"で失敗した。

blog/bin/rails restart

Petrik de Heus
同Changelogより

rails/thor - GitHub


つっつきボイス:「アプリの外部から/bin/rails restartを呼べるようにするためにthor gemを使ったんですね」「restartタスクが追加されてますが、内容としてはtmp/restart.txtファイルを置くだけで、既存の再起動の仕組みを使っているんですね」

module Rails
  module Command
    class RestartCommand < Base # :nodoc:
      desc "restart", "Restart app by touching tmp/restart.txt"
      def perform
        require "fileutils"
        FileUtils.mkdir_p Rails::Command.application_root.join("tmp")
        FileUtils.touch   Rails::Command.application_root.join("tmp/restart.txt")
      end
    end
  end
end

🔗 Arel::Nodes::NodeにAPIドキュメントが追加された

動機/背景
現在のArelはprivate APIであり、ドキュメントがほぼ存在しないが、APIとして非常に有用であり、アプリケーションでもgemでも同じぐらい使われている。最終的にはArelをpublic APIにできればと思っている。このプルリクは、Arelにドキュメントを追加して最終的にpublicにする取り組みの出発点である。
詳細
このプルリクでは、SQL文生成でよくあるノードの利用方法について簡単な説明文を追加する。網羅的なドキュメントではないが、今後ドキュメントを増やす取り組みの出発点となる。
追加情報
Ruby on Rails Discordで@matthewdがドキュメント作成を提案してくれた。
同PRより


つっつきボイス:「これはArelを愛するkazzさんが喜びそう」「お〜、これは嬉しいヤツです😂」「Arelは公式には使って欲しくないというスタンスだったから今までAPIドキュメントがなかったんでしたっけ」「想像ですけど、Arelもそろそろ枯れてきたしAPIを公開してもいいのではという流れなのかも」「期待しちゃいます」

「Arel怖いです...」「Arel大好き😋」「Arelの他のドキュメントはまだ空ですが、今後も増やすようですね」「Arelがないと書けないようなActive Recordコードは今でもいろいろあるので、Arelを使う人のためにドキュメントを整備する方向に行くのはわかる」

Arelのススメ — JOINをArelで書こう

ちなみにArelはActive Relationとも呼ばれるそうです。

🔗 fixtureパスをRailsエンジン単位で指定可能になった

動機/背景
このプルリクを作成した理由は、fixture_pathをRailsエンジンレベルで設定することで、アプリを複数のデプロイ可能な単位に分割しやすくするため。

詳細
このプルリクはTestFixtures#fixture_paths(複数形)を導入する。これはクラス属性で空配列である。これを用いることで、Railsのデフォルト以外のfixtureパスをアプリケーションごとに追加可能になる。
たとえば、Railsエンジンを複数利用しているアプリケーションがあり、それぞれが独自のテストとfixtureを持っている場合、エンジンが独自のfixtureへのパスをfixtureパスに追加できるようになる。

module UserManagement
  class Engine < Rails::Engine

    initializer("user_management.fixture_path) do
      ActiveSupport.on_load(:active_support_test_case) do
        self.fixture_path << "#{Rails.root}/user_management/test/fixtures}"
      end
    end
  end
end

追加情報
単数形のfixture_pathは非推奨とした。古い振る舞いとの後方互換性を維持するために、fixture_pathのリーダーとライターを書き直した。
同PRより


つっつきボイス:「単数形のfixture_pathが非推奨になって複数形のfixture_pathsになったんですね」「fixtureパスを追加したい場合って何だろうと思ったら、Railsエンジンが独自のfixtureを持ってる場合か」「これはできる方が嬉しいですね👍」

参考: Rails API ActiveRecord::TestFixtures
参考: Rails エンジン入門 - Railsガイド

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

🔗 Delegated Typesでカスタムカラム名を指定するforeign_typeオプションが追加された

delegated_type{role}_typeカラム名をforeign_typeオプションでカスタマイズ可能になった。

概要
Delegated TypesはRailsの既存のポリモーフィック関連付けに強く依存している。delegated_typeで作成される主要な関連付けはポリモーフィックbelongs_to関連付けである。

ポリモーフィック関連付けを使っているが、型やクラスを含むカラム名をカスタマイズする必要があるアプリでは、foreign_typeオプションが利用可能である。Delegated Typesでも同様にカスタマイズできれば便利だろう。

delegated_typesは既に背後のbelongs_to関連付けに沿ってオプションを渡すので、比較的素直に変更できる。単にtypeカラムを"#{role}_type"で派生させるのではなく、foreign_typeオプションに沿って派生させることが可能になる(ポリモーフィック関連付けで使われるのと同じオプションである)。
例:

delegated_type :entryable, types: %w[ Message Comment ], foreign_type: :entry_class

同PRより


つっつきボイス:「"#{role}_type"というRails wayな命名に沿っていないカラム名をDelegated Typesで直接指定できるようになったんですね」「なるほど」「既存のデータベースを使いたい場合はこれができないと困る」

:foreign_type
関連付けられるオブジェクトの型を保存するカラムを指定する。デフォルトでは、渡されたrole“_type”サフィックスを追加した名前を推論する。たとえば、delegated_type :entryable, types: %w[ Message Comment ]の関連付けを定義したクラスは、“entryable_type”という名前をデフォルトの:foreign_typeとして使うようになる。
edgeガイドActiveRecord::DelegatedTypeより

「Delegated TypesはAPIドキュメントの翻訳があります↓」

Rails: ActiveRecord::DelegatedType APIドキュメント(翻訳)

🔗 番外: rubocopでmarkdownドキュメントをチェックするようになった

Railsのmarkdownファイルを対象にrubocop-mdを導入する。
これにより、ガイド内のコードスニペットもRailsのコーディング規約に準拠するようになり、変更の旅に手作業で確認せずに済むようになる。
なお、Style/StringLiteralsによる変更量は非常に大きいので、ガイドでは意図的に無効にしてある。その変更は、このプルリクがマージされた後で1件のプルリクでまとめて行おうと思っている。
もっと細かな変更についてはzzak#2zzak#3で見られる。
同PRより


つっつきボイス:「ドキュメント関連の改修ですが、一番うれしいのは私かも😂」「Railsガイドのmarkdownファイルに対してrubocop-mdやmarkdownlintでスタイルのCIチェックをかけるようになったんですね↓」「rubocop-mdもmarkdownlintも知らなかった、今度使ってみようっと」

rubocop/rubocop-md - GitHub
markdownlint/markdownlint - GitHub

「ところで、原文のmarkdownでパラグラフがしょっちゅう80文字で強制改行されていたりするんですが、あれがあるとRailsガイド↓の翻訳がとてもやりにくいんですよ😢」「そのあたりを修正するとなると修正量が増えそうですね」「プルリクを投げたくても、強制改行はHTMLでの表示には影響しないので本質的ではないし、スタイル修正のコミットを増やすとgit blameする人の邪魔になりそうで、ずっとためらっていました」

参考: Ruby on Rails ガイド:体系的に Rails を学ぼう
参考: Git - git-blame Documentation

「markdownドキュメントを大勢でメンテナンスすると、rubocop-mdのようなlintツールなしでは書式がばらついてしまいますよね」「ツールで書式を整えるようにすれば今後ドキュメントのプルリクを減らす効果もあるし、書式をどうするかで争うことも減ると思うのでいいと思います👍」

🔗Rails

🔗 AnyCableをRails以外のものにも使う(Ruby Weeklyより)


つっつきボイス:「AnyCable gemとAnyCable-GoというWebSocketサーバーを使ってTwilioをHanamiに接続するという記事だそうです」「そういえばAnyCableもAnyCable-GoもEvil Martiansが手掛けていますね」

anycable/anycable - GitHub
anycable/anycable-go - GitHub

参考: Hanami | The web, with simplicity

AnyCable 1.0: RubyとGoによるリアルタイムWebの4年間(翻訳)

「そういえばTwilioはこれまでもよく話題に登場していますね(ウォッチ20200901)」「Twilioは電話/SMS/メールなどのAPIを多数提供している大規模メッセージングサービスで、この種のサービスとしては最古参の部類でしょうね」

参考: Twilio(トゥイリオ) | SMS、音声、ビデオ、二要素認証用のコミュニケーションAPI | Twilio
参考: ショートメッセージサービス - Wikipedia -- SMS

「商用メール配信サービスで有名なSendGridもTwilioの傘下です」

参考: Email Delivery, API, Marketing Service | SendGrid
参考: SendGrid - Wikipedia

sendgrid/sendgrid-ruby - GitHub

「Twilioは歴史が長いし、エンタープライズ向けの大規模メッセージング機能がいろいろ充実しているんですよ: マルチアカウントにも対応していて、プロジェクトを作ってその中でマルチアカウントの権限を管理するといったAWS Organization的なこともできる(それなりの費用がかかりますが)」「なるほど」「KDDIが代理店になっているので請求書が日本語なのもいい」「先ごろKDDIがTwilioの代理店をやめると発表したのが話題になっていましたね」

参考: 弊社によるTwilioサービス提供の終了とお手続きについて|Twilio - KDDI Web Communications

🔗 RailsからPostmark経由でメール送信する(Ruby Weeklyより)

# 同記事より
# config/environments/production.rb
config.action_mailer.perform_deliveries = true
config.action_mailer.delivery_method = :postmark

config.action_mailer.postmark_settings = {
  api_token: ENV["POSTMARK_API_TOKEN"]
}

# or use Rails Encrypted Credentials

config.action_mailer.postmark_settings = {
  api_token: Rails.application.secrets.postmark_api_token
}

つっつきボイス:「Postmarkというメール配信サービスを使う記事だそうです」「SendGrid的なサービスのひとつかな: 「Postmarkはメール配信をトランザクションストリームとブロードキャストストームに分けているのが特徴らしい↓」「最大で10のストリームが使えるんですね」「同じIPアドレスから大量にメール送信すると詰まったりすることがありますけど、ここではジョブキューを分けるような感じでストリームを分ける形にしているのかもしれませんね」

参考: A better way to send all your application email | Postmark

activecampaign/postmark-gem - GitHub

🔗 RubyやActive Supportでファイルを暗号化する(Ruby Weeklyより)


つっつきボイス:「SecureRandomはActive Supportに前からあるけど、encrypted_fileというのもあるのか」「記事ではinvisible_inkというgemも使っていますね」

# 同記事より
def build_encrypted_file(file)
  ActiveSupport::EncryptedFile.new(
    content_path: file,
    key_path: "invisible_ink.key",
    env_key: "INVISIBLE_INK_KEY",
    raise_if_missing_key: true
  )
end

参考: Rails API SecureRandomモジュール
参考: Active Support コア拡張機能 - Railsガイド

stevepolitodesign/invisible_ink - GitHub

「記事では、暗号化済みファイルをエディタで開くときに復号して、保存するときに暗号化している↓: Railsのcredential編集機能みたいなことをやろうとしているっぽいですね」「たしかに」

# 同記事より
when "write"
  file = ARGV.shift
  handle_missing_file_argument if file.nil?
  if ENV["EDITOR"].to_s.empty?
    puts "No $EDITOR to open file in"
    exit 1
  end
  begin
    encrypted_file = build_encrypted_file(file)
    encrypted_file.write(nil) unless File.exist?(file)
    encrypted_file.change do |tmp_path|
      system(ENV["EDITOR"], tmp_path.to_s)
    rescue Interrupt
      puts "File not saved"
    end
  rescue ActiveSupport::EncryptedFile::MissingKeyError => error
    handle_missing_key(error)
  end

参考: §10.1 独自のcredential -- Rails セキュリティガイド - Railsガイド

🔗 RailsチュートリアルのCodeSpacesリポジトリ

yasslab/codespaces-railstutorial - GitHub


つっつきボイス:「RailsチュートリアルがGitHub CodeSpacesにチュートリアル用のリポジトリを作ったそうです: チュートリアル1〜3章の環境構築をスキップできます」「教育分野だと環境構築周りでトラブルが起きやすくてインストラクターの時間が食われがちですよね」「かといってツールが増えすぎても時間を取られるし」「今どきGitHubアカウントは必ず作るでしょうから、GitHub Codespaceならツールを増やさなくても使えるのがいいですね👍」

参考: GitHub Codespaces の概要 - GitHub Docs


つっつき後に以下の予告動画(セルフ切り抜き)が公開されました。


前編は以上です。

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

週刊Railsウォッチ: 書籍『Rebuilding Rails』、OpenSSL 1.1.1が今年9月でEOLほか(20230330後編)

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

Rails公式ニュース

Ruby Weekly


CONTACT

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