Czego nauczyło mnie przeniesienie danych z jednej tabeli do drugiej?

By 25 May 2020 Praktyczna Java

Banalne zada­nia w IT chy­ba nie ist­nieją. Migrac­ja do nowej tabeli nie powin­na być niczym trud­nym — niem­niej, w żyjącej na pro­dukcji aplikacji, w połącze­niu z dodaniem nowego zestawu funkcji stała się całkiem ciekawym wyzwaniem. O tym chcę Ci dzisi­aj opowiedzieć w moim poście.

Po pierwsze: Po co ta migracja?

Odpowiedź na to pytanie była dość kluc­zowa dla pro­ce­su, wszak musi­ałam wytłu­maczyć biz­ne­sowi, dlaczego coś, co wydawało im się bard­zo prostym zadaniem, powin­niśmy poprzedz­ić taką małą rewolucją. I myślę, że każdy więk­szy refak­tor w jakim moczysz palce powin­no poprzedz­ić takie rozważanie: Po co? Jaki będzie tego koszt? Co zys­ka pro­dukt? (bardziej szczegółowy zestaw pytań zna­jdziesz we wpisie o  wyborze bib­liote­ki, co jest podob­nej skali przedsięwzięciem). 

Aby odpowiedzieć na te pyta­nia odbyłam kil­ka spotkań z zespołem BE, a potem, gdy miałam już konkretne argu­men­ty, Prod­uct Man­agerem. Na tech­nicznych spotka­ni­ach roz­maw­ial­iśmy głównie o kon­sek­wenc­jach i kosz­tach takiej decyzji vs jej alter­naty­wy, a także szukaliśmy ogól­nej architek­tu­ry nowego rozwiąza­nia. Nie zgłębi­a­jąc się w super detale imple­men­tacji, na początku pra­cy nad pro­duk­tem, dla którego piszę kod, została pod­ję­ta decyz­ja o wyko­rzys­ta­niu wewnętrznej platformy/ ser­wisu, który służył podob­ne­mu celowi. Z powodu jego architek­tu­ry, która jest w dużej mierze mono­li­ty­cz­na, współdzielona jest też baza danych, w tym także niek­tóre ist­niejące wcześniej tabele. Ta decyz­ja przyniosła nasze­mu zespołowi wiele korzyś­ci —  dużo dostal­iśmy ‘za dar­mo’, dzię­ki czemu w bard­zo krótkim cza­sie mogliśmy wypuś­cić gotowy pro­dukt. Niem­niej były też pewne minusy tej decyzji, z czego jeden kluc­zowy — jeden z mod­eli, który postanow­iliśmy uży­wać, był w innych sys­temach wyko­rzysty­wany w zupełnie inny sposób. Nasze ‘obe­jś­cie’ tego sys­te­mu dzi­ałało sobie dobre 1.5 roku, po czym inne zespoły zaczęły — słusznie — dość szorstko reagować na kole­jne dodawane tam kolum­ny. Nie dostal­iśmy czer­wonego światła, niem­niej zaczęliśmy dostrze­gać, że nasz pot­worek powoli chce uciec z akwar­i­um ;) wtedy właśnie pod­jęliśmy decyzję o prze­niesie­niu go do nowej struk­tu­ry, a główną korzyś­cią była nieza­leżność, którą my zyskaliśmy, a pozostałe zespoły odzyskały. Gdy dostałam zielone światło zabrałam się za pracę (w moim zes­pole takie zadanie będzie real­i­zował 1 pro­gramista, być może z małym wspar­ciem na poszczegól­nych taskach od innych członków zespołu, niem­niej cały plan dzi­ała­nia, propozy­c­ja architek­tu­ry i pon­ad 90% real­iza­cji będą autorstwa tej jed­nej osoby).

 

Po drugie, zaplanuj (i spisz!)

