Jak odstranit dědičnost jedné tabulky z monolitů kolejnic

Dědictví je snadné - dokud nebudete muset řešit technický dluh a daně.

Když před pěti lety vznikla hlavní kódová základna společnosti Learn, byla dědičnost Single Table Inheritance (STI) docela populární. Tým Flatiron Labs v té době do toho vstoupil všude - využíval ho pro vše od hodnocení a učebních osnov až po události krmení aktivit a obsah v našem rostoucím systému řízení učení. A to bylo skvělé - práce byla dokončena. To instruktorům umožnilo poskytovat kurikulum, sledovat pokrok studentů a vytvářet poutavé uživatelské prostředí.

Jak ale zdůraznilo mnoho příspěvků v blogu (tento, tento a například tento), STI nedochází k superškálování měřítka, zvláště když rostou data a nové podtřídy se od svých nadtried a od sebe navzájem velmi liší. Jak jste možná uhodli, totéž se stalo v naší kódové základně! Naše škola se rozšířila a podporovali jsme stále více funkcí a typů lekcí. Postupem času začaly modely nadýmat a mutovat a již neodrážejí správnou abstrakci pro doménu.

Chvíli jsme žili v tomto prostoru, dávali jsme tomuto kódu široké lůžko a opravovali ho jen v případě potřeby. A pak přišel čas na refaktor.

Během několika posledních měsíců jsem se vydal na misi na odstranění jedné zvlášť drsné instance STI, která zahrnovala poněkud nejasně pojmenovaný model obsahu. Jak snadné je nastavení STI na začátku, je ve skutečnosti docela obtížné jej odstranit.

Takže v tomto příspěvku se budu trochu věnovat STI, poskytuji nějaký kontext o naší doméně, nastíním rozsah práce a diskutuji o strategiích, které jsem použil k bezpečnému nasazení změn, zatímco minimalizuji povrchovou plochu pro vážné poškození, zatímco jsem vykuchal jádro naší aplikace.

O dědičnosti jedné tabulky (STI)

Stručně řečeno, dědičnost jedné tabulky v kolejnicích umožňuje uložit do stejné tabulky více typů tříd. V aktivním záznamu je název třídy uložen jako typ v tabulce. Například můžete mít Lab, Readme a Project vše živé v tabulce obsahu:

třída Lab 

V tomto příkladu jsou laboratoře, readmy a projekty všechny typy obsahu, které by mohly být spojeny s lekcí.

Schéma naší tabulky obsahu vypadalo trochu takto, takže můžete vidět, že typ je právě uložen v tabulce.

create_table "content", force:: kaskáda 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"
konec

Identifikace rozsahu práce

Obsah se v celé aplikaci šíří, někdy matoucí. Toto například popisuje vztahy v modelu lekce.

lekce  {order (ordinal:: asc)}
  has_one: content, cizí_key:: curriculum_id
  has_many: readmes, cizí_key:: curriculum_id
  has_one: lab, cizí_key:: curriculum_id
  has_one: readme, cizí_key:: curriculum_id
  has_many: přiřazené_pozice, prostřednictvím:: content
konec

Zmatený? Stejně jako já. A to byl jen jeden z mnoha modelů, které jsem musel změnit.

Takže se svými skvělými a talentovanými spoluhráči (Kate Travers, Steven Nunez a Spencer Rogers) jsem vymyslel lepší design, který pomůže omezit zmatek a usnadnit rozšiřování tohoto systému.

Nový design

Koncept, který se Obsah snažil reprezentovat, byl prostředníkem mezi GithubRepository a lekcí.

Každá část „kanonického“ obsahu lekce je spojena s úložištěm na GitHubu. Když jsou lekce publikovány nebo „nasazeny“ studentům, vytvoříme kopii tohoto úložiště GitHub a poskytneme studentům odkaz na něj. Spojení mezi lekcí a implementovanou verzí se nazývá AssignedRepo.

Existují tedy úložiště GitHub na obou koncích výuky: kanonická verze a nasazená verze.

třída Obsah 
class AssignedRepo 

V jednu chvíli byly hodiny schopny mít více obsahů, ale v našem současném světě tomu tak již není. Místo toho existují různé druhy lekcí, které se na sebe mohou podívat pomocí nahlédnutí do souborů zahrnutých v jejich přidružených úložištích.

Rozhodli jsme se tedy nahradit obsah novým konceptem nazvaným CanonicalMaterial a dát AssignedRepo přímý odkaz na související lekci namísto procházení obsahu.

Schéma systému Old to New, kde červené tečkované čáry označují cesty označené jako deprecation

Pokud to zní matoucí a jako hodně práce, je to proto, že je. Klíčové s sebou však je, že jsme museli vyměnit model v docela velké kódové základně a nakonec jsme se někde změnili v oblasti 6000 řádků kódu.

Klíčové s sebou však je, že jsme museli vyměnit model v docela velké kódové základně a nakonec jsme se někde změnili v oblasti 6000 řádků kódu.

Strategie pro refaktoring a nahrazení STI

Nový model

Nejprve jsme vytvořili novou tabulku nazvanou canonical_materials a vytvořili nový model a asociace.

