Cum să eliminați moștenirea tabelului unic de pe șinele dvs. Monolit

Moștenirea este ușoară - până când va trebui să faceți față datoriilor tehnice și impozitelor.

Când principala bază de cod a lui Learn a apărut în urmă cu cinci ani, Moștenirea unei tabele unice (STI) a fost destul de populară. Echipa Flatiron Labs la acea vreme a continuat să o folosească - folosind-o pentru toate, de la evaluări și curriculum la evenimente de alimentare și conținut pentru activități în cadrul sistemului nostru de management de învățare în creștere. Și a fost minunat - s-a terminat treaba. Aceasta le-a permis instructorilor să ofere curriculum, să urmărească progresul studenților și să creeze o experiență de implicare a utilizatorului.

Dar, după cum au subliniat multe postări de pe blog (acesta, acesta, și acesta, de exemplu), STI nu se extinde super bine, mai ales că datele cresc și subclasele noi încep să varieze foarte mult de la superclasele lor și unele de altele. După cum ați putut ghici, la fel s-a întâmplat și în baza noastră de coduri! Școala noastră s-a extins și am susținut tot mai multe caracteristici și tipuri de lecții. De-a lungul timpului, modelele au început să umfle și să mute și să nu mai reflecte abstractizarea potrivită pentru domeniu.

Am locuit în acel spațiu o perioadă, dând codului respectiv o dană largă și o facem patch-uri doar atunci când este necesar. Și atunci a venit vremea refactorului.

În ultimele câteva luni, m-am angajat într-o misiune de a elimina o instanță deosebit de glandară a STI, una care a implicat un model numit oarecum ambiguu. La fel de ușor de configurat inițial STI, este de fapt destul de dificil de eliminat.

Așadar, în această postare, voi acoperi un pic despre STI, voi oferi un anumit context despre domeniul nostru, voi prezenta un sfat de activitate și voi discuta strategiile pe care le-am folosit pentru a implementa în mod sigur modificările în timp ce minimizez suprafața pentru daune grave în timp ce am eliminat miezul. din aplicația noastră.

Despre moștenirea tabelului unic (STI)

Pe scurt, Moștenirea unei tabele unice în șine vă permite să stocați mai multe tipuri de clase în aceeași tabelă. În Active Record, numele clasei este stocat ca tipul din tabel. De exemplu, este posibil să aveți un Lab, Readme și Project toate în direct în tabelul conținutului:

clasa Lab 

În acest exemplu, laboratoarele, readmesurile și proiectele sunt toate tipurile de conținut care ar putea fi asociate cu o lecție.

Schema tabelului nostru de conținut arăta cam așa, astfel încât puteți vedea că tipul este doar stocat în tabel.

create_table "content", force:: cascade do | t |
  t.integer „curriculum_id”,
  t.string „tip”,
  t.text "markdown_format",
  t.string "titlu",
  t.integer "track_id",
  t.integer "github_repository_id"
Sfârșit

Identificarea domeniului de muncă

Conținutul s-a extins în toată aplicația, uneori confuz. De exemplu, aceasta a descris relațiile din modelul Lecției.

lecția de clasă  {ordine (ordinal:: asc)}
  are_one: conținut, cheie străină:: curriculum_id
  has_many: readmes, Foreign_key:: curriculum_id
  are_one: laborator, cheie străină:: curriculum_id
  has_one: readme, străin_key:: curriculum_id
  are_many: assignat_repos, prin:: continut
Sfârșit

Confuz? La fel și eu. Și acesta a fost doar un model din multe pe care a trebuit să le schimb.

Așa că, alături de coechipierii mei strălucitori și talentați (Kate Travers, Steven Nunez și Spencer Rogers), am creat un design mai bun care să ajute la reducerea confuziei și să faciliteze extinderea acestui sistem.

Un nou design

Conceptul pe care Content a încercat să îl reprezinte era un intermediar între un GithubRepository și o lecție.

