Tech Racho エンジニアの「?」を「!」に。
  • 開発

Rails 6: Action Textのファイルアップロードを分解調査する(翻訳)

概要

原著者の許諾を得て翻訳・公開いたします。

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つに分かれています。

  1. アップロード
  2. パース、変換、アタッチ
  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. 変換

  1. Action Textはcontentのツリーを探索して、[data-trix-attachment]セレクタにマッチする要素があるかどうかを調べます。
  2. 該当のノードが見つかると、data-trix-attachmentの値をパースして一連の属性に変換します。
  3. Action Textはこれらの属性を用いてaction-text-attachmentノードを初期化します。このノードをシリアライズしてhtmlにすると以下のような感じになります。

3. アタッチ

次はリッチテキストレコードに画像blobをアタッチします。しかしすべてのアタッチメントをどのように検索すればよいのでしょうか?ここで、contentオブジェクトにはattachablesというメソッド呼び出しがあることに気が付きました。このメソッドは、(ツリーを探索してコレクションすることで)その中に含まれるaction-text-attachment要素をすべて返します。

Action Textの添付ファイル(attachment)は、実際には以下の3種類です。

  1. ActiveStorage経由でアップロードしたアセット
  2. Trixパーシャル
  3. リモート画像

ActionTextはこれらの種類のattachableをすべて検索し、ActiveStorage::BlobをgrepしてActionText::RichTextレコードにアタッチしてから、レコードを保存します。

署名済みグローバルid

ここまでで何か重要な手順が1つ抜けていることに皆さんお気づきでしょうか?「attachableはいつの間にblobレコードになったのか?」と首をかしげる方もいるでしょう。

この点を説明するには、先ほどのaction-text-attachmentノード内に表示されるsgidという属性をまず理解しておく必要があります。

sgidは「署名済みグローバルid(Signed Global ID)」のことです。何のこっちゃとお思いでしょうが、意味は次のとおりです。

  1. sgidはアプリ全体のグローバル識別子である。
  2. sgidはblobオブジェクトから直接生成される。
  3. sgidには、このblobオブジェクトを検索するのに必要な必要なものがすべて含まれている(モデル名やレコードidも)
  4. sgidは他人がこのデータを書き換えられないように署名されている(署名済みグローバルidは以下のようにコンソールで調べられます)。

グローバルidや署名済みグローバルidについて詳しく知りたいのであれば、globalid gemをチェックすべきです。

ActionTextはこれのおかげで、idもモデル名も持たない添付ファイルblobを見つけられるのです。

レンダリング

テキストコンテンツの処理方法と保存方法を理解してしまえば、ActionTextでコンテンツをレンダリングする方法を推測するのは難しくありません。

  1. コンテンツをパースしてツリーにする
  2. すべての添付ファイルノードを検索する
  3. 添付ファイルの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が以下をリツイートしました🎉。

関連記事

週刊Railsウォッチ(20181203)Railsのglobalidとは、AWS LambdaがRubyに対応、JSはPromiseを最初に学べほか


CONTACT

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