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

週刊Railsウォッチ(20201116前編)6.1のActive Storageでimage_processing gemが必須に、Webアプリ設計の変遷とフロントエンド領域の再定義ほか

こんにちは、hachi8833です。

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

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

以下のコミットリストのChangelogを中心に見繕いました。ドキュメントの更新が増えているようです。

PredicateBuilderで委譲をサポート

このコミットより前は、where(id: user)where(author: user)オブジェクトを値として指定するクエリがシリアライズに成功するのは、値(この例ではuser)がActiveRecord::Baseの子孫のインスタンスである場合だけだった。
(SimpleDelegatorやdelegate_missing_toやその他の方法で)デコレーターを用いるアプリケーションでは、この変換が必須なのが少々つらくなることがある。
このコミットはPredicateBuilderを変更して、基本となる比較条件を#respond_to?呼び出しに置き換える。
同PRより大意


つっつきボイス:「where(id: user)where(author: user)のようにwhere()にActive Recordオブジェクトを渡すことができるんですが、デコレーターを通す場合は変換しないといけなかったのか」

「Changelogのコード例↓はいわゆるPORO(Plain Old Ruby Object)で、delegate_missing_to :@authorを書いておけばその後のPost.where(author: AdminAuthor.new(author))が動くように変更されたのね」

#idと主キーを委譲するオブジェクトを用いてpredicate条件をビルドする。
Changelogより

# Changelogより
class AdminAuthor
  delegate_missing_to :@author
  def initialize(author)
    @author = author
  end
end

Post.where(author: AdminAuthor.new(author))

参考: ruby - How can I determine if an object is a PORO or not? - Stack Overflow

「変更部分を見ると↓、修正前のif value.is_a?(Base)はActive Recordの場合だけ処理を進めるけど、value.respond_to?(:id)に変えたことで#idメソッドを持っているかどうかでチェックするようになった」「なるほど、Active Record以外のオブジェクトでも#idメソッドがあれば動くんですね」「どのぐらい使われるかはわかりませんが、やりたいことは見えてきました」

# activerecord/lib/active_record/relation/predicate_builder.rb#L61
    def build(attribute, value, operator = nil)
-     value = value.id if value.is_a?(Base)
+     value = value.id if value.respond_to?(:id)
      if operator ||= table.type(attribute.name).force_equality?(value) && :eq
        bind = build_bind_attribute(attribute.name, value)
        attribute.public_send(operator, bind)
      else
        handler_for(value).call(attribute, value)
      end
    end

has_one :throughでコンストラクタを使えるようになった

これに関する参考情報はガイドやドキュメントに見つからなかったが、has_one :through関連付けの関連付けコンストラクタが無効になっているように見える。自分は単にこれをテストで有効にしたところ、何も壊れなかったので、これを無効にすべき理由が何なのかを考えている。そのあたりがわかる情報をもらえると嬉しい。また、無効にすべき十分な理由がある、この変更に重要性がない、または不要なリスクがあるならば、ドキュメントを更新してhas_one :throughではbuild_associationcreate_associationを利用できないことを反映すべき(もしそうならこのPRの代わりに自分がドキュメントを変更してもよい)。
同PRより大意


つっつきボイス:「今まではhas_one :throughのときにbuild_associationcreate_associationなどのコンストラクタが使えなかったのか」「今までできなかった理由が謎」「使ってはいけないという記述がドキュメントに見当たらなかったので実装したそうです」

「テストコードでもコンストラクタのビルドをチェックしている↓」

# activerecord/test/cases/associations/has_one_through_associations_test.rb#L71
+ def test_association_build_constructor_builds_through_record
+   new_member = Member.create(name: "Chris")
+   new_club = new_member.build_club
+   assert new_member.current_membership
+   assert_equal new_club, new_member.club
+   assert_predicate new_club, :new_record?
+   assert_predicate new_member.current_membership, :new_record?
+   assert new_member.save
+   assert_predicate new_club, :persisted?
+   assert_predicate new_member.current_membership, :persisted?
+ end

「なるほど、calculate_constructableメソッドで:throughを止めてたのね↓」「:throughの場合はコンストラクタブルにならないようにわざわざチェックしてたとは」「ホントだ」「このメソッドがあった理由は謎ですが、消したらできるようになったのでプルリクしたんでしょうね」

# activerecord/lib/active_record/reflection.rb#L693
-
-     private
-       def calculate_constructable(macro, options)
-         !options[:through]
-       end

has_one :throughはあまり書かなさそうだけど、使いたいときに使えるようになったのはいいと思います」

開発時のルーティングをリロードフックの後でappendするようになった

