So entfernen Sie die Vererbung einzelner Tabellen von Ihrem Rails-Monolithen

Vererbung ist einfach - bis Sie sich mit technischen Schulden und Steuern auseinandersetzen müssen.

Als vor fünf Jahren die Hauptcodebasis von Learn ins Leben gerufen wurde, war Single Table Inheritance (STI) recht beliebt. Das damalige Team von Flatiron Labs hat alles getan - von Assessments und Lehrplänen bis hin zu Aktivitätsfeed-Ereignissen und Inhalten in unserem wachsenden Lernmanagementsystem. Und das war großartig - es hat die Arbeit erledigt. Es ermöglichte Lehrern, Lehrpläne zu erstellen, den Fortschritt der Schüler zu verfolgen und eine ansprechende Benutzererfahrung zu schaffen.

Aber wie viele Blog-Posts bereits erwähnt haben (dieser, dieser und dieser zum Beispiel), lässt sich STI nicht besonders gut skalieren, zumal die Datenmengen zunehmen und neue Unterklassen beginnen, sich stark von ihren Oberklassen und voneinander zu unterscheiden. Wie Sie vielleicht erraten haben, ist das auch in unserer Codebasis passiert! Unsere Schule wurde erweitert und wir unterstützten immer mehr Funktionen und Unterrichtsarten. Im Laufe der Zeit begannen sich die Modelle aufzublähen und zu mutieren und spiegeln nicht mehr die richtige Abstraktion für die Domäne wider.

Wir haben eine Weile in diesem Raum gelebt, dem Code einen weiten Bogen gemacht und ihn nur dann geflickt, wenn es nötig war. Und dann war es an der Zeit, umzugestalten.

In den letzten Monaten habe ich mich auf die Mission gemacht, eine besonders krasse Instanz von STI zu entfernen, bei der es sich um das etwas mehrdeutig benannte Inhaltsmodell handelt. So einfach die Einrichtung von STI ist, so schwierig ist es tatsächlich, STI zu entfernen.

In diesem Beitrag werde ich mich ein wenig mit STI befassen, einen Kontext zu unserer Domäne erläutern, den Arbeitsumfang skizzieren und Strategien erläutern, die ich eingesetzt habe, um Änderungen sicher zu implementieren und gleichzeitig die Oberfläche für schwerwiegende Schäden zu minimieren, während ich den Kern entkernte unserer App.

Informationen zur Einzeltabellenvererbung (STI)

Kurz gesagt, mit Single Table Inheritance in Rails können Sie mehrere Klassentypen in derselben Tabelle speichern. In Active Record wird der Klassenname als Typ in der Tabelle gespeichert. Beispiel: In der Inhaltsübersicht befinden sich möglicherweise Lab, Readme und Project:

class Lab 

In diesem Beispiel sind Labs, Readmes und Projekte alle Arten von Inhalten, die einer Lektion zugeordnet werden können.

Das Schema unseres Inhaltsverzeichnisses sah ungefähr so ​​aus, sodass Sie sehen können, dass der Typ nur in der Tabelle gespeichert ist.

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"
Ende

Ermittlung des Arbeitsumfangs

Inhalt breitete sich in der gesamten App aus, manchmal verwirrend. In diesem Beispiel wurden die Beziehungen im Unterrichtsmodell beschrieben.

