NHibernate – migracja bazy danych

Nieuchronnie zbliża się moment, w którym aplikacja, za którą jestem współodpowiedzialny zostanie opublikowana i zacznie działać u klienta. Lubię ten moment bo jest to ostateczna weryfikacja naszej pracy. Zdaje sobie jednak sprawę, że wiąże się to z wprowadzeniem pewnych dodatkowych funkcjonalności w aplikacji, które w fazie rozwojowej są (całkowicie?) zbędne. I właśnie jedyną z takich funkcjonalności chciałem omówić a konkretnie migrację bazy danych.

Początki

Pamiętam jak dziś moment, w którym oddaliśmy (na szczęście do testów) po raz pierwszy aplikację klientowi. Ogólnie był on zadowolony, chciał abyśmy poprawili kilka rzeczy (tu większa czcionka, jakieś skróty klawiszowe itp.) oraz dorobili kilka funkcjonalności. Oczywiście nie stanowiło to dla nas żadnego problemu. Zakasaliśmy rękawy i wzięliśmy się ostro do roboty. Gdy przeróbki były już gotowe ogarnęły nas pewne wątpliwości. Otóż nie przemyśleliśmy jednej rzeczy – w jaki sposób zrobić update bazy danych? Dodatkowe funkcjonalności wiązały się z wprowadzeniem nowych tabel i nowych kolumn w istniejącej już strukturze danych. Co gorsz jeśli chodzi o C# + NHibernate to nie za bardzo mieliśmy się kogo poradzić – byliśmy młodzi i niedoświadczeni. Na szczęście w innych językach też są takie zagadnienia więc po konsultacji z programistą innego niż C# języka wiedzieliśmy, że musimy napisać własny mechanizm migracji. Co też niezwłocznie uczyniłem.

Własne rozwiązanie

Korzystając z systemu kontroli wersji (SVN brr!!!) wyodrębniłem zmiany, które dokonaliśmy w bazie a następnie przepisałem je do skryptu, który miałem uruchomić. Wiedziałem już co mam zrobić, wiedziałem jak (ADO.) pozostał tylko jeden problem jak to zrobić aby skrypt wykonał się tylko raz i żeby następne skrypty (bo już szykowały się następne zmiany) też wykonywały się raz i to w określonej kolejności. Zrobiłem wtedy w sumie prostą rzecz. Utworzyłem tabele, w której zapisuje informację o bieżącej wersji aplikacji. Uruchamiając aplikację, która korzysta z nowej wersji programu(a w zasadzie bazy) porównywane są numery kolejnych aktualizacji i sekwencyjnie są uruchamiane. Oczywiście aplikację zabezpieczyłem przed błędem (a przynajmniej tak mi się wydawało) tzn. jeśli aplikacja w procesie updatu zwróci błąd to wszystkie operacje wchodzące w skład migracji należy uznać za niebyłe. Byłem z siebie dumny jak paw.

Wątpliwości

Wszystko działało dobrze (i nadal działa – po co ruszać coś co nie sprawia problemów), ale mnie ogarniały coraz większe wątpliwości. Porównywałem mój mechanizm migracji z wzorcowym mechanizmem, który kiedyś nam pokazano (w działaniu – nie w kodzie). Pomimo faktu, że język starego programu wydawał się archaiczny (Visual FoxPro) to oferował więcej niż mój mechanizm. Przede wszystkim była w nim możliwość cofnięcia bazy do starszej wersji. Nie potrzebowałem wtedy tego mechanizmu (i do tej pory nie zdarzyło mi się z niego korzystać w mojej aplikacji), ale sama świadomość, że można to zrobić lepiej działała na mnie jak płachta na byka. Kwestią o wiele istotniejszą było jednak powiązanie mechanizmu migracji z bazą danych, z której korzystałem. Cała transakcyjność zależna była od bazy, co lepiej zobrazuje przykład:

CREATE TABLE  test.Person1 (id INT);
CREATE TABLE  test.Person2 (id INT);
CREATE TABLE  test.Person3 (id INT);
CREATE TABLE  test.Person1 (id INT);

To treść skryptu updatującego strukturę bazy. Jeśli potraktujemy to jako jednolity tekst (a tak właśnie robiłem) to co się stanie w bazie? Ile (i czy w ogóle) tabel zostanie utworzonych w bazie danych? Odpowiedź brzmi to zależy. Zależy od tego jakiej bazy używamy. W niektórych zapisane zostaną trzy tabele a w innych żadna (swoją drogą rozumiem teraz sens pytania o transakcje i DDL – lepiej późno niż wcale).