Zaplanowanie wszys­tkiego również było ważne. Miałam przed sobą  1.5–2 lata żywego pro­duk­tu. Z kilko­ma rzecza­mi napisany­mi trochę na agrafkę,z co najm­niej kilko­ma inny­mi ser­wisa­mi, w których również trze­ba było wprowadz­ić zmi­any. Na szczęś­cie jest Open­Grok, który poz­wolił mi zmapować wszys­tkie miejs­ca wyma­ga­jące modyfikacji.

Poniżej zna­jdziesz mój plan dzi­ała­nia — w ramach ćwiczenia zas­tanów się, jakich kroków wyma­gała by taka oper­ac­ja w twoim projekcie?

Z grub­sza mój plan wyglą­dał następu­ją­co: 

  • Dodaj nową tabelę:
    • Stwórz nowy model(obiekt) + tabelę
    • Zak­tu­al­izuj obec­ne mod­ele, które uży­wa­ją tego obiek­tu (i ich tabele)
    • Migrac­ja danych (stara tabela do nowej tabeli + obiek­ty współza­leżne uży­wa­jące id z nowej tabeli)
  • Dodaj logikę związaną z pisaniem do nowej tabeli:
    • Nasz Sys­tem:
      • CRUD
      • Uży­wanie id z nowej tabeli przez inne obiek­ty w ich CRUD,
      • Dodaj logikę bufera (z poprzed­nią imple­men­tacją ta funkc­ja nie była łat­wa w implementacji), 
      • Dodaj wysyłanie even­tów do innych sys­temów, gdy dane ule­ga­ją zmianie,
      • Dodaj możli­wość nadawa­nia kole­jnoś­ci (z poprzed­nią imple­men­tacją ta funkc­ja nie była łat­wa w implementacji),
      • Uak­tu­al­nij sys­tem impor­tu, by uży­wał nowego modelu
    • Dodaj wspar­cie dla uży­wa­nia nowego mod­elu przez:
      • Sys­tem do indeksowania,
      • Sys­tem do wykry­wa­nia konfliktów,
      • Wyszuki­wanie,
      • Sys­tem do rekomendacji,
  • Dodaj logikę do czy­ta­nia z nowej tabeli:
    • Dodaj w opar­ciu o gate logikę do uży­wa­nia nowej tabeli przez API,
    • Uży­waj danych z nowej tabeli przez sys­tem do renderowania.
  • Syn­chro­niza­c­ja:
    • Gdy updatu­jesz stare dane, syn­chro­nizuj je z nowymi, 
    • Gdy updatu­jesz obiek­ty, które wyko­rzys­tu­ją id migrowanego obiek­tu, uzu­peł­ni­aj stare i nowe id.
    • Dodaj joby, które będą sprawdzać syn­chro­niza­cję pomiędzy starą a nową tabelą. 
  • Sprzą­tanie:
    • Oznacz starą imple­men­tac­je jako @Deprecated,
    • Loguj uży­cia starego API,
    • Usuń kod, a także pomoc­niczą kolum­nę (lega­cyId) z nowej tabeli.

Zrozum obecną implementacje, tak by znaleźć najprostszy sposób na zmianę

Zakładam, że twój plan różni się od mojego. Ba, mogę Ci śmi­ało powiedzieć, że sama musi­ałam zmienić swo­je bard­zo wstęp­ne założe­nia. W ide­al­nym świecie, w pier­wszym etapie robiłabym zapisy­wanie do nowej i starej tabeli, a potem — gdy wiem, że wszys­tkie dane są syn­chro­ni­zowane — zro­biłabym to samo dla odczy­ty­wa­nia. I jak to wszys­tko by dzi­ałało, zaczęłabym dodawać nową logikę. Niem­niej, pod­jęłam inna decyzję, głównie z uwa­gi na wewnętrzne bib­liote­ki jakie uży­wamy — powodu­ją one że dużo skom­p­likowanej logi­ki po pros­tu dziedz­iczyliśmy (mamy takie główne klasy dla resource czy repos­i­to­ry, które również korzys­ta­ją z kon­cepcji life cycle han­dlerów — czyli klasy, których metody są uży­wane przed lub po jakim­iś oper­ac­ja­mi — np. oper­ac­ja­mi na bazie danych: beforeCre­ate, after­Delete itp.) a sposób ich imple­men­tacji nie pozwalał na proste dodanie nowego dao, by tą całą logikę utrzy­mać. Stąd pod­jęłam decyz­je o dziedz­icze­niu całej logi­ki (ale nieste­ty przez nowe klasy), i o włas­nej log­ice do syn­chro­niza­cji. Było to znacznie bardziej bez­pieczne, bo corowe funkc­je mogły być dziedz­ic­zone, niem­niej było w tym sporo dup­likacji kodu. Kon­sek­wenc­ja takiego pode­jś­cia była pod­mi­an­ka API na FE, czyli dodatkowa pra­ca, na którą musi­ałam mieć zgodę tego zespołu. Niem­niej takie rozwiązanie było najbez­pieczniejszym dla tego refaktoru.

