概要
原著者の許諾を得て翻訳・公開いたします。
- 英語記事: Disassembling Rails — How does ActionText deal with file upload?
- 原文公開日: 2019/08/06
- 著者: Stan Lo -- Goby言語の作者でありRails開発者/コントリビューターです。
Rails 6: Action Textのファイルアップロードを分解調査する(翻訳)
原注: 本記事はRails 6.0.0.rc2を元にしています。
正直に申し上げると、Action Textコンポーネントに対してこれといった興味はありませんでした。Action Textを使うことは業務でもサブプロジェクトでもまずありそうになかったからです。しかし#36177を解決するために多少時間を取ってAction Textを調べました。
Action Textはある種の「沼」なのですが、現時点ではどうもこれといったオンライン資料が見当たらないので、自分が調べてわかった範囲で本記事にまとめました。本記事がAction Textに取り組み始めた人にもお役に立てばと願っています。
本記事のテーマはAction Textのファイルアップロード機能なので、Active Storageコンポーネント(主にダイレクトアップロード機能)についても多少言及します。
全体の手順は以下の3つに分かれています。
- アップロード
- パース、変換、アタッチ
- レンダリング
(Action Textを調べているうちに、あっと驚くものを見つけましたので、本記事の最後に書いておきます😉)
1. アップロード
本セクションはAction Textとはあまり関連していませんが、ブラウザのテキスト入力エリアに画像をドラッグアンドドロップする仕組みもめちゃめちゃ面白いと思います(さてどうでしょう?)😄。
1. RailsのJavaScriptライブラリは、最初に以下のようなファイルメタデータを持つPOSTリクエストを/rails/active_storage/direct_uploads
に送信します(このエンドポイントは設定変更可能です)。
2. RailsはActiveStorage::Blob
レコードを1件作成し、ダイレクトアップロード用のURLとそのblobの署名済みidを返します。
3. JSライブラリは、ダイレクトアップロードのURLを受け取るとそのファイルアップロードを実行します。
4. それと並行してTrixエディタのテキストエリアに画像が挿入され、画像がfigure
要素でラップされます。アップロードされたファイルのメタデータは、figure
要素のdata-trix-attachment
属性の中に配置されます。
この時点までに、アップロードした画像はページで表示され、アップロードしたファイルはストレージに、画像のblobレコードはデータベースにそれぞれ保存されます🎉。
しかしRailsは、次の手順でテキストコンテンツを保存するまでは、blobレコードをアタッチする対象をまだ認識していません。
パース、変換、アタッチ
テキストコンテンツを無事に受け取っても、まだデータベースには保存できません。以下のような作業がまだ残っています。
- その画像のblobレコードは現時点では孤立しているので、
ActionText::RichText
レコードなどにアタッチする方法を見つける必要があります。 - テキストコンテンツを必ずしもTrixエディタに表示するとは限らないので、Trixの要素を直接保存するのはよい考えとは言えません。つまり、Action Textで何らかの変換をかけておく必要があります。
以上を実行するために、まずコンテンツをパースする必要があります。
1. パース
ActionText::RichText
オブジェクトが初期化されるときに、テキストコンテンツはActionText::Content
オブジェクトの初期化に使われます(このオブジェクトはこの後のセクションでcontentオブジェクトと呼ぶことにします)。
このcontentオブジェクトは、コンテンツをツリー構造で保存するので、内部の要素を検索/置換するのは簡単です。
2. 変換
- Action Textはcontentのツリーを探索して、
[data-trix-attachment]
セレクタにマッチする要素があるかどうかを調べます。 - 該当のノードが見つかると、
data-trix-attachment
の値をパースして一連の属性に変換します。 - Action Textはこれらの属性を用いて
action-text-attachment
ノードを初期化します。このノードをシリアライズしてhtmlにすると以下のような感じになります。
3. アタッチ
次はリッチテキストレコードに画像blobをアタッチします。しかしすべてのアタッチメントをどのように検索すればよいのでしょうか?ここで、contentオブジェクトにはattachables
というメソッド呼び出しがあることに気が付きました。このメソッドは、(ツリーを探索してコレクションすることで)その中に含まれるaction-text-attachment
要素をすべて返します。
Action Textの添付ファイル(attachment)は、実際には以下の3種類です。
ActiveStorage
経由でアップロードしたアセットTrix
パーシャル- リモート画像
ActionText
はこれらの種類のattachableをすべて検索し、ActiveStorage::Blob
をgrepしてActionText::RichText
レコードにアタッチしてから、レコードを保存します。
署名済みグローバルid
ここまでで何か重要な手順が1つ抜けていることに皆さんお気づきでしょうか?「attachableはいつの間にblobレコードになったのか?」と首をかしげる方もいるでしょう。
この点を説明するには、先ほどのaction-text-attachment
ノード内に表示されるsgid
という属性をまず理解しておく必要があります。
sgid
は「署名済みグローバルid(Signed Global ID)」のことです。何のこっちゃとお思いでしょうが、意味は次のとおりです。
sgid
はアプリ全体のグローバル識別子である。sgid
はblobオブジェクトから直接生成される。sgid
には、このblobオブジェクトを検索するのに必要な必要なものがすべて含まれている(モデル名やレコードidも)sgid
は他人がこのデータを書き換えられないように署名されている(署名済みグローバルidは以下のようにコンソールで調べられます)。
グローバルidや署名済みグローバルidについて詳しく知りたいのであれば、globalid gemをチェックすべきです。
ActionText
はこれのおかげで、idもモデル名も持たない添付ファイルblobを見つけられるのです。
レンダリング
テキストコンテンツの処理方法と保存方法を理解してしまえば、ActionText
でコンテンツをレンダリングする方法を推測するのは難しくありません。
- コンテンツをパースしてツリーにする
- すべての添付ファイルノードを検索する
- 添付ファイルの
sgid
で画像blobをグローバルに検索する
さてそれでは、本記事の冒頭で予告した「あっと驚くもの」で締めくくりましょう。n+1クエリを見つけてしまったのです!!!
ActionText
は、コンテンツやblobをeager loadするために、以下のようなスコープをいくつか提供しています。
にもかかわらず、テキストコンテンツをレンダリングするときに、アプリケーションでn+1クエリが生み出されていることがわかります。一体どうなっているのでしょうか?
原因は、blobレコードをsgid
で検索しているからです!
上は本質的に以下のようなものです。
この動作が添付ファイルの要素ごとに行われるので、eager loadingのスコープの恩恵をまったく得られません。実際、場合によってはこれらのスコープを使うと、利用できないデータをeager loadするクエリが余分に作成されるため、むしろ悪化することすらあります(#36177で報告されているissueがそれです)。
訳注: #36177は記事公開時点で引き続きopenになっています。
まとめ
Action Textをあれこれ調べたり遊んだりしたことで、Action Textというコンポーネントの設計が実によくできていて、リッチテキストエディタが必要になった場合のベストチョイスだと思うようになりました。ただし、Action Textにはまだ改良の余地が残されています。前述のn+1クエリなどがそうですね。
最後になりますが、既にAction Textを使い始めている方から本記事にフィードバックをいただけるとうれしく思います。ぜひみんなでこの素晴らしいAction Textを使いやすいコンポーネントとして成熟させましょう😄。
おたより発掘
↓DHHが以下をリツイートしました🎉。
🕵️♂️ Deep dive into Rails by @_st0012 following the path of a file upload through Trix → ActionText → ActiveStorage and back. https://t.co/KoDDOdBbs6
— Javan Makhmali (@javan) August 9, 2019
関連記事
週刊Railsウォッチ(20181203)Railsのglobalidとは、AWS LambdaがRubyに対応、JSはPromiseを最初に学べほか