Niestandardowe generowanie kluczy głównych w NHibernate

Robiąc ostatnio mały projekt postanowiłem w końcu wykorzystać NHibernate, na temat tego ORM'a jest w necie masa informacji więc nie będę się na jego temat rozpisywał. Podobnie nie będę opisywał konfiguratora Fluent NHibernate, który jest znacznie bardziej intuicyjny niż tradycyjne pliki hbm (oparte na XML).

Problemem przed jakim stanąłem był fakt że moja aplikacja miała współpracować z bazą na której działała inna aplikacja, posiadająca dosyć charakterystyczną politykę generowania kluczy głównych. Która nijak nie pasowała do żadnej standardowej polityki zawartej standardowo w Bibliotece hibernate'a.

Na szczęście NHibernate jest przygotowany na taką ewentualność, jego API zawiera interfejs IIDentifierGenerator, który umożliwia pisanie własnych polityk generowania kluczy. Interfejs ten zawiera jedną metodę Generate.

public class MyGenerator:IIDentiferGenerator
{
    public object Generate(ISessionImplementor session, Object obj)
   {
        return Guid.NewGuid()
    }
}

Pierwszy parametr zapewnia dostęp do obiektów związanych z konfiguracją połączenia z bazą danych oraz stanem tego połączenia, drugim parametrem jest obiekt dla którego generowany jest klucz. Przykład powyżej dla każdego obiektu zwróci wartość statycznej metody NewGuid która nadaje się na klucz główny ponieważ z definicji jest to Global Unique Identifier. Jednak generowanie nowego Guida nie jest niczym nadzwyczajnym po za tym biblioteka NHibernate'a już ją zawiera.

Częstym sposobem generowania kluczy głównych jest procedura składowana (lub fragment zapytania SQL) która zwraca najniższą nieużywaną wartość którą aplikacja może wykorzystać jako klucz dla danej tabeli.

Aby uruchomić takie zapytanie pod kontrolą NHibernate należy wykorzystać właściwości Connection i Batcher parametru session przekazanego do metody generate.

public class MyGenerator:IIDentiferGenerator
{
    public object Generate(ISessionImplementor session, Object obj)
   {
        using(IDbCommand command = session.Connection.GenerateCommand())
        {
            command.CommandText = "exec GetNextId()";
            using(IDataReader  reader = session.Batcher.ExecuteReader())
            {
                if (reader.Read())
                {
                     return reader.GetInt(0);
                }
                else
                {
                      throw new Exception ("Nie da się wygenerować klucza.");
                }
            }
        }
    }
}

Klasa przedstawiona powyżej jedną poważną wadę: jest bardzo sztywna tj. trudno byłoby ją wykorzystać do generowania klucza dla dowolnej encji. Informację na temat tabeli w której należy sprawdzać "Zajętość" danego identyfikatora można uzyskać tylko przez weryfikację typu parametru obj metody Generate. Wykorzystanie tego faktu sprawiłoby to że programista musiałby dla każdego obsługiwanego typu dopisywać kilka linijek kodu w tej metodzie co generowałoby masę powtarzalnego kodu.

Znacznie lepszym rozwiązaniem jest Implementacja Interfejsu IConfigurable zawierającego metodę Configure która przyjmuje jako parametry następujące wartości:

type – typ dla jakiego jest konfigurowana klasa

params – Słownik klucz <=> wartość przechowujący dodatkowe dane konfiguracyjne.

d – informacja na temat silnika bazodanowego przechowującego dane.

Parametrem nas interesującym jest params zawiera on dane na temat tabeli w której zapisywany jest dany typ oraz kolumny będącej kluczem głównym. Nie jestem pewien czy dla każdego silnika bazodanowego nazwy kluczy są takie same dlatego nie podaję tutaj konkretnego fragmentu kodu.

Pozostaje jeszcze tylko kwestia jak powiedzieć NHibernate'owi żeby dla danej klasy używał naszego generatora. We FluentHibernate wymaga to wpisania jednej linijki w klasie Mapującej:

Id(obj => obj.ID).GeneratedBy().Custom<MyGenerator>()

Edit: 02-10-2010

Podczas prac w NHibernate zauważyłem że zachowuje się on dosyć specyficznie podczas zapisu więcej niż jednego obiektu jedego typu w ciągu jednej tranzakcji. Otóż najpierw generuje on klucze główne dla wszystkich nowych obiektów, a następnie dokonuje ich zapisu. Jeżeli teraz oprzemy nasz mechanizm zapisu na zapytaniu do bazy to musimy się liczyć z faktem iż za każdym razem generowanie klucza będzie się odbywać na niezmienionej bazie. Więc zapytanie wywołane za każdym razem zwróci tą samą wartość. Należy to uwzględnić w kodzie, najczęściej przez duplikację części logiki zapytania, co jest oczywistą wadą tego rozwiązania, zaletą jego zaś jest mniejsza ilość zapytań co zazwyczaj poprawia aplikację pod względem wydajnosciowym.