Migracja danych to nie wszystko

Znacznie bardziej skom­p­likowana jest syn­chro­niza­c­ja danych. Tak naprawdę, przed puszcze­niem migracji musisz mieć gotową całą logikę syn­chro­niza­cji, tak by w trak­cie joba, ten sync był już możli­wy. Dzię­ki niemu znacznie łatwiej jest też stop­niowo wys­taw­iać użytkown­ików na nowe API. 

Tutaj chy­ba kluc­zowe jest odpowied­nie logowanie, żeby wiedzieć kiedy syn­chro­niza­c­ja zaw­iodła (np. log error z narzędziem typu Sen­try). Dodatkowo, uru­chomiłam jeszcze codzi­enne joby, które sprawdza­ły nowe i stare dane. W ten sposób łat­wo było wych­wycić każdy nieza­im­ple­men­towany jeszcze przy­padek, a także dawało pewność, że dane klien­tów są w porządku. 

 

Kole­jnym pomoc­nikiem było uży­cie bramek (ang. gates), których zasa­da dzi­ała­nia jest pros­ta, przed wyko­naniem oper­acji, sprawdza­sz czy dane id (np. użytkown­i­ka) jest dopuszc­zone dla danej funkcjon­al­noś­ci: jeśli tak — uży­wasz, jeśli nie — pomi­jasz. Może brz­mi to górnolot­nie, ale w prak­tyce to zwykły if w twoim kodzie:

 