Unterrichtsstunde  {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: assign_repos, through:: contents
Ende

Verwirrt? Ich auch. Und das war nur ein Modell von vielen, das ich ändern musste.

Daher habe ich mit meinen brillanten und talentierten Teamkollegen (Kate Travers, Steven Nunez und Spencer Rogers) ein besseres Design entwickelt, um die Verwirrung zu verringern und die Erweiterung des Systems zu vereinfachen.

Ein neues Design

Das Konzept, das Content darzustellen versuchte, war ein Vermittler zwischen einem GithubRepository und einer Lektion.

Jeder „kanonische“ Unterrichtsinhalt ist mit einem Repository auf GitHub verknüpft. Wenn Lektionen für Schüler veröffentlicht oder „bereitgestellt“ werden, erstellen wir eine Kopie dieses GitHub-Repositorys und geben den Schülern einen Link dazu. Die Verknüpfung zwischen einer Lektion und der bereitgestellten Version wird als AssignedRepo bezeichnet.

Es gibt also GitHub-Repositorys an beiden Enden des Unterrichts: die kanonische Version und die implementierte Version.

Klasse Inhalt 
class AssignedRepo 

Früher konnten die Lektionen mehrere Inhalte enthalten, aber in unserer heutigen Welt ist dies nicht mehr der Fall. Stattdessen gibt es verschiedene Arten von Lektionen, die sich anhand der in den zugehörigen Repositorys enthaltenen Dateien selbst ansehen können.

Daher haben wir beschlossen, den Inhalt durch ein neues Konzept namens CanonicalMaterial zu ersetzen und dem AssignedRepo einen direkten Verweis auf die zugehörige Lektion zu geben, anstatt den Inhalt durchzugehen.

Alt-zu-Neu-Systemdiagramm, in dem rot gepunktete Linien Pfade angeben, die als veraltet markiert sind

Wenn das verwirrend klingt und viel Arbeit bedeutet, liegt es daran, dass Das Wichtigste ist jedoch, dass wir ein Modell in einer ziemlich großen Codebasis ersetzen mussten und es irgendwo im Bereich von 6000 Codezeilen geändert haben.

Das Wichtigste ist jedoch, dass wir ein Modell in einer ziemlich großen Codebasis ersetzen mussten und es irgendwo im Bereich von 6000 Codezeilen geändert haben.

Strategien für das Refactoring und Ersetzen von STI

Das neue Modell

Zuerst haben wir eine neue Tabelle mit dem Namen canonical_materials erstellt und das neue Modell und die neuen Assoziationen erstellt.

Klasse CanonicalMaterial 

Wir haben der Curriculums-Tabelle auch den Fremdschlüssel canonical_material_id hinzugefügt, damit in einer Lektion ein Verweis darauf beibehalten werden kann.

Wir haben der Tabelle assigned_repos die Spalte lesson_id hinzugefügt.

Duale Schreibvorgänge

Nachdem die neuen Tabellen und Spalten vorhanden waren, haben wir begonnen, gleichzeitig in die alten und neuen Tabellen zu schreiben, sodass wir eine Backfill-Aufgabe nicht mehr als einmal ausführen müssen. Jedes Mal, wenn versucht wird, eine Inhaltszeile zu erstellen oder zu aktualisieren, wird auch ein kanonisches_Material erstellt oder aktualisiert.

Beispielsweise:

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

lesson.canonical_material = repo.canonical_material
lesson.save

Dies ermöglichte es uns, den Grundstein für die endgültige Entfernung von Inhalten zu legen.

Hinterfüllung

Der nächste Schritt in diesem Prozess war das Auffüllen der Daten. Wir haben Rechenaufgaben geschrieben, um unsere Tabellen zu füllen und sicherzustellen, dass für jedes GithubRepository ein CanonicalMaterial vorhanden ist und dass jede Lektion ein CanonicalMaterial enthält. Und dann haben wir die Aufgaben auf unserem Produktionsserver ausgeführt.

In dieser Refactoring-Runde haben wir es vorgezogen, gültige Daten zu haben, damit wir einen sauberen Bruch mit der alten Vorgehensweise machen können. Eine andere praktikable Option besteht jedoch darin, Code zu schreiben, der noch ältere Modelle unterstützt. Nach unserer Erfahrung war es verwirrender und kostspieliger, Code zu verwalten, der das traditionelle Denken unterstützt, als das Auffüllen und Sicherstellen, dass die Daten gültig sind.

Nach unserer Erfahrung war es verwirrender und kostspieliger, Code zu verwalten, der das traditionelle Denken unterstützt, als das Auffüllen und Sicherstellen, dass die Daten gültig sind.

Ersatz

Und dann begann der lustige Teil. Um den Austausch so sicher wie möglich zu machen, verwendeten wir Feature-Flags, um dunklen Code in kleineren PRs zu versenden. Auf diese Weise konnten wir eine schnellere Rückkopplungsschleife erstellen und schneller feststellen, ob Probleme aufgetreten sind. Dazu haben wir das Rollout-Juwel verwendet, das wir auch für die Entwicklung von Standardfunktionen verwenden.

Nach was zu suchen

Eine der schwierigsten Aufgaben bei der Ersetzung war die Vielzahl der zu suchenden Aufgaben. Das Wort „Inhalt“ ist leider sehr allgemein gehalten, so dass es unmöglich war, eine einfache, globale Suche und Ersetzung durchzuführen. Daher habe ich mich eher auf eine Suche mit größerem Umfang konzentriert, um die Variationen zu berücksichtigen.

Wenn Sie STI entfernen, sollten Sie nach folgenden Kriterien suchen:

  • Die Singular- und Pluralform des Modells, einschließlich aller Unterklassen, Methoden, Nutzmethoden, Assoziationen und Abfragen.
  • Hardcodierte SQL-Abfragen
  • Controller
  • Serialisierer
  • Ansichten

Für den Inhalt bedeutete das beispielsweise, nach Folgendem zu suchen:

  • : content - für Assoziationen und Abfragen
  • : Inhalte - für Assoziationen und Abfragen
  • .joins (: contents) - für Join-Abfragen, die bei der vorherigen Suche abgefangen werden sollen
  • .includes (: contents) - für eifrige Verknüpfungen zweiter Ordnung, die auch von der vorherigen Suche erfasst werden sollten
  • Inhalt: - für verschachtelte Abfragen
  • Inhalt: - wieder mehr verschachtelte Abfragen
  • content_id - für Abfragen direkt nach ID
  • .content - Methodenaufrufe
  • .contents - Aufrufe von Erfassungsmethoden
  • .build_content - Dienstprogrammmethode, die von der Zuordnung has_one und belong_to hinzugefügt wurde
  • .create_content - Dienstprogrammmethode, die von der Zuordnung has_one und belong_to hinzugefügt wurde
  • .content_ids - Dienstprogrammmethode, die von der Zuordnung has_many hinzugefügt wurde
  • Inhalt - der Klassenname selbst
  • Inhalt - die einfache Zeichenfolge für fest codierte Verweise oder SQL-Abfragen

Ich glaube, das ist eine ziemlich umfassende Liste für den Inhalt. Und dann habe ich dasselbe für Labor, Readme und Projekt gemacht. Sie können sehen, dass es schwierig ist, alle Stellen zu finden, an denen ein Modell verwendet wird, da Rails so flexibel ist und viele Dienstprogrammmethoden hinzufügt.

So ersetzen Sie die Implementierung tatsächlich, nachdem Sie alle Anrufer gefunden haben

Sobald Sie alle Anrufseiten des Modells gefunden haben, das Sie ersetzen oder entfernen möchten, können Sie die Dinge umschreiben. Im Allgemeinen war der Prozess, dem wir folgten

  1. Ersetzen Sie das Methodenverhalten in der Definition oder ändern Sie die Methode an der Aufrufstelle
  2. Schreiben Sie neue Methoden und rufen Sie sie hinter einem Feature-Flag an der Aufrufstelle auf
  3. Abhängigkeiten von Assoziationen zu Methoden aufheben
  4. Lösen Sie Fehler hinter einem Feature-Flag aus, wenn Sie sich über eine Methode nicht sicher sind
  5. Tauschen Sie Objekte mit derselben Schnittstelle aus

Hier sind Beispiele für jede Strategie.

1a. Ersetzen Sie das Methodenverhalten oder die Abfrage

Einige der Ersetzungen sind ziemlich einfach. Sie setzen das Feature-Flag so ein, dass es sagt: "Rufen Sie diesen Code anstelle dieses anderen Codes auf, wenn dieses Flag aktiviert ist."

Anstatt also basierend auf Inhalten abzufragen, fragen wir hier basierend auf canonical_material ab.

1b. Ändern Sie die Methode an der Anrufstelle

Manchmal ist es einfacher, die Methode am Aufrufstandort zu ersetzen, um die aufgerufenen Methoden zu standardisieren. (Sie sollten Ihre Testsuite ausführen und / oder Tests schreiben, wenn Sie dies tun.) Dies kann den Weg für weiteres Refactoring ebnen.

In diesem Beispiel wird gezeigt, wie die Abhängigkeit von der Spalte canonical_id aufgehoben wird, die bald nicht mehr vorhanden sein wird. Beachten Sie, dass wir die Methode am Aufrufstandort ersetzt haben, ohne dass dies hinter einem Feature-Flag steht. Bei diesem Refactoring ist uns aufgefallen, dass wir die canonical_id an mehreren Stellen abgelegt haben. Daher haben wir die Logik in eine andere Methode eingeschlossen, mit der wir andere Abfragen verketten können. Die Methode am Anrufstandort wurde geändert, das Verhalten wurde jedoch erst geändert, nachdem das Feature-Flag aktiviert wurde.

2. Schreiben Sie neue Methoden und rufen Sie sie hinter einem Feature-Flag an der Aufrufstelle auf

Diese Strategie bezieht sich auf das Ersetzen der Methode. Nur in dieser schreiben wir eine neue Methode und rufen sie hinter einem Feature-Flag an der Aufrufstelle auf. Es war besonders nützlich für eine Methode, die nur an einer Stelle aufgerufen wurde. Es ermöglichte uns auch, der Methode eine bessere Signatur zu geben - immer nützlich.

3. Abhängigkeiten von Assoziationen zu Methoden aufheben

In diesem nächsten Beispiel hat eine Spur viele Labors. Da wir wissen, dass die Zuordnung has_many Dienstprogrammmethoden hinzufügt, haben wir die am häufigsten aufgerufene ersetzt und die Zeile has_many: labs entfernt. Diese Methode entspricht der gleichen Schnittstelle, sodass alles, was die Methode vor dem Aktivieren der Funktion aufrief, weiterhin funktioniert.

4. Lösen Sie Fehler hinter einem Feature-Flag aus, wenn Sie sich über eine Methode nicht sicher sind

Manchmal waren wir uns nicht sicher, ob wir einen Anruf verpasst haben. Anstatt zunächst nur harte Methoden zu entfernen, haben wir absichtlich Fehler gemeldet, damit wir sie während der manuellen Testphase abfangen können. Dies gab uns eine bessere Möglichkeit, herauszufinden, wo eine Methode aufgerufen wurde.

5. Tauschen Sie Objekte mit derselben Schnittstelle aus

Weil wir die Laborassoziation loswerden wollten, haben wir die Implementierung des Labors umgeschrieben? Methode. Anstatt auf das Vorhandensein eines Laborprotokolls zu prüfen, haben wir das kanonische_Material ausgetauscht, den Aufruf delegiert und dieses Objekt auf dieselbe Methode antworten lassen.

Dies waren die hilfreichsten Strategien, um Abhängigkeiten aufzubrechen und neue Objekte in unserem Rails-Monolithen einzutauschen. Nach Durchsicht der Hunderte von Definitionen und Aufrufseiten haben wir sie einzeln ersetzt oder neu geschrieben. Es ist ein langwieriger Prozess, den ich niemandem wünsche, aber er war letztendlich äußerst hilfreich, um die Lesbarkeit unserer Codebasis zu verbessern und alten Code zu entfernen, der herumstand und nichts tat. Es dauerte einige frustrierende und haarsträubende Wochen, bis wir das Ende erreicht hatten, aber nachdem wir die meisten Referenzen ersetzt hatten, begannen wir mit manuellen Tests.

Testen & Manuelles Testen

Da sich die Änderungen auf Funktionen in der gesamten Codebasis auswirkten, von denen einige nicht getestet wurden, war die Qualitätssicherung mit Sicherheit schwierig, aber wir haben unser Bestes gegeben. Wir haben einen manuellen Test auf unserem QS-Server durchgeführt, bei dem viele Bugs und Edge-Fälle aufgetreten sind. Und dann gingen wir voran und schrieben für kritischere Pfade neue Tests.

Ausrollen, live gehen und aufräumen

Nachdem wir die Qualitätssicherung bestanden hatten, haben wir unser Feature-Flag gesetzt und das System in den Ruhezustand versetzt. Nachdem wir sicher waren, dass es stabil ist, haben wir die Feature-Flags und alten Codepfade aus der Codebasis entfernt. Leider war dies schwieriger als erwartet, da ein Großteil der Testsuite neu geschrieben werden musste, hauptsächlich Fabriken, die sich implizit auf das Content-Modell stützten. Im Nachhinein hätten wir beim Refactoring zwei Testreihen schreiben können, eine für den aktuellen Code und eine für den Code hinter einem Feature-Flag.

Als letzten Schritt, der noch bevorsteht, sollten wir Daten sichern und unsere nicht verwendeten Tabellen löschen.

Und das, Freunde, ist eine Möglichkeit, die weitläufige Single Table Inheritance in Ihrem Rails-Monolithen loszuwerden. Vielleicht hilft Ihnen auch diese Fallstudie.

Haben Sie andere Möglichkeiten, STI zu entfernen oder umzugestalten? Wir sind neugierig zu wissen. Lass es uns in den Kommentaren wissen.

Außerdem stellen wir ein! Trete unserem Team bei. Wir sind cool, ich verspreche es.

Ressourcen und zusätzliche Lektüre

  • Rails Guides-Vererbung
  • Wie und wann man Single Table Inheritance in Rails von Eugene Wang verwendet (Flatiron Grad!)
  • Refactoring unserer Rails-App aus Single-Table-Vererbung
  • Single Table Inheritance vs. Polymorphic Associations in Rails
  • Vererbung einzelner Tabellen mit Rails 5.02

Um mehr über die Flatiron School zu erfahren, besuchen Sie die Website, folgen Sie uns auf Facebook und Twitter und besuchen Sie uns bei bevorstehenden Veranstaltungen in Ihrer Nähe.

Die Flatiron School ist ein stolzes Mitglied der WeWork-Familie. Schauen Sie sich unsere Schwester-Technologie-Blogs WeWork Technology and Making Meetup an.