třída CanonicalMaterial 

Do tabulky učebních osnov jsme také přidali cizí klíč canonical_material_id, aby lekce mohla udržovat odkaz na ni.

Do tabulky přiřazených_repozic jsme přidali sloupec lesson_id.

Duální zápisy

Po zavedení nových tabulek a sloupců jsme začali psát na staré i nové tabulky současně, takže bychom nemuseli spouštět více než jednou. Kdykoli se něco pokusilo vytvořit nebo aktualizovat řádek obsahu, vytvořili bychom také nebo aktualizovali kanonický_materiál.

Například:

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

lesson.canonical_material = repo.canonical_material
lesson.save

To nám umožnilo položit základy pro konečné odstranění obsahu.

Zasypávání

Dalším krokem v procesu bylo vyplnění dat. Napsali jsme rake úkoly, abychom naplnili naše tabulky a zajistili, aby pro každý GithubRepository existoval CanonicalMaterial a aby každá Lekce měla CanonicalMaterial. A pak jsme úlohy spustili na našem produkčním serveru.

V tomto kole refactoringu jsme upřednostňovali platná data, abychom mohli udělat čistou přestávku se starým způsobem dělat věci. Další možnou možností je však napsat kód, který stále podporuje starší modely. Podle našich zkušeností bylo udržování kódu, který podporuje staré myšlení, mnohem matoucí a nákladnější, než bylo doplňování a ověřování platnosti dat.

Podle našich zkušeností bylo udržování kódu, který podporuje staré myšlení, mnohem matoucí a nákladnější, než bylo doplňování a ověřování platnosti dat.

Výměna, nahrazení

A pak zábavná část začala. Aby byla náhrada co nejbezpečnější, použili jsme vlaječky funkcí k odeslání tmavého kódu do menších PR, což nám umožnilo vytvořit rychlejší smyčku zpětné vazby a vědět dříve, jestli se věci rozbijí. K tomu jsme použili drahokam rozvinutí, který také používáme pro vývoj standardních funkcí.

Co hledat

Jednou z nejtěžších součástí výměny bylo pouhé množství věcí, které je třeba hledat. Slovo „obsah“ je, bohužel, velmi obecné, takže nebylo možné provést jednoduché globální hledání a nahrazení, takže jsem měl tendenci provádět více zaměřené vyhledávání a snažil se vysvětlit tyto variace.

Při odstraňování STI byste měli hledat tyto věci:

  • Singulární a množné formy modelu, včetně všech jeho podtříd, metod, užitkových metod, asociací a dotazů.
  • Pevné dotazy SQL
  • Ovladače
  • Serializátory
  • Zobrazení

Například pro obsah, který znamenal hledání:

  • : content - pro asociace a dotazy
  • : content - pro asociace a dotazy
  • .joins (: content) - pro dotazy spojení, které by měly být zachyceny předchozím vyhledáváním
  • .includes (: content) - pro dychtivé načtení asociací druhého řádu, které by také mělo být zachyceno předchozím vyhledáváním
  • content: - pro vnořené dotazy
  • obsah: - opět více vnořených dotazů
  • content_id - pro dotazy přímo podle ID
  • .content - volání metod
  • .contents - volání metody sběru
  • .build_content - nástrojová metoda přidaná sdružením has_one a patří_to
  • .create_content - nástrojová metoda přidaná sdružením has_one a patří_to
  • .content_ids - nástrojová metoda přidaná asociací has_many
  • Obsah - samotný název třídy
  • content - prostý řetězec pro jakékoli pevné kódy nebo dotazy SQL

Věřím, že je to docela obsáhlý seznam obsahu. A pak jsem udělal totéž pro laboratoř, readme a projekt. Můžete vidět, že protože Rails je tak flexibilní a přidává mnoho užitečných metod, je těžké najít všechna místa, kde se model nakonec používá.

Jak skutečně nahradit implementaci poté, co jste našli všechny volající

Jakmile jste skutečně našli všechny stránky pro volání modelu, který se pokoušíte nahradit nebo odebrat, můžete věci přepsat. Obecně platí, že jsme postupovali

  1. Nahradit chování metody v definici nebo změnit metodu na stránce volání
  2. Napište nové metody a zavolejte je za příznak funkce na stránce volání
  3. Zrušte závislosti na asociacích s metodami
  4. Pokud si nejste jisti metodou, zvyšte chyby za příznakem funkce
  5. Zaměňte objekty, které mají stejné rozhraní

Zde jsou příklady každé strategie.

1a. Nahraďte chování metody nebo dotaz

Některé z náhrad jsou docela jednoduché. Když je tento příznak zapnutý, přidáte příznak funkce na místo, abyste řekli „zavolejte tento kód místo tohoto jiného kódu.“

Takže namísto dotazování na základě obsahu jsme zde dotazovali na základě canonical_material.

1b. Změňte metodu v místě volání

Někdy je jednodušší nahradit metodu na stránce volání, aby se standardizované metody standardizovaly. (Když to uděláte, měli byste spustit testovací sadu a / nebo napsat testy.) Pokud tak učiníte, můžete otevřít cestu k dalšímu refaktoringu.

