Railsモノリスから単一テーブルの継承を削除する方法

継承は簡単です—技術的な負債と税金を処理する必要があるまで。

Learnのメインコードベースが5年前に登場したとき、Single Table Inheritance(STI)は非常に人気がありました。当時のFlatiron Labsチームは、評価やカリキュラムからアクティビティフィードイベント、成長し続ける学習管理システム内のコンテンツに至るまですべてを使用していました。そしてそれは素晴らしかった-それは仕事を成し遂げた。インストラクターはカリキュラムを提供し、生徒の進歩を追跡し、魅力的なユーザーエクスペリエンスを作成することができました。

しかし、多くのブログ投稿が指摘しているように(この記事、この記事、この記事の例など)、STIは特にデータが大きくなり、新しいサブクラスがスーパークラスや相互に大きく変化し始めるため、あまりうまく拡張できません。ご想像のとおり、コードベースでも同じことが起こりました。私たちの学校は拡大し、より多くの機能とレッスンタイプをサポートしました。時間が経つにつれて、モデルは膨張し、変化し始め、ドメインの正しい抽象化を反映しなくなりました。

私たちはしばらくそのスペースに住んでいて、そのコードに広い停泊地を与え、必要なときだけパッチを適用しました。そして、リファクタリングの時が来ました。

過去数ヶ月にわたって、私は、STIの特に厄介な1つのインスタンスを削除するという使命に乗り出しました。 STIは最初のセットアップと同じくらい簡単ですが、実際に削除するのはかなり困難です。

そのため、この投稿では、STIについて少し説明し、ドメインに関するコンテキストを提供し、作業範囲を概説し、変更を安全に展開するために採用した戦略について説明します。アプリの。

単一テーブル継承(STI)について

簡単に言えば、Railsの単一テーブル継承を使用すると、複数のタイプのクラスを同じテーブルに格納できます。 Active Recordでは、クラス名はタイプとしてテーブルに保存されます。たとえば、Labs、Readme、およびProjectがすべて目次に存在する場合があります。

クラスLab <コンテンツ;終わり
クラスReadme 

この例では、ラボ、readme、およびプロジェクトはすべて、レッスンに関連付けることができるタイプのコンテンツです。

コンテンツテーブルのスキーマは少しこのように見えたので、タイプがテーブルに格納されていることがわかります。

create_table "content"、force::cascade do | t |
  t.integer "curriculum_id"、
  t.string "type"、
  t.text "markdown_format"、
  t.string "title"、
  t.integer "track_id"、
  t.integer "github_repository_id"
終わり

作業範囲の特定

コンテンツはアプリ全体に広がり、時には混乱を招きます。たとえば、これはレッスンモデルの関係を説明しています。

クラスレッスン<カリキュラム
  has_many:contents、-> {order(ordinal::asc)}
  has_one:content、foreign_key::curriculum_id
  has_many:readmes、foreign_key::curriculum_id
  has_one:lab、foreign_key::curriculum_id
  has_one:readme、foreign_key::curriculum_id
  has_many:assigned_repos、through::contents
終わり

混乱した?私もそうでした。それは、私が変えなければならなかった多くのモデルの1つにすぎません。

そのため、才能のある才能のあるチームメイト(ケイトトラバーズ、スティーブンヌネス、スペンサーロジャース)とともに、混乱を減らし、このシステムを拡張しやすくするために、より良い設計をブレインストーミングしました。

新しいデザイン

コンテンツが表現しようとしていたコンセプトは、GithubRepositoryとレッスンの間の仲介でした。

「標準的な」レッスンコンテンツの各部分は、GitHubのリポジトリにリンクされています。レッスンが学生に公開または「展開」されると、そのGitHubリポジトリのコピーを作成し、学生にリンクを提供します。レッスンとデプロイされたバージョン間のリンクは、AssignedRepoと呼ばれます。

そのため、レッスンの両端にGitHubリポジトリがあります:正規バージョンとデプロイ済みバージョンです。

クラスContent 
クラスAssignedRepo 

ある時点で、レッスンは複数のコンテンツを持つことができましたが、現在の世界ではそうではありません。代わりに、さまざまな種類のレッスンがあり、関連するリポジトリに含まれるファイルを調べることで自分自身を内省できます。

そのため、コンテンツをCanonicalMaterialと呼ばれる新しいコンセプトに置き換え、AssignedRepoにコンテンツを介さずに関連するレッスンへの直接参照を与えることにしました。

古いシステム図から新しいシステム図、赤い点線は廃止予定のパスを示しています

混乱を招き、多くの作業が必要な場合は、それが理由です。しかし、重要なポイントは、かなり大きなコードベースのモデルを交換する必要があり、最終的に6000行のコードのどこかで変更することになったことです。

ただし、重要なポイントは、かなり大きなコードベースのモデルを交換する必要があり、最終的に6000行のコードの領域でどこかを変更することになったことです。