FluentMigrator

Znam już ograniczenia mojego sposobu migracji i postanowiłem znaleźć jakieś rozwiązanie. Oczywiście w NHibernate istnieje coś takiego jak SchemaUpdate, ale o ile w wersji rozwojowej aplikacji jest to dostatecznie dobre rozwiązanie to używanie tego na produkcji nie jest dobrym pomysłem. Ponieważ jestem leniwym programistą postanowiłem znaleźć już jakąś gotową bibliotekę. Trochę poszukiwań i znalazłem coś co na tą chwilę mi odpowiada FluentMigrator.
Użycie biblioteki jest proste. Zaczynamy standardowo od instalacji właściwego pakietu

Install-Package FluentMigrator

Następnie tworzymy klasę służącą do migracji dziedziczącą z abstrakcyjnej klasy Migration i implementujemy abstrakcyjne metody Up oraz Down (Down pod warunkiem, że jest potrzeba) i tyle.

[Migration(1)]
public class TestMigration : Migration
{
    private const string TableName = "Person";

    public override void Up()
    {
        Create.Table(TableName)
            .WithColumn("Id").AsInt32().NotNullable()
            .WithColumn("Name").AsString().NotNullable();
    }

    public override void Down()
    {
        Delete.Table(TableName);
    }
}

Atrybut Migration wskazuje który numer kolejny ma mieć dana migracja (bo w końcu kolejność jest ważna). Ponadto atrybut możemy wzbogacić o informacje o transakcyjności procesu migracji (domyślnie włączona) oraz uzupełnić o opis czego dana migracja dotyczy.

Jeśli, ktoś nie chce korzystać z dobrodziejstw interfejsu fluent (albo ma zbyt skomplikowane zapytanie) to zawsze może uruchomić skrypt na jeden z trzech sposobów.

Execute.EmbeddedScript(scriptName);
Execute.Script(pathToScript);
Execute.Sql(sqlStatement);

To tylko fragment możliwości jakie oferuje FluentMigrator. Można na przykład uzależnić update od rodzaju bazy, do której się odnosi:

IfDatabase("oracle").Execute.EmbeddedScript(scriptName);

Zadowolony z siebie postanowiłem teraz w jakiś sposób uruchomić moją migrację i przetestować jak ona działa. W tym celu należy ściągnąć jeszcze jeden pakiet

Install-Package FluentMigrator.Tools

Po jego ściągnięciu żadna nowa referencja nie jest dodawana do projektu. Teraz jedynie wystarczy z pomocą wiersz poleceń odpalić narzędzie Migrate.exe

Migrate.exe -a Migration.dll -db kindOfDataBase -conn connectionString 

Pełna składnia wraz z wszystkimi przełącznikami dostępna jest po uruchomieniu z wiersza poleceń.

Po uruchomieniu migracji zajrzałem jeszcze do bazy danych. A tam została utworzona dodatkowa tabela VersionInfo. Zawiera ona informację o migracjach zastosowanych do naszej bazy wraz z ich datą i opisem (jeśli zostawimy opis pusty to będzie to nazwa naszej klasy). Nie byłbym sobą gdybym nie przeprowadził drobnego eksperymentu. Otóż za pierwszym razem miałem dwie migracje nr 1 i nr 3. Wykonałem migrację po czym rozbudowałem mój projekt o migrację nr 2. Odpalając skrypt (przy domyślnych ustawieniach) została dołożona migracja nr 2. Należy o tym pamiętać i albo nie zostawiać dziur w numeracji albo odpowiednio zmodyfikować skrypt migracyjny(a ogólnie to używać tego świadomie).

2 thoughts on “NHibernate – migracja bazy danych

  1. Pingback: dotnetomaniak.pl
  2. Również polecam, stosuję bibliotekę w każdym nowym projekcie. Szczególnie przydatne również w trakcie developmentu, kiedy każdy programista posiada własną bazę danych i wszystkie trzeba aktualizować w ten sam sposób wraz z nową wersją aplikacji.

    Z innej beczki: jeszcze nie zdażyło mi się zaimplementować metodę “Down” 🙂

Comments are closed.