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

週刊Railsウォッチ(20201208前編)レガシーRailsアプリを引き継ぐときの6つの作業、サーバーレスプロジェクトをRailsに移行ほか

こんにちは、hachi8833です。

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

今回は、BPS昼の定例勉強会でつっつき会を行いました。

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

公式の更新情報から見繕いました。


つっつきボイス:「6.1 RC2が出ましたし、最終リリースまであとちょっとという感じになってきましたね」「お、今見るとマイルストーンでオープンのissueが0件になってる」「RC2でまたいくつかissueが追加される可能性はありそう」「最終リリースは年内ぐらいかな?案外来週ぐらいにしれっと出たりするかもしれませんけど」「Rubyとどっちが早く出るかな?」

その後、ウォッチ公開日のマイルストーンでオープンのissueは3件になっています。

新機能: ERBでHashをHTML属性に変換できるようにする

ERBで以下のようにHashをHTML属性に変換して展開可能にする。

<input <%= tag.attributes(type: :text, aria: { label: "Search" }) %>>
<%# => <input type="text" aria-label="Search"> %>

ActionView::Helpers::TagHelper#tag_optionsの実装を利用してERBと属性変換を組み合わることで、テンプレートでHTML文字列をtagcontent_tagで置き換えなくてもやれるようにする。
同PRより大意


つっつきボイス:「attributesヘルパーメソッドが追加されたんですね↓」「上のようにハッシュをHTML属性として展開するのは前からできてたような気もしたけど、あれはSlimの書き方だったか」「ERBでもこれが使えるようになったのはいいですね👍」

# actionview/lib/action_view/helpers/tag_helper.rb#L56
+       def attributes(attributes)
+         tag_options(attributes.to_h).to_s.strip.html_safe
+       end

参考: slim/README.jp.md at master · slim-template/slim

以下の記事によるとHaml 4以降でもできるそうです。

参考: HAML 4+ expands nested element attributes - makandra dev

新機能: where.associatedで関連付けにデータが存在するかどうかをチェックできるようになった

以下は自分たちのアプリからの抜粋。

class Account < ApplicationRecord
  has_many :users, -> { joins(:contact).where.not(contact_id: nil) }
end

このuserは、contactのdelegatedy typeであるcontactablecontactableを置き換えて1件のuserをバックグラウンドで削除するというのはよくあるパターン。

上の書き方では関連付けの先にデータが存在するかどうかだけを知りたい場合に構文が少々煩雑になるが、これを以下のように書けるようになる。

class Account < ApplicationRecord
  has_many :users, -> { where.associated(:contact) }
end

これは#34727で追加されたwhere.missingの鏡写しになる。
同PRより大意


つっつきボイス:「元のjoins(:contact).where.not(contact_id: nil)は、joinした先のcontact_idが存在するかどうかだけを確認したいときに使いそうなクエリですね: これはたしかに悩ましくて、contactに制約が付いていない場合なんかだとcontactにcontact_idの外部キーが存在しない可能性があるので、関連付け先にデータが確実に存在するかどうかを確認するにはjoinsを書かないといけなくなりますが、改修後はwhere.associated(:contact)のように既に関連付けにデータが存在するという意味でassociatedを使って簡潔に書けるようになったということでしょうね」「なるほど」「こう書きたい気持ちはわかります」

「慣れるまではassociatedがコードで使われていてもすぐにピンとこないかも」「制約を付けて回避できるなら制約でやる方がいいでしょうね」「プルリクメッセージで引用されているwhere.missing↓はassociatedと対照的に、関連付けが存在しないものをフィルタで取り出すメソッド」

rails statsにCSSやERBの情報も表示するようになった

GitHubのモノリスのサイズ情報を得る方法を探していて、rails statsではapp/viewsディレクトリやapp/assets/stylesheetsディレクトリの情報が含まれていないことに気づいた。
これらのフォルダについても情報を出せば便利だと思う。このプルリクはこれらをViewsとStylesheetsという項目として追加する。


同PRより大意


