17 wrz 2009

nHibernate associations introduction

Mapowanie relacji pomiędzy tabelami na asocjacje klas to dusza ORMow.
To przy okazji najbardziej problematyczne zagadnienie.
Zagadnienie relacji jest szerokie jak rzeka i nie sposób opisać wszystkich przypadków. Najważniejsze z mojego punktu widzenia jest jednak zrozumieć sedno sprawy, zrozumienia tego brakowało przy moich pierwszych próbach i przyznam że zjadło mi to wiele godzin.

Dzięki zastosowaniu klas POCO możemy posługiwać się niedostępnymi dla modelu DataSetów kolekcjami i powiązaniami między nimi. Musimy jednak pamiętać o tym że:
asocjacje w nHibernate są z natury skierowane jednokierunkowe.

Powiązanie powyższych klas to dla nHibernate dwa zupełnie osobne powiązania Bid -> Item oraz Item -> Bid.
To dalej oznacza że przypisania bid.Item = item oraz item.Bids.Add(bid) to dwie oddzielne operacje.

Modelując powyższe klasy będziemy mieli taki oto przykład.
Na początek proste jednokierunkowe powiązaniemany-to-one
public class Bid 
{
  ...
  private Item item;
  public Item Item 
  {
    get { return item; }
    set { item = value; }
  }
...
}
Oraz plik XML

  ...
  

Kolumna ITEM_ID tabeli BID jest kluczem obcym do klucza głównego tabeli ITEM.

Do czasu kiedy powiązania pozostają jedno -kierunkowe bardzo prosto korzysta się z nHibernata nie zdając sobie nawet sprawy z tego co może się dziać gdzie głębiej.
Ale czy nie było by użytecznym mieć również powiązanie w drugą stronę? Oj było by, a w aplikacjach opartych na czysto microsoftowych kontrolkach aspowych nie wyobrażam sobie żeby można było nie mieć dwukierunkowych powiązań.

Tak więc stwórzmy powiązanie w drugą stronę one-to-many.
public class Item 
{
  ...
  private ISet bids = new HashedSet();
  public ISet Bids {
    get { return bids; }
    set { bids = value; }
  }
  public void AddBid(Bid bid) 
  {
    bid.Item = this;
    bids.Add(bid);
  }
...
}

...
  
    
    
  

Mapowanie kolumny określone elementem jest to kolumna klucza obcego w tabeli BID.
Dla obu asocjacji została określona ta sama kolumna klucza obcego.
Teraz mamy dwie różne jednokierunkowe acjocjacje do tego samego klucza obcego.
I to jest oczywiście problem jeśli spróbujemy zrobić jakąś operację zapewne dostaniemy błąd z serii "Próba modyfikacji obiektu przypisanego do innej sesji". Wynika to z faktu że mając dwie różne asocjacje mamy dwie różne reprezentacje tej samej wartości klucza w pamięci - a co za tym idzie dwie różne zmiany i konflikt.
nHibernate sam z siebie NIE wykrywa faktu że te dwie zmiany dotyczą tej samej kolumny w bazie danych. Musimy powiedzieć nHibernatowi że te dwa powiązania jednokierunkowe to jedno powiązanie dwukierunkowe. A wystarczy do tego atrybut INVERSE

...
  
    
    
  

Deklarując atrybut INVERS="true" (domyślnie oczywiście false) wyraźnie wskazujemy która końcówka tej asocjacji ma być synchronizowana z bazą danych. W tym przypadku mówimy że do bazy propagowane mają być zmiany w obiekcie Item natomiast zmiany w kolekcji bids mają być ignorowane. Zapis item.Bids.Add(bid) nie będzie miał żadnego efektu w składnicy danych, ponieważ domyślną wartością kolejnego atrybutu jest "none" (chyba że wywołamy sobie na tym obiekcie ISession.Save()).
  
...
  
     
     
   

Aby zmiany dokonane w Bids były propagowane do bazy musimy ustawić atrybut CASCADE="save-update" (możliwe jest również "all-delete-orphan" przydatne przy relacjach parent-child).
Atrybut CASCADE mówi nHibernatowi aby zapisał do bazy każdy nowy obiekt Bid jeśli Bid jest 'składnikiem' Item.

16 wrz 2009

Definiowania meta danych mapowania