appendしたrootルーティングが内部のwelcomeコントローラより優先されるようにする。
自分が解決しようとしているこの特定のユースケースは「rootルーティングがないアプリ」と「rootルーティングをappendするエンジン」の組み合わせである。ルーティングファイルは初期段階ではここで読み込まれるが、welcomeコントローラはここで追加されているらしい。これはつまり、現在はビルトインのルーティングがadd_builtin_routeで追加されるより「前に」実行されるエンジンイニシャライザを作成しないといけないということになる。このパッチによって、ビルトインのルーティングはルーティングファイルが評価された「後で」アプリケーションに追加されるようになる。
同PRより大意


つっつきボイス:「welcomeコントローラは、Railsのdevelopmentモードでデフォルト表示される例のWelcome画面を出すヤツですね」

「なるほど、developmentモードでビルトインのルーティング処理を移動してroutes_reloader.executeを追加している↓」「Railsエンジンとの兼ね合いでデフォルトのrootルーティングの読み込み順がうまく設定できなかったのを、ユーザー定義のルーティングを先に設定してからビルトインのルーティングを設定するように修正したらしい」「appendしたrootがそれによって内部のwelcomeコントローラより優先されるようになったんですね」

# railties/lib/rails/application/finisher.rb#L198 (移動前との差分を表示しています)
      initializer :add_builtin_route do |app|
        if Rails.env.development?
          app.routes.prepend do
            get "/rails/info/properties" => "rails/info#properties", internal: true
            get "/rails/info/routes"     => "rails/info#routes", internal: true
            get "/rails/info"            => "rails/info#index", internal: true
          end

          app.routes.append do
            get "/"                      => "rails/welcome#index", internal: true
          end

+         routes_reloader.execute
        end
      end

「たしかRailsのルーティングは先に読み込まれたものが有効になったと思うので、それも関連していたのかもしれませんね」

参考: Rails のルーティング - Railsガイド

Railsのルーティングは、ルーティングファイルの「上からの記載順に」マッチします。このため、たとえばresources :photosというルーティングがget 'photos/poll'よりも前の行にあれば、resources行のshowアクションがget行の記述よりも優先されますので、get行のルーティングは有効になりません。これを修正するには、get行をresourcesよりも上の行に移動してください。これにより、get行がマッチするようになります。
Railsガイドより

アップグレードガイドにimage_processingセクションを追加

Active Storageでimage_processing gemが必須になった

Active Storageでvariantsを処理するときに、mini_magickを直接用いるのではなくimage_processing gemをバンドルすることが必須になりました。image_processingはデフォルトでmini_magickを背後で用いるよう構成されるので、アップグレードは(訳注: Gemfileの)mini_magick gemを単にimage_processing gemに置き換えて、既に不要になったcombine_optionsが明示的に使われているのを削除しておくのが最も簡単です。
ただし、素のresize呼び出しをimage_processingのマクロに変更することが推奨されます(リサイズ後のサムネイル画像もシャープになるので)。

video.preview(resize: "100x100")
video.preview(resize: "100x100>")
video.preview(resize: "100x100^")

たとえば上をそれぞれ以下のように置き換えます。

video.preview(resize_to_fit: [100, 100])
video.preview(resize_to_limit: [100, 100])
video.preview(resize_to_fill: [100, 100])

同PRより大意

janko/image_processing - GitHub


つっつきボイス:「アップグレードガイドに追加された注意書きです」「以前Railsに導入されたimage_processing gemが6.1からは必須になったのか(ウォッチ20180511)」「これまではmini_magickかimage_processingのどちらかを使えばいいという感じだった気がします」「今後はmini_magickを使いたければimage_processing経由で使ってくれということか」「これまでmini_magickを素で使ってた人は修正が必要になりますね」

minimagick/minimagick - GitHub

Rails

onkさんの「Smart UIパターン」記事


つっつきボイス:「はてブで大バズリしていたonkさんの記事がRailsにも言及していたのでピックアップしました」「そういえば最近のTwitterでテーブルデータゲートウェイの話題を見かけた気がするけど、もしかするとこの記事がきっかけだったのかも🤔」

参考: テーブルデータゲートウェイ

「記事にも登場している『エンタープライズアプリケーションアーキテクチャパターン』(通称PoEAA)↓はやっぱり名著」「記事でもPoEAAに登場したトランザクションスクリプトやアクティブレコードやデータマッパーなどのいろんなパターンをおさらいしていますね」

エンタープライズアプリケーションアーキテクチャパターンを読む: 1.概要