Fiecare bucată de conținut de lecție „canonică” este legată de un depozit de pe GitHub. Când lecțiile sunt publicate sau „dislocate” studenților, facem o copie a depozitului respectiv GitHub și le oferim studenților un link către acesta. Legătura dintre o lecție și versiunea implementată se numește AssignedRepo.

Deci există depozite GitHub la ambele capete ale lecțiilor: versiunea canonică și versiunea dislocată.

conținut de clasă 
clasa AssignedRepo 

La un moment dat, lecțiile au putut avea mai multe bucăți de conținut, dar în lumea noastră actuală, acest lucru nu mai este cazul. În schimb, există diferite tipuri de lecții, care se pot introspecta asupra lor, analizând fișierele incluse în depozitele asociate.

Deci, ceea ce am decis să facem a fost să înlocuim Content cu un nou concept numit CanonicalMaterial și să oferim AsignatRepo o referință directă la lecția sa asociată în loc să parcurgem Conținut.

Diagrama sistemului vechi până la nou, unde liniile punctate roșii indică căi marcate pentru depreciere

Dacă sună confuz și pare multă muncă, este pentru că așa este. Totuși, este important să înlocuim un model într-o bază de cod destul de mare și am ajuns să schimbăm undeva pe 6000 de linii de cod.

Totuși, este important să înlocuim un model într-o bază de cod destul de mare și am ajuns să schimbăm undeva pe 6000 de linii de cod.

Strategii pentru refactorizarea și înlocuirea STI

Noul model

Mai întâi, am creat un nou tabel numit canonical_materials și am creat noul model și asociații.

clasa CanonicalMaterial 

De asemenea, am adăugat o cheie străină de canonical_material_id în tabelul curriculum-urilor, astfel încât o lecție să poată menține o referință la aceasta.

La tabelul assignat_repos, am adăugat o coloană lecție_id.

Scrieri duale

După ce noile tabele și coloane erau pe loc, am început să scriem în tabelele vechi și cele noi simultan, astfel încât să nu mai fie nevoie să executăm o sarcină de completare mai mult de o dată. De fiecare dată când a încercat ceva să creeze sau să actualizeze un rând de conținut, am creat și actualiza un material canonical_.

De exemplu:

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

lecție.canonical_material = repo.canonical_material
lesson.save

Acest lucru ne-a permis să punem bazele pentru eliminarea în final a conținutului.

rambleiere

Următorul pas în proces a fost reumplerea datelor. Am scris sarcini rake pentru a ne popula tabelele și pentru a ne asigura că există un CanonicalMaterial pentru fiecare GithubRepository și că fiecare lecție a avut un CanonicalMaterial. Și apoi am executat sarcinile pe serverul nostru de producție.

În această rundă de refactorizare, am preferat să avem date valide, astfel încât să putem face o pauză curată cu modul moștenit de a face lucrurile. O altă opțiune viabilă este totuși să scrieți cod care acceptă încă modele mai vechi. În experiența noastră, a fost mai confuz și mai costisitor să mențin un cod care să susțină gândirea moștenită decât a fost să reîncărcați și să vă asigurați că datele sunt valabile.

În experiența noastră, a fost mai confuz și mai costisitor să mențin un cod care să susțină gândirea moștenită decât a fost să reîncărcați și să vă asigurați că datele sunt valabile.

Înlocuire

Și atunci a început partea distractivă. Pentru a înlocui cât mai în siguranță înlocuirea, am folosit steaguri caracteristice pentru a livra coduri întunecate în PR-uri mai mici, ceea ce ne-a permis să creăm o buclă de feedback mai rapidă și să știm mai repede dacă lucrurile s-au rupt. Pentru aceasta, am folosit gema de derulare, pe care o folosim și pentru dezvoltarea standard de funcții.

Ce să caute

Una dintre cele mai dificile părți ale înlocuirii a fost numărul mare de lucruri de căutat. Cuvântul „conținut” este, din păcate, super generic, așa că a fost imposibil să fac o căutare simplă și globală și să o înlocuiesc, așa că am avut tendința de a face o căutare mai detaliată încercând să iau în calcul variantele.