つっつきボイス:「rails statsの出力項目に情報が増えましたね」「ビューとCSSが今までなかったのか」

# railties/lib/rails/code_statistics.rb#L43
-   def calculate_directory_statistics(directory, pattern = /^(?!\.).*?\.(rb|js|ts|coffee|rake)$/)
+   def calculate_directory_statistics(directory, pattern = /^(?!\.).*?\.(rb|js|ts|css|scss|coffee|rake|erb)$/)

なお手元のRails 6.0.3では以下のように表示されました。ここにはminitestのディレクトリも出力されていますが、試しにtestディレクトリを削除したら出なくなりました。

新機能: リッチテキスト関連付けを一括でeager loadingできるようになった

Action Textはwith_rich_text_#{name}ヘルパーを提供して、リッチテキストの関連付けを楽にプリロードできるようになっている。これは1個のモデル上の1個のリッチテキストフィールドではうまくいくが、リッチテキストフィールドが複数の場合は個別のフィールドを読み込むためにActionTextテーブルへのクエリが繰り返される。

以下のようにモデルの中にユーザーが生成した動的なコンテンツが多数ある場合を考える。

class Page < ApplicationRecord
  has_rich_text :header
  has_rich_text :sub_header
  has_rich_text :content
  has_rich_text :aside
  has_rich_text :footer
  ...
end

Pageのすべてのコンテンツをeager loadingで表示しようとすると以下のようになる。

Page
  .with_rich_text_header
  .with_rich_text_sub_header
  .with_rich_text_content
  .with_rich_text_aside
  .with_rich_text_footer
  .find(params[:id])

この場合Railsが6回もクエリを実行するとことになる(1回はPageの読み込み、5回は個別のActionText読み込み)。

このプルリクはwith_all_rich_textを追加する。これはeager_loadを使い、has_rich_text関連付けへのリフレクションを行って、すべてのリッチテキストの関連付けを一括読み込みする。

Page.with_all_rich_text.find(params[:id])

その他
#37976によると現在のActionTextの内部は流動的とのことだが、この機能を今後のリリースに追加することに関心があるか、あるいはアプリケーション固有のヘルパーの方がよいかどうかをチェックして欲しい。
同PRより大意


つっつきボイス:「with_rich_text_*はテンプレートで複数使うこともありそうなので、それをwith_all_rich_textでひとつのクエリで書けるようにしたということか」「項目の数だけクエリが発行されなくて済むのはいいですね👍」

# actiontext/lib/action_text/attribute.rb#L50
+     def with_all_rich_text
+       eager_load(rich_text_association_names)
+     end
+
+     private
+       def rich_text_association_names
+         reflect_on_all_associations(:has_one).collect(&:name).select { |n| n.start_with?("rich_text_") }
+       end

travel_toブロックで日時をStringで取るときにアプリのタイムゾーンが使われるよう修正

バグのように見えるが、ドキュメントにこの機能の説明が見当たらなかった。
travel_toは"2004-11-24 01:04:44"のようなstringを引数に取れるが、Stringで追加定義されるto_timeメソッドによってアプリケーションのタイムゾーン情報が失われてローカルに設定されてしまう。

# 現状
travel_to "2004-11-24 01:04:44" do
  Time.zone.now.to_s(:db) # => "2004-11-24 06:04:44" 
end 

# 期待する動作
travel_to "2004-11-24 01:04:44" do
  Time.zone.now.to_s(:db) # => "2004-11-24 01:04:44"  
end 

同PRより大意

参考: ActiveSupport::Testing::TimeHelpers

# activesupport/lib/active_support/testing/time_helpers.rb#L157
        if date_or_time.is_a?(Date) && !date_or_time.is_a?(DateTime)
          now = date_or_time.midnight.to_time
+       elsif date_or_time.is_a?(String)
+         now = Time.zone.parse(date_or_time)
        else
          now = date_or_time.to_time.change(usec: 0)
        end

つっつきボイス:「バグ修正のようです」「例を見るとたしかにtravel_toにStringを渡した場合にタイムゾーンが変わってる」「内部でto_timeが呼ばれるとString形式の日時からタイムゾーンが落ちてたのか」「ここを意識したことがなかったということは、今までの自分はTimeオブジェクトを渡していたということかも」