STIのリファクタリングと置換の戦略

新しいモデル

まず、canonical_materialsという新しいテーブルを作成し、新しいモデルと関連付けを作成しました。

クラスCanonicalMaterial 

また、レッスンで参照を維持できるように、canonical_material_idの外部キーをカリキュラムテーブルに追加しました。

assign_reposテーブルに、lesson_id列を追加しました。

デュアルライト

新しいテーブルと列が配置された後、古いテーブルと新しいテーブルに同時に書き込みを開始したため、バックフィルタスクを複数回実行する必要はありません。コンテンツ行を作成または更新しようとするたびに、canonical_materialも作成または更新します。

例えば:

lesson.build_content(
  'repo_name' => repo.name、
  'github_repository_id' => repo_id、
  'markdown_format' => repo.readme
)

lesson.canonical_material = repo.canonical_material
lesson.save

これにより、最終的にコンテンツを削除するための基礎を築くことができました。

埋め戻し

プロセスの次のステップは、データを埋め戻すことでした。テーブルにデータを入力し、各GithubRepositoryにCanonicalMaterialが存在し、各レッスンにCanonicalMaterialがあることを確認するために、rakeタスクを作成しました。そして、実稼働サーバーでタスクを実行しました。

このリファクタリングのラウンドでは、有効なデータを使用することをお勧めしました。そうすることで、従来の方法で問題を解決できます。ただし、別の実行可能なオプションは、古いモデルを引き続きサポートするコードを記述することです。私たちの経験では、従来の思考をサポートするコードを維持する方が、バックフィルしてデータが有効であることを確認するよりも混乱を招き、費用がかかりました。

私たちの経験では、従来の思考をサポートするコードを維持する方が、バックフィルしてデータが有効であることを確認するよりも混乱を招き、費用がかかりました。

置換

そして、楽しい部分が始まりました。交換を可能な限り安全にするために、機能フラグを使用して、より小さなPRにダークコードを送信しました。これを行うには、標準機能の開発にも使用するロールアウトgemを使用しました。

検索対象

交換を行うことの最も難しい部分の1つは、検索するものが非常に多いことでした。 「コンテンツ」という言葉は残念ながら非常に一般的であるため、単純なグローバル検索および置換を実行することは不可能でした。そのため、バリエーションを考慮して、よりスコープの広い検索を実行する傾向がありました。

STIを削除するときは、次のものを検索する必要があります。

  • すべてのサブクラス、メソッド、ユーティリティメソッド、関連付け、クエリなど、モデルの単数形と複数形。
  • ハードコードされたSQLクエリ
  • コントローラー
  • シリアライザー
  • 視聴回数

たとえば、コンテンツの場合、それは以下を探すことを意味しました:

  • :content —関連付けとクエリ用
  • :contents —アソシエーションとクエリ用
  • .joins(:contents)—結合クエリ用。以前の検索でキャッチする必要があります
  • .includes(:contents)—二次関連を積極的にロードするためのもので、以前の検索でもキャッチする必要があります
  • 内容:—ネストされたクエリ用
  • 内容:—繰り返しますが、よりネストされたクエリ
  • content_id — IDによる直接クエリ用
  • .content —メソッド呼び出し
  • .contents —コレクションメソッドの呼び出し
  • .build_content — has_oneおよびbelongs_toアソシエーションによって追加されたユーティリティメソッド
  • .create_content — has_oneおよびbelongs_toアソシエーションによって追加されたユーティリティメソッド
  • .content_ids — has_many関連付けによって追加されたユーティリティメソッド
  • 内容—クラス名自体
  • 内容—ハードコーディングされた参照またはSQLクエリのプレーン文字列

これはコンテンツのかなり包括的なリストだと思います。そして、ラボ、readme、およびプロジェクトについても同じことをしました。 Railsは非常に柔軟性が高く、多くのユーティリティメソッドが追加されているため、モデルが使用されている場所をすべて見つけることは困難です。

すべての呼び出し元を見つけた後、実際に実装を置き換える方法

交換または削除しようとしているモデルのすべての通話サイトを実際に見つけたら、書き直します。一般的に、私たちが従ったプロセスは

  1. 定義内のメソッドの動作を置き換えるか、呼び出しサイトでメソッドを変更します
  2. 新しいメソッドを作成し、呼び出しサイトで機能フラグの背後で呼び出します
  3. メソッドとの関連付けの依存関係を解除する
  4. 方法がわからない場合は、機能フラグの背後でエラーを発生させます
  5. 同じインターフェースを持つオブジェクトのスワップ

各戦略の例を次に示します。

1a。メソッドの動作またはクエリを置き換える

置換のいくつかは非常に簡単です。 「このフラグがオンの場合、この他のコードの代わりにこのコードを呼び出す」と言うために、機能フラグを設定します。