Când eliminați STI, acestea sunt lucrurile pe care ar trebui să le căutați:

  • Formele singular și plural ale modelului, incluzând toate subclasele, metodele, metodele de utilitate, asociațiile și interogările.
  • Interogări SQL hard-codate
  • controlerele
  • Serializers
  • Vizualizări

De exemplu, pentru conținut, asta înseamnă căutarea:

  • : conținut - pentru asociații și interogări
  • : conținut - pentru asociații și interogări
  • .joins (: conținut) - pentru interogări de unire, care ar trebui să fie surprinse de căutarea anterioară
  • .include (: conținut) - pentru încărcarea dornică a asociațiilor de ordinul doi, care ar trebui să fie surprinse și de căutarea anterioară
  • continut: - pentru interogari imbricate
  • conținut: - din nou, mai multe interogări cuibărite
  • content_id - pentru interogări direct de ID
  • .content - apeluri la metodă
  • .contents - apeluri la metoda de colectare
  • .build_content - metodă de utilitate adăugată de asocierea has_one și belong_to
  • .create_content - metodă de utilitate adăugată de asocierea has_one și aparține_to
  • .content_ids - metodă de utilitate adăugată de asociația has_many
  • Conținut - numele clasei în sine
  • conținut - șirul simplu pentru orice referințe hardcodate sau interogări SQL

Cred că este o listă destul de cuprinzătoare pentru conținut. Și atunci am făcut același lucru pentru laborator, readme și proiect. Puteți vedea că, deoarece Rails este atât de flexibil și adaugă multe metode de utilitate, este greu de găsit toate locurile în care un model ajunge să fie utilizat.

Cum să înlocuiți efectiv implementarea după ce ați găsit toți apelanții

După ce ați localizat de fapt toate site-urile de apel ale modelului pe care încercați să îl înlocuiți sau să îl eliminați, veți rescrie lucrurile. În general, procesul pe care l-am urmat a fost

  1. Înlocuiți comportamentul metodei în definiție sau modificați metoda pe site-ul apelului
  2. Scrieți metode noi și chemați-le în spatele unui steag de funcții pe site-ul apelului
  3. Rupeți dependențele de asocieri cu metode
  4. Creșteți erori în spatele unui indicator de caracteristică dacă nu sunteți sigur cu privire la o metodă
  5. Schimbă obiecte care au aceeași interfață

Iată exemple din fiecare strategie.

1a. Înlocuiți comportamentul sau interogarea metodei

Unele dintre înlocuitori sunt destul de simple. Puneți steagul funcțional pentru a spune „apelați acest cod în locul acestui alt cod atunci când acest steag este activat.”

Deci, în loc de interogare pe bază de conținut, aici vom interoga pe baza canonical_material.

1b. Modificați metoda de pe site-ul apelurilor

Uneori, este mai ușor să înlocuiți metoda de pe site-ul apelurilor pentru a standardiza metodele apelate. (Ar trebui să rulați suita de teste și / sau să scrieți teste atunci când faceți acest lucru.) În acest fel, puteți deschide calea către refactorizarea ulterioară.

Acest exemplu demonstrează modul de rupere a dependenței de coloana canonical_id, care în curând nu va mai exista. Observați că am înlocuit metoda de pe site-ul apelurilor, fără să punem asta în spatele unui steag de caracteristici. Făcând această refactorizare, am observat că am scos canonical_id în mai mult de un loc, așa că am înfășurat logica pentru a face asta într-o altă metodă pe care am putea să o legăm pe alte interogări. Metoda de pe site-ul apelurilor a fost schimbată, dar comportamentul nu s-a schimbat până când nu a fost activat steagul caracteristicilor.

2. Scrieți metode noi și chemați-le în spatele unui steag de funcții pe site-ul apelului

Această strategie este legată de înlocuirea metodei, doar în aceasta, scriem o nouă metodă și o numim în spatele unui steag de funcții pe site-ul apelului. Era util mai ales pentru o metodă care era numită doar într-un singur loc. De asemenea, ne-a permis să oferim metodei o semnătură mai bună - întotdeauna utilă.

3. Rupeți dependențele de asocieri cu metode