if(isUngated(id,”NewFeature”){
   newLogic();
}else{
  old Logic();
}

 

W tym pro­ce­sie wyko­rzys­tałam dwie takie bram­ki, jed­ną dla pan­elu admi­na (zapis + odczyt), a drugą dla logi­ki, która pokazu­je te dane publicznie(tylko odczyt). 

W ten sposób mogłam najpierw udostęp­ni­ać tylko część funkcjon­al­noś­ci, a potem eta­pa­mi zwięk­szać ilość korzys­ta­ją­cych z niej użytkowników.

Nie przepisuj wszystkiego

Wiado­mo — pełny refak­tor kusi, w szczegól­noś­ci, gdy nadarza się taka okaz­ja, niem­niej rekomen­du­je tutaj dużą ostrożność. Głównym celem mojego pro­ce­su było prze­niesie­nie logi­ki tak, by nie dodać do niej żad­nego nowego buga. Nie znaczy to jed­nak, że masz tylko zro­bić kopi­uj-wklej starego kodu. Z każdym com­mitem możesz dodać małe zmi­any do kodu, które razem naprawdę robią różnicę. Gdy tworzysz taki PR dokład­nie je wytłu­macz, i podlinkuj starą logikę. Mówiąc o popraw­ia­n­iu, pamię­taj też o tes­tach — te kiep­sko napisane, mogą Ci tylko zaszkodz­ić, dlat­ego jeśli tylko możesz zwery­fikuj te, które są już w kodzie i dodaj braku­jące sce­nar­iusze czy aser­c­je (tutaj może przy­dać się jakieś narzędzie do code cov­er­age czy testów muta­cyjnych). 

 

Bądź ostrożny z dodatkowymi zmianami

Migrowanie czegoś innego w tym samym cza­sie nie jest najlep­szym pomysłem i może pokrzyżować two­je plany. Tego mój zespół doświad­czył na włas­nej skórze, bo razem z tymi zmi­ana­mi równole­gle migrowal­iśmy inne (w teorii były to znacznie mniejsze zmi­any). I tak — miało to sens robić to w tym samym cza­sie (tak by mój nowy obiekt, korzys­tał już z tej nowej logi­ki), ale dodało to znacznie więcej sce­nar­iuszy i przy­pad­ków brze­gowych do obsłuże­nia. Musi­ałam zsyn­chro­ni­zować swo­ją pracę z drugim pro­gramis­tom, i mieć abso­lut­ną pewność, że obo­je mamy taką samą wiz­ję na te zmiany. 

Następ­nym razem chy­ba wolałabym zmieni­ać to po sobie, a nie równolegle.

 

Bądź cierpliwy

To było naprawdę duże zadanie. Pojaw­iały się blok­ery, gdy np. musi­ałam zmienić coś w bib­liotece uży­wanej przez kilka­set innych wewnętrznych pro­jek­tów (więc musi­ałam poczekać, aż wszys­tkie będą na ‘nowej’ wer­sji moich obiek­tów, zan­im będę ich real­nie uży­wać w inter­akc­jach z inny­mi ser­wisa­mi). Cześć z moich zadań nie była intere­su­ją­ca, bo jak­by nie patrzeć sporo było tutaj kopi­owa­nia ze zrozu­mie­niem ;) No i w końcu — to po pros­tu trwało i cza­sem ciężko było zobaczyć postęp. Tutaj zbaw­ie­niem była moja lista zadań z której skreślałam te już wyko­nane. Dodatkowym aspek­tem było to, że ta migrac­ja nie przynosiła bezpośred­nio nowych korzyś­ci biz­ne­sowych, w rezulta­cie czego 2 razy musi­ałam pau­zować ten pro­ces, bo inne zada­nia miały więk­szy pri­o­ry­tet. Było to może ciekawe oder­wanie od tego naprawdę sporego zada­nia, ale z drugiej strony zmniejsza­ło moją pewność siebie co do jakoś­ci imple­men­tacji — taki powrót po tygod­niu pra­cy nad inny­mi rzecza­mi powodował, że sporo musi­ałam sobie przy­pom­nieć. Niem­niej, dzię­ki testom i sprawdza­niu syn­chro­niza­cji mogłam szy­bko wró­cić do dal­szej pracy.

 

Jak to wszystko poszło?

Dzisi­aj cała logi­ka jest już na pro­dukcji, dla każdego z użytkown­ików. Z uwa­gi na bard­zo wolne udostęp­ni­an­ie logi­ki dla użytkown­ików, a także joby, które sprawdza­ły syn­chro­niza­c­je danych udało nam się to osiągnąć bez żad­nego błę­du zgłos­zonego przez koń­cowych użytkown­ików, niem­niej było kilka­naś­cie bugów, które zła­pal­iśmy w trak­cie całego pro­ce­su. Wyda­je mi się, że najtrud­niejszym etapem całego pro­ce­su było planowanie i zapro­jek­towanie syn­chro­niza­cji danych. Ważne było dla mnie wspar­cie zespołu FE, a także BE, który poma­gał mi przy przenosze­niu małych por­cji logiki.

Mam nadzieję, że powyższy opis okazał się dla Ciebie ciekawy i pomoc­ny. Jeśli masz dodatkowe pyta­nia — pytaj śmi­ało, bo zda­ję sobie sprawę, że część tego pro­ce­su opisałam pobieżnie. Z chę­cią też zapoz­nam się z two­ją ostat­nią migracją — zostaw komen­tarz z opisem two­jego pode­jś­cia do takiego zadania!