「そしてRailsの密結合設計の話と@_yasaichiさんの名スライド『Ruby on Railsの正体と向き合い方も扱われていますね(ウォッチ20190401)』」「Railsは最短期間で開発できる代わりにその構造のまま大きくなるとすごく大変になる、はそのとおりだと思います」「Railsほどのラピッド開発を他のフレームワークでやるのは大変そう」

「記事にある『本当に複雑なものと、複雑ではあるが工夫で対処できるもの』という見出しはわかりやすい切り口ですね」「おぉ」「本質的に複雑なものもあるけど、複雑なりに対処できるものもあるという話」「おや、TechRacho記事も参照されている」「はい、それでこの記事に気づきました」

本当に複雑なものと、複雑ではあるが工夫で対処できるもの
同記事見出しより

「そして複雑さに対処する方法がいくつかリストアップされていますね: TrailblazerのOperationや、HanamiのInteractorなど」「rectifyは見たことなかった」

andypike/rectify - GitHub

「その次の『同機能の CUI コマンドを作るときに重複が無い』のパラグラフ↓もいいですね: 言い換えるとコントローラがコントローラの役割だけを担うようになっている」「なるほど」

Web アプリケーションとしてのインタフェース (View 以外) が Controller の役目で、「同機能の CUI コマンドを作るときに重複が無い」という基準で考えると良い。
同記事より

「『Controller or Action と 1:1 対応する新しい層』という考え方も流行りましたね: この場合ファイル数がものすごく増えてしまうんですが、そこはもう仕方がない」

「System on Record(SoR)とSystem of Engagement(SoE)という考えも見たことある↓」

「『SoEの戦場がクライアント側に移っている』もわかる: ユーザーとのインタラクション処理をフロントエンド側に移して、そこでGraphQLのインターフェイスが使えればいいという感じでやることが増えているように思います: その分SoRの複雑さについては引き続きサーバーサイドエンジニアが面倒を見る必要はあるでしょうね」


「この記事も含めて、最近の設計関連記事の多くが似たような結論に達しているのが面白いですね: たとえばActive Recordはあまりにもよくできていて、小さい問題を解決するときには非常によい、とか」

「その一方でフロントエンドでGraphQLを使うと割と手軽に同じようなこともできるようになりつつありますね: もちろんActive Recordのようにサーバーサイドでないと解決できないことは依然としてありますが」「従来だとサーバーサイドでAPIを作らないといけなかったのが、GraphQLによってフロントエンジニアがサーバーサイドエンジニアを煩わせずにやれる部分が増えてきたように思います」「それが主戦場がクライアント側に移ったということですね」

「逆にサーバーサイドでないとできないことは何かというと、データの永続化や、複雑なドメインロジックの取り扱いのような、ブラックボックスの中をいじる部分」「たしかに」

「フロントエンド側でやれることが増えてきた分、サーバーサイドエンジニアは今後そうしたブラックボックスの中というか下のレイヤに注力する方が今後生き残りやすいのかもしれないとちょっと思いました」「考えさせらる...」

「onkさんの記事、設計方法の変遷を見渡すことができて素晴らしい👍」「ありがたいです🙏」

スライド『「フロントエンド領域」を再定義する』もチェックしておきたい

「そういえばつい最近mizchiさんもフロントエンドエンジニアの職務範囲についてスライドを発表していましたね」「あ、これですか↓」「そうそう、ここで説明されているフロントエンドの変遷もいろいろ納得がいきました👍」

「従来のモノリシックなフレームワークでは↓、サーバーサイドエンジニアがJSやHTMLまでカバーしていて、マークアップエンジニアがいなければ上から下まで全部やっていた」「とても見やすい図ですね」「Railsの推奨構成は基本的にこれ」

「やがてフロントエンドフレームワークが使われるようになると↓、サーバーは永続化とAPIに集中するようになってくるけど、SSR(Server Side Rendering)をやろうとするとサーバー側かフロントエンド側のどちらかが相手側に越境しないといけなくなってつらくなってきた」「ふむふむ」

参考: 【動画付き】Next.js の Server Side Rendering (SSR) を理解する。create-react-app と比べてみた。 - Qiita

「スライドではそこから更に進んで以下のようにフロントエンドフレームワークがSSRを扱う(つまりサーバー側のレンダリングをフロントエンドで行う)ようになっている↓: そうなるとサーバー側はますますバックエンドAPIに徹するようになってきて、基本的な画面もサーバー側では扱わなくなってきたりする」「最近のフル装備フロントエンドはこんな構成なのか」「フロントエンドの職務範囲が広がってきているんですね」