Lepiej będzie tu zawrzeć orginalną definicje metadanych
ORM tools require a metadata format for the application to specify the mapping between classes and tables, properties and columns, associations and foreign keys, .NET types and SQL types. This information is called the object/relational mapping metadata. It defines the transformation between the different data type systems and relationship representations.
Omawiać będę tylko mapowanie poprzez XML (możliwe jest jeszcze mapowanie poprzez atrybuty) ponieważ jest ono stosowane w projekcie w którym pracuje.
Przykładowy xml mapowania – plik taki powinien mieć rozszerzenie hbm.xml czyli np. Category.hbm.xml oraz musi być włączony do projektu .net jako Embedded Resource



 






Property mapping
Mapowanie property zazwyczaj zawiera nazwę property, nazwę kolumny w bazie danych i może mieć jeszcze kilka innych atrybutów, jednak nie są one już konieczne. Zalecane jest stosowanie not-null w celu walidacji danych przed wysłaniem do bazy. Typ określany jest na podstawie typu property za pomocą reflection – jeśli nie zostanie podany.
Przykładowe mapowanie propercji:

Oczywiście jest wiele dalszych możliwości jednak w większości przypadków proste propercje są wystarczające, przynajmniej narazie ;)

Możemy sterować zapisem danej propercji do bazy

Powyższa property nie zostanie nigdy zmodyfikowana w bazie (nie pojawi się ani w Insercie ani w Updacie)

Być może przydatnym może okazać się poniższe porównanie.



...



=====



...


Czyli możemy sobie zadeklarować domyślny assembly i namespace dla klasy lub powtarzać pełną nazwę za każdym razem.
Kolejnym krokiem są asocjacje

POCO

Ostatnio wrzuci mnie w projekcik z którym z woli klienta dostęp do bazy realizowany jest z wykorzystaniem nHibernata
Troche więc o nHibernacie aby nie umkneło na przyszłość.
Na początek
POCO = Plain Old CLR Object
Czyli proste niezwiązane klasy zawierające zazwyczaj proste property.
NHibernate nie wymaga nawet aby klasa była serializowalna wymaga jedynie domyślnego bezparametrowego konstruktora oraz publicznych property wyrażających asocjacje między klasami POCO.
Klasy POCO są reprezentacją modelu fizycznego, trzeba przestrzegać kilka zasad, rozważmy więc poniższy przykład. To asocjacja jeden-do-wiele a przypadkiem jeszcze do siebie samego.
public class Category : ISerializable 
{
   private string name;
   private Category parentCategory;
   private ISet childCategories = new HashedSet();
   public Category() { }
   ...
}
Aby zaimplementować powiązanie potrzebujemy dwóch atrybutów parentCategory implementujące końcówkę‘jeden’ połączenia oraz childCategories – atrybut kolekcja implementujący końcówkę ‘wiele’.
public ISet ChildCategories 
{
   get { return childCategories; }
   set { childCategories = value; }
}
public Category ParentCategory 
{
   get { return parentCategory; }
   set { parentCategory = value; }
}
Dodawanie childa Categoty wyglądała by tak
Category aParent = new Category();
Category aChild = new Category();
aChild.ParentCategory = aParent;
aParent.ChildCategories.Add(aChild);
Dobrze jest systematyzować takie dodawanie połączeń w procedury z wiadomych powodów
public void AddChildCategory(Category childCategory) {
if (childCategory.ParentCategory != null)
childCategory.ParentCategory.ChildCategories
.Remove(childCategory);
childCategory.ParentCategory = this;
childCategories.Add(childCategory);
}
Zawsze w takim przypadku muszą być wykonane dwie akcje:
- property parent musi zostać ustawione dla obiektu child
- obiekt child musi zostać dodany do kolekcji childów parenta.

Wynika to z faktu że NHibernate nie zarządza sam z siebie asocjacjami więc jeśli chcemy stworzyć lub zmienić jakąś asocjacje to należy zrobić tyle ile byśmy zrobili bez NHibernata. Obiekt powinien być tak napisany aby przechodzić testy jednostkowe, więc wszystkie asocjacje muszą zostać przypisane na poziomie obiektu. Jeśli asocjacja jest dwukierunkowa oba kierunki muszą zostać uzupełnione.
'