したがって、コンテンツに基づいてクエリを実行する代わりに、ここではcanonical_materialに基づいてクエリを実行します。

1b。呼び出しサイトでメソッドを変更します

時々、呼び出されたメソッドを標準化するために、呼び出しサイトでメソッドを置き換える方が簡単です。 (これを行う場合は、テストスイートを実行するか、テストを作成する必要があります。)そうすることで、リファクタリングをさらに進めることができます。

この例は、まもなく存在しなくなるcanonical_id列への依存関係を解消する方法を示しています。呼び出しサイトのメソッドを、機能フラグの後ろに配置せずに置き換えたことに注意してください。このリファクタリングを行う際に、canonical_idを複数の場所で取り出していることに気付いたので、他のクエリにチェーンできる別のメソッドでそれを行うためのロジックをラップしました。呼び出しサイトのメソッドは変更されましたが、機能フラグがオンになるまで動作は変わりませんでした。

2.新しいメソッドを作成し、呼び出しサイトの機能フラグの背後で呼び出します

この戦略はメソッドの置換に関連しています。このメソッドでのみ、新しいメソッドを作成し、呼び出しサイトで機能フラグの背後で呼び出します。これは、1か所でしか呼び出されないメソッドに特に役立ちました。また、このメソッドにより良い署名を与えることができました。常に便利です。

3.メソッドとの関連付けの依存関係を解除する

次の例では、has_many labsというトラックがあります。 has_manyアソシエーションがユーティリティメソッドを追加することがわかっているため、最も一般的に呼び出されるものを置き換え、has_many:labs行を削除しました。このメソッドは同じインターフェースに準拠しているため、機能がオンになる前にメソッドを呼び出していたものはすべて機能し続けます。

4.方法がわからない場合は、機能フラグの背後でエラーを発生させます

通話サイトを見逃したかどうかわからないこともありました。そのため、最初はメソッドを完全に削除するのではなく、意図的にエラーを発生させて、手動テスト段階でエラーをキャッチできるようにしました。これにより、メソッドが呼び出された場所を追跡するためのより良い方法が得られました。

5.同じインターフェースを持つオブジェクトのスワップ

ラボの関連付けを削除したかったため、ラボの実装を書き直しましたか?方法。ラボレコードの存在を確認する代わりに、canonical_materialを交換し、呼び出しを委任し、そのオブジェクトを同じメソッドに応答させました。

これらは、Railsモノリス全体で依存関係を解消し、新しいオブジェクトを交換するための最も役立つ戦略でした。数百の定義と呼び出しサイトを確認した後、それらを1つずつ置換または書き換えました。誰にも望まない退屈なプロセスですが、最終的にはコードベースを読みやすくし、何もしていなかった古いコードを削除するのに非常に役立ちました。最後までイライラして髪を引っ張るのに数週間かかりましたが、参照の大部分を置き換えた後、手動テストを開始しました。

テストと手動テスト

変更はコードベース全体の機能に影響を与えたため、一部はテストされていなかったため、QAを確実に行うことは困難でしたが、最善を尽くしました。 QAサーバーで手動テストを実行しましたが、多くのバグとエッジケースが見つかりました。そして、私たちは先に進み、よりクリティカルなパスのために、新しいテストを作成しました。

ロールアウト、稼働、クリーンアップ

QAに合格した後、機能フラグをオンにし、システムを安定させました。安定していることを確認した後、機能フラグと古いコードパスをコードベースから削除しました。悲しいことに、これは多くのテストスイート、主にコンテンツモデルに暗黙的に依存する工場の書き換えを伴うため、予想よりも困難でした。振り返ってみると、リファクタリング中に2セットのテストを書くことができました。1つは現在のコード用で、もう1つは機能フラグの背後にあるコード用です。

今後の最終ステップとして、データをバックアップし、未使用のテーブルを削除する必要があります。

そして、それは、Railsのモノリスに広がる単一テーブルの継承をなくす方法の1つです。おそらく、このケーススタディも役立ちます。

STIを削除またはリファクタリングする他の方法はありますか?知りたいです。コメントで教えてください。

また、雇用しています!私たちのチームに参加してください。かっこいいです、約束します。

リソースと追加資料

  • Railsガイドの継承
  • Railsで単一テーブル継承を使用する方法とタイミングby Eugene Wang(Flatiron Grad!)
  • 単一テーブルの継承からRailsアプリをリファクタリングする
  • Railsでの単一テーブルの継承とポリモーフィックな関連付け
  • Rails 5.02を使用した単一テーブルの継承

Flatiron Schoolの詳細については、Webサイトにアクセスし、FacebookおよびTwitterでフォローしてください。また、近くの今後のイベントにアクセスしてください。

Flatiron SchoolはWeWorkファミリーの一員です。姉妹テクノロジーのブログであるWeWork TechnologyとMaking Meetupをご覧ください。