Tento příklad ukazuje, jak přerušit závislost na sloupci canonical_id, který již brzy nebude existovat. Všimněte si, že jsme tuto metodu na místě volání nahradili, aniž bychom ji museli umístit za příznak funkce. Při provádění tohoto refaktoringu jsme si všimli, že jsme kanonical_id vytrhli na více než jednom místě, a proto jsme logiku zabalili, abychom to provedli jiným způsobem, který bychom mohli zřetězit na jiné dotazy. Metoda na stránce volání byla změněna, ale chování se nezměnilo, dokud nebyl zapnut příznak funkce.

2. Napište nové metody a zavolejte je za příznak funkce v místě volání

Tato strategie souvisí s náhradou metody, pouze v této metodě píšeme novou metodu a nazýváme ji za příznakem funkce v místě volání. To bylo zvláště užitečné pro metodu, která byla volána pouze na jednom místě. Také nám to umožnilo dát této metodě lepší podpis - vždy užitečné.

3. Přerušte závislosti na asociacích s metodami

V tomto příštím příkladu má stopa laboratoře. Protože víme, že přidružení has_many přidává pomocné metody, nahradili jsme ten nejčastěji nazývaný a odstraněný řádek has_many: labs. Tato metoda odpovídá stejnému rozhraní, takže cokoli, kdo volá metodu před zapnutím funkce, bude i nadále fungovat.

4. Pokud si nejste jisti metodou, zvyšte chyby za příznakem funkce

Někdy jsme si nebyli jisti, zda jsme zmeškali web pro volání. Namísto náročných metod odstraňování jsme nejprve záměrně vyvolali chyby, abychom je mohli zachytit během fáze manuálního testování. To nám poskytlo lepší způsob, jak zjistit, kde byla metoda volána.

5. Zaměňte objekty, které mají stejné rozhraní

Protože jsme se chtěli zbavit asociace laboratoře, přepsali jsme implementaci laboratoře? metoda. Místo toho, abychom zkontrolovali přítomnost laboratorního záznamu, vyměnili jsme si canonical_material, delegovali volání a nechali tento objekt reagovat na stejnou metodu.

To byly nejužitečnější strategie pro lámání závislostí a výměnu nových objektů v celém monolitickém Rails. Po kontrole stovek definic a stránek volání jsme je nahradili nebo přepsali jeden po druhém. Je to únavný proces, který bych nikomu nechtěl, ale nakonec to bylo nesmírně užitečné pro to, aby byla naše kódová základna lépe čitelná a pro odstranění starého kódu, který seděl a nedělal nic. Trvalo několik frustrujících týdnů a tahů za vlasy, než jsme se dostali na konec, ale jakmile jsme nahradili většinu referencí, začali jsme provádět ruční testování.

Testování a ruční testování

Protože změny ovlivnily funkce v celé kódové základně, z nichž některé nebyly testovány, bylo s jistotou obtížné zajistit QA, ale udělali jsme, co bylo v našich silách. Provedli jsme ruční testování na našem serveru QA, který zachytil spoustu chyb a okrajových případů. A pak jsme šli napřed a pro kritičtější cesty jsme napsali nové testy.

Rozbalte, začněte žít a vyčistěte

Po absolvování QA jsme otočili náš příznak funkce a nechali systém usadit se. Poté, co jsme si byli jisti, že je stabilní, jsme z kódové základny odstranili příznaky funkcí a staré cesty kódů. To, bohužel, bylo těžší, než se očekávalo, protože to znamenalo přepis hodně testovací sady, většinou továren, které se implicitně spoléhaly na model obsahu. Při zpětném pohledu jsme mohli udělat dvě sady testů, zatímco jsme refaktorovali, jeden pro aktuální kód a jeden pro kód za příznakem funkce.

Jako poslední krok, který ještě zbývá, bychom měli zálohovat data a zrušit naše nepoužité tabulky.

A to, přátelé, je jeden ze způsobů, jak se zbavit rozlehlé dědictví Single Table v monolitech Rails. Možná vám tato případová studie pomůže.

Máte jiné způsoby, jak odstranit STI nebo refaktoring? Jsme zvědaví vědět. Dejte nám vědět v komentářích.

Také najímáme! Připojte se k našemu týmu. Jsme v pohodě, slibuji.

Zdroje a další čtení

  • Dědičnost vodítek kolejnic
  • Jak a kdy použít dědičnost jedné tabulky v kolejích od Eugene Wanga (Flatiron Grad!)
  • Refaktoringování našich kolejnic z dědičnosti jednoho stolu
  • Dědičnost jedné tabulky vs. polymorfní asociace v kolejích
  • Dědičnost jedné tabulky pomocí kolejnic 5.02

Chcete-li se dozvědět více o Flatiron School, navštivte web, sledujte nás na Facebooku a Twitteru a navštivte nás na nadcházejících událostech ve vašem okolí.

Flatiron School je hrdým členem rodiny WeWork. Podívejte se na naše sesterské blogy WeWork Technology and Making Meetup.