În acest exemplu următor, o pistă are laboratoare_many. Deoarece știm că asociația has_many adaugă metode de utilitate, am înlocuit-o pe cea mai des numită și am eliminat linia has_many: labs. Această metodă se conformează aceleiași interfețe, astfel încât orice lucru care a apelat metoda înainte de activarea funcției va continua să funcționeze.

4. Creșteți erori în spatele unui indicator de caracteristică dacă nu sunteți sigur despre o metodă

De câteva ori nu eram siguri dacă am ratat un site de apeluri. Deci, în loc să eliminăm la început metodele grele, am ridicat intenționat erori, astfel încât să le putem prinde în faza de testare manuală. Aceasta ne-a oferit un mod mai bun de a urmări unde a fost apelată o metodă.

5. Schimbă obiecte care au aceeași interfață

Pentru că am vrut să scăpăm de asociația de laborator, am rescris implementarea laboratorului? metodă. În loc să verificăm prezența unei fișe de laborator, am schimbat în canonical_material, am delegat apelul și am făcut ca obiectul să răspundă la aceeași metodă.

Acestea au fost cele mai utile strategii pentru ruperea dependențelor și schimbul de obiecte noi în întregul monolit al Rails. După ce am analizat sutele de definiții și site-uri de apeluri, le-am înlocuit sau le-am rescris unul câte unul. Este un proces obositor pe care nu mi-l doresc nimănui, dar în cele din urmă a fost extrem de util pentru a ne face mai lizibilă baza de coduri și pentru a elimina codul vechi care stătea în preajmă pentru a nu face nimic. A fost nevoie de câteva săptămâni frustrante și de atragere a părului pentru a ajunge la sfârșit, dar odată ce am înlocuit majoritatea referințelor, am început să facem teste manuale.

Testare și testare manuală

Deoarece modificările au afectat caracteristicile de pe întreaga bază de cod, dintre care unele nu au fost testate, a fost greu de făcut QA cu certitudine, dar am făcut tot posibilul. Am efectuat testări manuale pe serverul nostru QA, care a surprins o mulțime de erori și cazuri de margine. Și apoi am mers înainte și pentru căi mai critice, am scris teste noi.

Derulați, mergeți în direct și curățați

După trecerea QA, am activat indicatorul nostru de caracteristici și lăsăm sistemul să se stabilească. După ce ne-am asigurat că este stabil, am eliminat din baza de cod steagurile de caracteristici și căile de cod vechi. Din păcate, acest lucru a fost mai greu decât se aștepta, deoarece a implicat rescrierea multor suite de teste, în mare parte fabrici care s-au bazat implicit pe modelul Content. În retrospectivă, ceea ce am fi putut face a fost să scriem două seturi de teste în timp ce refactoriam, unul pentru codul curent și altul pentru codul din spatele unui steag de caracteristici.

Ca un pas final, care urmează să fie, ar trebui să facem o copie de rezervă a datelor și să aruncăm tabelele nefolosite.

Și asta, prieteni, este o modalitate de a scăpa de moștenirea unică a mesei din monolitul Rails. Poate că acest studiu de caz te va ajuta și pe tine.

Aveți alte modalități de a elimina STI sau de a refactoriza? Suntem curioși să știm. Spuneți-ne în comentarii.

De asemenea, angajăm! Alătură-te echipei noastre. Suntem misto, promit.

Resurse și lectură suplimentară

  • Șinele Ghiduri Moștenire
  • Cum și când se folosește moștenirea de masă unică în balustrade de Eugene Wang (Flatiron Grad!)
  • Refactorizarea aplicației noastre cu șinele din moștenire cu o singură masă
  • Moștenire în tabel unic vs. Asociații polimorfe în șine
  • Moștenire în tabel unic folosind șinele 5.02

Pentru a afla mai multe despre Flatiron School, accesați site-ul, urmați-ne pe Facebook și Twitter și vizitați-ne la evenimentele viitoare lângă dvs.

Flatiron School este un membru mândru al familiei WeWork. Vezi blogurile noastre de tehnologie sora WeWork Technology and Making Meetup.