Tworząc standardowe mapowania NHibernate za pomocą plików XML trzeba pamiętać o kilku rzeczach, które bardzo łatwo przeoczyć. Po ich przeoczeniu program nie działa i wywala błędy, a ich treść nie zawsze nakierowuje na przyczynę. Najlepszym tego przykładem jest chyba konieczność oznaczania plików mapowań jako "embedded resource".
Fluent NHibernate znacznie upraszcza sprawę, ale i przy nim trzeba uważać. Czasami dostajemy wyjątek niekoniecznie mówiący prosto z mostu o co chodzi. Jak ciężarna 15-latka, która w jakiś sposób musi oznajmić niczego się niespodziewającym rodzicom, że wkrótce hierarchia dziedziczenia kochanej rodziny zwiększy poziom o 1… a nie bardzo wie jak to zrobić.
Zaczniemy zabawę od takiej klasy:
1: public class Post 2: { 3: public int Id { get; set; } 4: public string Title { get; set; } 5: public User Author { get; set; } 6: }
z takim mapowaniem:
1: class PostMap : ClassMap<Post> 2: { 3: public PostMap() 4: { 5: Map(x => x.Id); 6: Map(x => x.Title); 7: Map(x => x.Author); 8: } 9: }
Jak można się domyślić, powyższy kod jest zepsuty jak zęby Baltazara Gąbki. Taka linijka dowiedzie nam racji w tej kwestii:
1: session.Save(new Post() { Title = "abc", Author = user });
Naprawmy go krok po kroku, walcząc z napotykanymi podczas uruchomienia błędami tak dzielnie jak dzielnie stare baby walczą na pielgrzymkach z TVN24. Beret na łysy czerep, pieluchomajtki pod flanelową kieckę i Allah Akbar!!!
Pierwszy problem:
NHibernate.MappingException: No persister for: Procent.Samples.Entities.Post
Z treści błędu dowiadujemy się, że system nie ma persistera dla klasy posta:). Po chwili namysłu olśnienie: klasa Post jest widoczna, ale nie wiadomo co z nią tak naprawdę trzeba zrobić. Czyli: NHibernate nie potrafi dotrzeć do jej mapowania. Czyli: klasa PostMap w kontekście NH nie istnieje. Dłoń z plaskiem uderza w czoło: oczywiście! Przecież domyślnie klasom nadawany jest modyfikator internal, a żeby NH mógł w ogóle dostrzec PostMap, musi być ona public. Mała zmiana dla człowieka, brak zmiany dla ludzkości:
1: public class PostMap : ClassMap<Post> 2: {
Tip 1: klasy mapowań muszą być PUBLICZNE.
Dumni jednak jeszcze być nie możemy, gdyż oto oczom naszym ukazuje się błąd kolejny:
System.Xml.Schema.XmlSchemaValidationException: The element ‘class’ in namespace ‘urn:nhibernate-mapping-2.2’ has invalid child element ‘property’ in namespace ‘urn:nhibernate-mapping-2.2’. List of possible elements expected: ‘meta, subselect, cache, synchronize, comment, tuplizer, id, composite-id’ in namespace ‘urn:nhibernate-mapping-2.2’.
Hę??
Pamiętać należy, że Fluent NH to tylko ładna nakładka na konfigurację NHibernate. Samo NH nic nie wie o takiej bibliotece i bezwzględnie oczekuje pliku XML. Fluent robi więc dokładnie to: generuje odpowiedni plik XML. Tak więc rozwiązania błędów zgłaszanych przez parser XML można spokojnie szukać w dokumentacji NH. W tym konkretnym przypadku odpowiedź zawarta jest w sekcji 5.1.4. id. A dokładniej ten fragment: "Mapped classes must declare the primary key column of the database table".
I oczywiście wszystko jest już jasne. Z rozpędu bądź niewiedzy potraktowaliśmy wszystkie właściwości jak zwykłe kolumny. Po niewielkiej zmianie zmianie idziemy dalej:
1: public PostMap() 2: { 3: Id(x => x.Id);
Tip 2: każda mapowana tabela musi mieć zdefiniowany klucz główny (Id() lub CompositeId())
Dwa błędy to nie tak strasznie. Ruszajmy zatem na trzeci z kolei:
NHibernate.MappingException: Could not determine type for: Procent.Samples.Entities.User, Procent.Samples.NH, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null, for columns: NHibernate.Mapping.Column(Author)
Tym razem coś nie tak z uzytkownikiem… Na wszelki wypadek sprawdzamy poprzednie przypadki w jego mapowaniu, ale wszystko jest OK: modyfikator publiczne i ma Id(). Ale zaraz… tutaj mamy dokładnie wskazane miejsce błędu: kolumna Author w mapowaniu Post. Nie chodzi więc o samą klasę użytkownika, po prostu zrobiliśmy coś nie tak w PostMap:
1: Map(x => x.Author);
Ano tak… przecież Author to nie jest zwykła kolumna. To jest relacja z tabelą użytkowników! Ponownie: zmiana jednej linijki:
1: References(x => x.Author);
i błąd znika.
Tip 3: uwaga na Map() i References() – nie można ich stosować zamiennie :) a w szale mapowania łatwo się zagapić.
Wydawać by się mogło, że trzy błędy to już nie tak mało w kodzie o objętości 10 linijek. A jednak…
"NHibernate.InvalidProxyTypeException: The following types may not be used as proxies:
Procent.Samples.Entities.Post: method get_Id should be ‘public/protected virtual’ or ‘protected internal virtual’
Procent.Samples.Entities.Post: method set_Id should be ‘public/protected virtual’ or ‘protected internal virtual’"
Jaka miła niespodzianka, wreszcie dostajemy wyjątek który wyraźnie wskazuje co jest nie tak. Bez ceregieli oznaczamy zatem wszystkie mapowane właściwości jako wirtualne:
1: public class Post 2: { 3: public virtual int Id { get; set; } 4: public virtual string Title { get; set; } 5: public virtual User Author { get; set; } 6: }
Tip 4: Wszystkie właściwości i metody w mapowanych klasach muszą być wirtualne.
Przedstawione pomyłki są bardzo podstawowe, ale pokazują że nawet na najprostszym poziomie trudności na osobę grającą w NHibernate czyha kilka pułapek. Wkrótce więcej wskazówek.
Fluent podobał mi się od początku, lecz jakoś nie mogłem się przemóc. Zwłaszcza, że w projekcie miałem już popisane XMLe i wszystko dobrze działało, więc nie chciało mi się robić tej samej roboty drugi raz.
Na dodatek zobaczyłem ten wpis na StackOverflow: http://stackoverflow.com/questions/1625597/is-there-any-data-on-nhibernate-vs-fluent-nhibernate-startup-performance
Nie wiem nad jak dużym projektem pracujesz, ale może masz własne spostrzeżenia/uwagi/pomiary(??) :)
@dario-g:
Projekt nie był duży, ale niezależnie od wielkości projektu – tworzenie mapowań odbywa się raz, więc wydajność tego procesu mnie nie interesuje. Jeśli inicjalizacja NH zamiast 2 sekund będzie trwała 6 czy nawet 10 sekund – dla mnie nie ma problemu. Ale taka różnica jest chyba niemożliwa, bo przeciez Fluent tworzy XMLa (co nie może być operacją kosztowną) a potem wszystko dzieje się już standardowo (tyle że wygodniej).
@procent:
No właśnie. Zastanawiające jest to, że Fluent robi pod spodem XMLa, który dalej jest obsługiwany jak XML, który piszesz ręcznie (czyli parsowanie, walidacja, itd, czyli ciężki proces). Na początku myślałem, że Fluent pomija ten proces i powinno być wygodniej i dużo szybciej. Jednak jak się okazało tak nie jest. :(
Po drugie każda dodatkowa zwłoka boli w ASP.NET, gdyż restart aplikacji oznacza oczekiwanie. A oczekiwanie 6 czy nawet 10(!!!) sekund to baardzo długi czas. :/
@dario-g
zgadzam sie… najwiekszy bol w programowaniu SharePoint to wlasnie oczekiwanie na restart app pool i rozruch srodowiska :/ czasami nawet mam wrazenie ze strona sie nie otworzy, robie kawe pale fajka i wracam by spojrzec na monitor i albo mialem racje ;) albo nie :)
Gutek
@dario-g:
Te 6-10 sekund bylo wziete "od czapy", nigdy nie czekalem az tyle. Zgadzam sie ze podczas programowania czy testowania kazda zwloka boli, ale godzimy sie na to wybierajac NHibernate – fluent nie ma tu moim zdaniem nic do rzeczy.
Zastanawiam się czy przy użyciu Fluent(lub NHibernate w ogóle) jest możliwe zrobienie klucza głównego dla tabeli który składał by się z kilku kolumn ?
@djsowa
http://blog.raffaeu.com/archive/2009/03/19/nhibernate-and-the-composite-id.aspx
Gutek
@tip4. NIe jest to wymagan. W NH po prostu robisz mapownaie a dla FNH dodajesz ponizszy kod do konfiguracji:
Niby żadna różnica ale mnie osobiście bardzo irytuje konieczność wirtualnych metod.
@Assassin:
Dzięki za tipa. Ja jednak należę do obozu uważającego, że WSZYSTKO powinno być domyślnie wirtualne, więc mnie osobiście takie cos nie mierzi – a wręcz przeciwnie.
@Assassin:
Bez virtuala nie masz LazyLoadingu i tyle.
Jak masz mapowanie w XMLach to wystarczy, że wpiszesz w definicji hibernate-mapping: default-lazy="true" i po sprawie :)