poniedziałek, 23 czerwca 2008

Migracje bazodanowe: czego w Javie nie znajdziemy?

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.

5 komentarzy:

KosciaK pisze...

Mam radę na biegające po klawiaturze sierściuchy. Zamiast pisać notkę w Bloggerze pisz w Google Docs. Tam nawet jeśli kot ci skasuje część tekstu to zawsze masz wbudowaną kontrolę wersji i po prostu przywracasz sobie poprzednią wersję (poprzedni automatyczny save).
Po napisaniu dajesz Publikuj i gdzieś tam jest opcja publikowania na blogu.
W Zoho Writer też pewnie by się dało, ale tak dobrze go nie znam

Lukasz pisze...

Powiem ogólnie, tworzenie schematu bazy danych na podstawie modelu danych to zły pomysł ;-)
No chyba, że używamy db4o, ale to przypadek graniczny.
Sam do tej pory tak robiłem, jednak obecnie zaczynam się przekonywać, że model danych nie powinien mapować się jeden do jeden na tabelki. Może tak być, ale w prostych / specyficznych przypadkach.
Tak jest właśnie w mojej firmie, tworzy się model, następnie za pomocą specjalnego narzędzia próbuje się go przekształcić na model bazy danych i dopiero na tej podstawie tworzy się mapowanie w Hibernatecie. A na końcu specjalne narzędzie analizuje, czy to mapowanie spełnia wymogi wewnętrzne i dopiero tworzy schemat bazy danych.
Pewnie, że nie każdy może sobie pozwolić na takie podejście. Jednak należy się zastanowić na schematem bazy danych, a nie tylko bezmyślnie mapować model na bazę danych i zlecać aby bazę utworzyło za nas narzędzie do mapowania.
Nie znam RoR i nie wiem jak on tworzy bazy danych, ale jeśli jest to automat, to robi to zawsze tak samo, co nie oznacza, że dobrze ;-)
Wiem, że my programiści nie zaprzątamy sobie głowy czymś tak przyziemnym jak baza danych, ale prawda jest taka, że nasze programy nigdy nie będą działać w oderwaniu od infrastruktury, którą trzeba rozumieć, żeby napisać dobry program :D

Paweł Szulc pisze...

@lukasz: nie do konca cie rozumiem, sposob jaki dany fraework (niezaleznie czy to hibernate toplink czy RoR) tworzy baze jest zawsze jasno okreslony. Jesli wiesz jak jest tworzony to nie ma tu najmniejszego problemu, tym bardziej jesli znasz sie na projektowaniu baz danych. W takich specyfikacjachach jak JPA masz dokladna kontrole nad wszystkim aspektami tworzonego schematu z modelu: tabel laczacych itp.

Osobiscie uwazam jednak ze w aplikacji w ktrej trzeba stosowac wspomniane przeze mnie patche, powinnien byc tez tak zwany patch zerowy, czyli taki ktry sklada sie z samych create table i tworzy poczatkowy, pierwszy schemat. Wtedy mamy pozniej zachowana jakas ciaglosc.


Nie mniej jednak narzedzie, ktre generowaloby mi skrypt sql nanoszacy zmiany pomiedzy schematem bazy a modelem, bylby bardzo przydatny.

Paweł Szulc pisze...

@kosciak: dzieki, bede uzywal na przyszlosc

pawelsto pisze...

Witam

Ciekawym narzędziem do śledzenie zmian w schemacie bazy danych oraz tworzeniem odpowienidego pliku zmian jest Liquibase -
http://www.liquibase.org/

Pozdrawiam