travel_toはテストで使うメソッドでしたっけ?」「travel_toのブロック内だけ日時を変更してテストしたいときなどに使いますね」

Rails: Timecopを使わなくても時間を止められた話

Rails

レガシーRailsアプリを引き継いだときにやること6つ

つっつきボイス:「記事の冒頭に、Rails 1.0がリリースされてから15周年とある」「もうそんなに経つんですね」「動画配信サービスのHuluもRailsとは知らなかった」「おそらくすべてではなくユーザーが目にするフロント部分などで使っているんでしょうね」

「ドキュメントの確認や整備、カスタムフォルダが追加されているかの確認などはいずれも大事」「カバレッジを100%に持っていくのは大変ですけどね」「ルーティングやDB構造、デプロイやstagingサーバーの構築なども同じく重要」

「この記事ではそうした作業が終わってからLinterを入れるのか: 動くことを確認してからlintをかけないとどこで壊れたかわかりにくくなることを考えれば、一理ある」「たしかに」「そうしてひととおり動くようになってコードをきれいにしてからRailsやgemのアップグレードを始める」「レガシーアプリに対応するときに順序としては基本的にこういう形になるでしょうね」「定番の作業項目をチェックできるのはよさそう👍」


同記事見出しより:

  • 1. コードレビューとローカルセットアップ
    • ドキュメントの概要をレビュー(技術的負債もチェック)
    • テストをレビュー
    • ルーティングやデータベース構造をレビュー
    • 残っているカスタムフォルダをレビュー
  • 2. テストのカバレッジを100%にする(理想的には)
  • 3. デプロイ方法のチェックとstagingサーバーのセットアップ
  • 4. RuboCopとPrettierでコードベースにlintをかける
  • 5. stagingとproductionにデプロイする
  • 6. Rails、Ruby、gemをアップグレードする

サーバーレスプロジェクトをRailsに移行する(Ruby Weeklyより)


つっつきボイス:「AWS Lambdaで動かしていたプロジェクトがつらくなってきたのでRailsに引っ越したという記事です」「たとえばサーバーサイドで複雑な処理を行うような場合はLambdaに合わないことは考えられますね」「たしかに」「AWS Lambdaはマイクロなプロセスを発行することを主に想定していますし、マイグレーション的なしくみが組み込まれていないので、CRUD的なデータ操作や重たいバッチ処理を多用するような複雑な処理をLambdaですべてまかなうのはしんどいでしょうね」

参考: AWS Lambda(イベント発生時にコードを実行)| AWS

Matestack: HTMLやJSを書かずにRailsをリアクティブにするエンジン(Ruby Weeklyより)

matestack/matestack-ui-core - GitHub


つっつきボイス:「書き方がちょっと面白かったので拾ってみました↓」「Matestackを見た感じでは、Railsに独自フロントエンドを入れてすべてをRubyで書きたいという人は今も結構いるようですね」「Basecampが出しているstimulus jsも使いたくなかったりするのかな?」「まだ新しそうなので日本語圏では情報がなさそうですが、英語情報はそこそこ出始めてるみたい」

# 同リポジトリより
class Components::Card < Matestack::Ui::Component

  requires :body
  optional :title
  optional :image

  def response
    div class: "card shadow-sm border-0 bg-light" do
      img path: image, class: "w-100" if image.present?
      div class: "card-body" do
        heading size: 5, text: title if title.present?
        paragraph class: "card-text", text: body
      end
    end
  end

end

参考: Stimulus: A modest JavaScript framework for the HTML you already have.


以下のツイートはつっつき後に見つけました。


前編は以上です。

バックナンバー(2020年度第4四半期)

週刊Railsウォッチ(20201124)strict loading violationの振る舞いを変更可能に、Railsモデルのアンチパターン、quine-relayとさまざまなクワインほか

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

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

Rails公式ニュース

Ruby Weekly


CONTACT

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