Czytam sobie sporo o Railsach ostatnio. Na tyle mi się framework spodobał (i język Ruby też), że chwilowo nawet myślałem o całkowitej migracji Java -> Ruby & Rails. Ponieważ jednak firm RoR'owych u nas we Wrocławiu jak na lekarstwo (w sumie tylko jedna, pracowników serdecznie pozdrawiam :) ), to ostatecznie pomysł ten zarzuciłem. Nie zmienia to faktu, że czytanie książek i blogów o Railsach sporo mi dało. Inaczej teraz trochę patrze na programowanie, jest też kilka spraw, które w frameworkach Javowych bardzo mi od tego czasu brakuje.
Jednym z takich elementów są migracje (ang. migrations), potężne narządzie frameworka Ruby on Rails.
W Javie pisząc aplikacje wykorzystujące bazę danych przyzwyczailiśmy się już do frameworków tzw. mapujących. To znaczy takich, w których model biznesowy jest mocno zintegrowany z bazą danych. Boli nas trochę fakt, że gdy tworzymy aplikacje z wykorzystaniem jakiegoś narzędzia ORM'owego, musimy wyspecyfikować:
- pole w modelu
- getter
- setter
- mapowanie w kierunku a->b
- mapowanie w kierunku b->a
- definicje kolumny
Oczywiście nie można dramatyzować: model (a wiec odpowiednie fieldy, gettery, settery) pomoże nam stworzyć nasze ulubione IDE, a najnowsze frameworki (z wykorzystaniem adnotacji) umiejscawiają nam definicje bazodanowe w samym modelu. Wszystko robi się niby DRY (Don't Repeat Yourself), wszystko wydaje się być w jednym miejscu i wszelkie zmiany w projekcie można wprowadzać bezboleśnie. Zapominamy tutaj o jednej ważnej sprawie - danych w bazie danych.
Możemy wyobrazić sobie, że po miesiącu czy dwóch naszego developingu, powstaje aplikacja/prototyp, która zostaje wdrożona u klienta. Schemat bazy danych jest oczywiście generowany z naszego modelu na bazie produkcyjnej u klienta. Czysta, nowa, świeżutka baza wraz z aplikacją zostaje uruchomiona. My zabieramy się za kolejną iterację, a klient wdraża się w jej pierwszą wersję. W między czasie dodajemy nowe kolumny, zmieniamy nazwy tabel, łączymy pewne rzeczy w jedną całość - jesteśmy bardzo agile - nie boimy sie zmian. Przychodzimy do klienta, chcemy mu wygenerować nowa bazę a on nam mówi "Hola hola, ja tu już mam pewne dane, nie chce ich tracić". Po chwili namysłu dochodzimy do wniosku, że możemy wygenerować plik SQL, w którym umieścimy wszystkie zmiany w nowej bazie danych względem tej starej (znany wszystkim DDL, czyli alter table i takie tam) i tak gotowy skrypt uruchomimy po stronie bazy produkcyjnej.
Ok, rozwiązanie niby dobre. Spotkałem się z nim w praktyce, pliki takie nazywane były "patchami" (moim zdaniem nazwa dobra) i tworzone były dla każdej kolejnej iteracji przygotowywanej aplikacji. Trzymane wraz z kodem w repozytorium nie miały prawa zaginąć. Teraz tworząc pole String nickname w modelu User musiałem dodatkowo w patchu patch_078.sql umieszczać wpis "alter table add column nickname varchar". W momencie integracji wersji deweloperskiej z produkcyjną, po wprowadzeniu zmian w kodzie uruchamiany zostawał patch i schemat bazy był aktualny z modelem. Wszystko niby fajnie, ale właśnie tu zaczynają się schody. Po pierwsze przestaje mieć pojedyńczej informacji (w tym przypadku informacji o schemacie bazodanowym) w jednym miejscu - co szybko prowadzić może do pomyłek. Druga sprawa to brak powiązania tych zmian z kodem na repozytorium. Wprowadzę zmiany w kodzie, commitne, mnie trochę czasu, update'uje patch na repozytorium. Mijają cztery dni i okazuje się, że muszę cofnąć zmiany w moim modelu. Muszę PAMIĘTAĆ, że odpowiednie zmiany powinny być update'owane w patchu (rollback na repozytorium mi tego nie zapewni). Ból, ból, ból i zgrzytanie zębów.
O wiele ciekawiej rozwiązane jest to w Railsach. Patche (tutaj zwane migracjami) tworzone są za nas automatycznie. Taki patch zawiera nie tylko metodę updatującą schemat bazy, ale również wycofującą te zmiany. W ogólności każda migracja to klasa Ruby'ego implementująca dwie metody up oraz down. Metoda up wprowadza zmiany w schemacie bazy danych, metoda down je wycofuje. Przez co uzyskujemy patch działający w dwie strony. Zestaw operacji jakie możemy wykonać jest dowolny, możemy wstawiać dane, zmieniać strukturę schematu (z użyciem predefiniowanych metod podobnych do tych które znamy z DDL, czyli create/drop table, add/remove column i tak dalej). Każda taka migracja ma przypisany swój numer porządkowy, a sam schemat trzyma informacje o swojej wersji. Jeśli zatem przykładowo mamy schemat bazy danych w wersji 004, a w czasie developingu pojawiły się dwie nowe migracje 005 oraz 006, to wywolanie komendy rake migrate spowoduje, że framework wywoła w kolejności metody up, najpierw z migracji 005, a następnie z migracji 006,a na końcu zmieni wersję schematu na 006. Jeśli kiedykolwiek trzeba będzie sie cofnąć w zmianach schematu, na przykład do wersji 004, wykonane zostaną kolejno metody down w migracji 006, a poźniej w 005. Piękna sprawa!
Klasy migracji są tworzone dla modeli automatycznie, więc nie ma obawy, że do repozytorium nie zostaną przekazane razem. Wtedy cofnięcie się w ramach repozytorium do kodu z przed na przykład kilku dni nie stanowi problemu.
O migracjach w Rails można by mówić wiele, fakt faktem brakuje mi czegoś takiego w frameworkach Javowych. Słyszałem, że jakieś OMR'y generują patche dla konkretnej bazy danych w oparciu o zmiany jakie zaszły w modelu. Jakie? Nie wiem, może wy mi powiecie, jestem bardzo ciekaw. Wtedy byłoby już fajnie, chociaż wciąż nie pozostawalibyśmy DRY, ale przynajmniej były to już jakiś krok w dobrym kierunku.
Orginał tego postu wyglądał ciutkę inaczej od tego co tu przeczytałeś, ale durny siersciuch (kot) gonił muchę po mieszkaniu, przebiegł się po klawiaturze, skasował sporą część zaznaczonego tekstu, a blogspot automatycznie po kilku sekundach zapisał zmiany. 40% tekstu jest odtworzone, a więc podatne na błędy i gorsze jakościowo - nie cierpie pisać czegoś dwa razy.
Z kota zrobie jutro gyros.
update: jeśli ktoś zna jakiś framework, który rzeczywiście na podstawie schematu bazy i modelu, jest w stanie wygenerować skrypt sql ze zmianami jakie należy wprowadzić w bazie, niech da mi prosze znać na maila lub w komentarzach. W JPA szukałem i się nie doszukałem.