「先のことはわかりませんが、将来こんなふうにフロントエンドがフルスタックになったらどうなるんだろう↓」「ここまでくると何だか逆方向に振り切って最初のレガシー図に近い形に戻ってませんか?」「結局そういうことですよね😆」「Twitterでも指摘してた人を見ました」「歴史は繰り返しつつあるのかも」

「上のようにフルスタックのフロントエンド構成になったら、APIのインターフェイス部分についてもフロントエンドが書くことになりますが、使うのはフロントエンド側なのでそこは自分たちが使いやすいように書けばいいと思います」「ただ、そのAPIの内部で使うドメインロジックについてはサーバーサイドのブラックボックスの中にしか書けないものもまだまだあるので、フロントエンドと通信するAPIのさらに裏にそのための「内部API」的なものが引き続き必要でしょうね」「APIのAPIという感じですね」「その部分がおそらくonkさんの記事で言う『本当に複雑なもの』に相当するかもしれないと最近思っています」

「少なくともサーバーサイドの仕事がいきなりなくなることはないんじゃないかなと想像してます」「現実にはサーバーサイドエンジニアも多少なりともフロントエンドを触ることになるでしょうね」「ただRailsの位置づけがバックエンドAPI中心になることが増えているのは感じています」「少々極端な想像ですが、たとえば今後Railsサーバーがフロントエンドと従来のような直接のWeb API通信をしなくなって、gRPCのようなものを介して通信する一種のドメインロジック用内部サーバーのようなものになる、という形も一応考えられますね(なお今のRailsはそういうものをまったく目指していないと思います)」

参考: サービス間通信のための新技術「gRPC」入門 | さくらのナレッジ

「Railsフレームワークの立ち位置は、ある意味でCSSフレームワークにおけるBootstrapと少し似ているかもしれませんね: プロダクトが成功してエンジニアリソースを多く割り当てられるようになり、継続開発期間が長くなるに連れて、だんだんBootstrapを必要としなくなったり好みが分かれたりすることもあるけど、プロトタイプを最初に作るときはやっぱりBootstrapが早いし便利なので今も使われ続けている、それと似たような立ち位置」「なるほど」

twbs/bootstrap - GitHub


つっつき後に、「週刊気になったITニュース」↓でも「フロントエンド領域を再定義する」スライドが取り上げられていたことに気づきました。

参考: 週刊気になったITニュース(2020/11/15号) - masa寿司の日記

Railsビューのパーシャルがパフォーマンスに与える影響(Ruby Weeklyより)


つっつきボイス:「ビューのパーシャルがパフォーマンスに与える影響の記事は周期的に見かけますね: パーシャルをループで回したりするよりコレクションを使ったりインライン展開したりする方が速いとか↓」「定番のお題ですね」


同記事より

「Railsのビュー周りは折に触れてパフォーマンスが改善されているので昔と違う部分があるかも」「定番の話ではありますがパフォーマンスへの影響が大きい部分なので、最新のRailsの実装で測定結果がどう変わっているかを知るうえでこういう記事を定期的にチェックしておくとよいと思います」「たとえばERBの速度が大きく向上したら、従来だと10倍の差だったのが20倍になったりするようなことがあるかもしれませんよね」

Railsから切り出されたgem完全ガイド

切り出されたgemがshimと呼ばれているとは知りませんでした。


つっつきボイス:「Railsからいろんな機能がgemに切り出されている」「こういう歴史的な記事はありがたい」「Rails 4.2で切り出されたgemが割と多いのか」「そうしておかないとRailsがいくらでも大きくなってしまいますね」

「shimといえばrbenvの設定にshimsディレクトリがありますね」「関連記事にもrbenvのことが書かれてる↓」「ホントだ」

参考: What is a shim?. A shim is a small library that… | by Ujjawal Dixit | Medium

RailsのトップFAQ 12件(Hacklinesより)


つっつきボイス:「質問の内容はとても基本的なんですが、頻度の高い質問に答えている記事です」「お〜、何て丁寧な回答!」「マイグレーションのロールバックとか、RubyやRailsのnilemptyblankの違いなどにみっちり答えているのがスゴい👍」「Railsにまだ慣れてない人にとって便利そう」

「と思ったら質問は12件なのに回答は5件しかない...?」「あれれ、ホントだ」「今後追加するのかな?」


後で気が付きましたが、元記事の末尾からリンクされていた以下の記事に12件のQ&Aがすべて載っていました。少々わかりにくいですね。


前編は以上です。

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

週刊Railsウォッチ(20201111後編)RubyConf 2020が11/17〜19オンライン開催、GitHub Container Registryベータ開始、スマートロックほか

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

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

Rails公式ニュース

Ruby Weekly

Hacklines

Hacklines


CONTACT

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