17/03/2012

RavenDB (cz. 5) - JsonIgnore

Home

Kiedy zapisujemy w Raven DB jakiś obiekt, to domyślnie w bazie zostaną zapisane wartości wszystkich jego właściwości, publicznych i prywatnych, a także tych tylko do odczytu. Nie zawsze jest to pożądane, niektóre rzeczy chcemy po prostu pominąć. W takiej sytuacji z pomocą przychodzi nam atrybut JsonIgnore. Właściwości oznaczone tym atrybutem będą pomijane przez silnik serializujący, a znajdziemy go w dll'ce Newtonsoft.Json.dll używanej przez Raven DB.
public class Test
{
 public int Id { get; set; }
 public int string WillBeSavedToRavenDB{ get; set; }
 [JsonIgnore]
 public int string WillBeIgnoredByRavenDB{ get; set; }
}
Użycie atrybutu JsonIgnore może być jednak problematyczne. W swoich projektach używam klasy BaseEntity, która jest klasą bazową dla innych encji np.: ExpressionEntity, TranslationEnity itd. Klasa ta zdefiniowana jest w osobnej bibliotece nie mającej niż wspólnego z Raven DB. W szczególności wykorzystuję ją w projektach, które korzystają z relacyjnej bazy danych.

Łatwo się domyślić, że klasa ta ma właściwości, których nie chcę zapisywać w dokumentowej, lub innej, bazie danych. Innymi słowy wymaga to abym oznaczył je atrybutem JsonIgnore czyli dodał do projektu zawierającego tą klasę referencję do biblioteki Newtonsoft.Json.dll. Nie chciałem jednak tego robić, bo jest to atrybut specyficzny dla Raven DB i biblioteka ta nie jest potrzebna we wszystkich moich projektach.

Problem rozwiązałem oznaczając interesujące właściwości jako virtual, umożliwiając tym samym ich przedefiniowanie (ang. override) i oznaczenie atrybutem JsonIgnore w projektach używających Raven DB.
public class BaseEntity
{
 public virtual string Name { get; set; }
}

public class TestEntity : BaseEntity
{
 public int Id { get; set; }
 public string Id { get; set; }
 [JsonIgnore]
 public override string Name
 {
  get { return base.Name; }
  set { base.Name = value; }
 }
}
Niestety z moich obserwacji wynika, że opisane podejście nie działa z właściwościami protected. Jeśli przedefiniowujemy taką właściwość i oznaczamy atrybutem JsonIgnore to zostanie to zignorowane, a jej wartość zostanie zapisana w bazie danych. Czyżby bug w Raven DB?

Podsumujmy co już umiemy:
  • Osadzić Raven DB w aplikacji hostującej.
  • Zainicjować Raven DB.
  • Skonfigurować dostęp do Raven Studio i API REST'owego.
  • Tworzyć obiekty POCO jakie mogą zostać umieszczone w Raven DB.
  • Dodawać/usuwać/modyfikować dokumenty.
  • Zadawać proste i te trochę bardziej skomplikowane zapytania.
  • Utworzyć indeks.
  • Skorzystać z algorytmu Map/Reduce.
  • Skorzystać z zapytań Lucene.
  • Wymusić zwrócenie przez zapytanie aktualnych danych.
  • Sterować tym, które właściwości zostaną zapisane do bazy danych.

03/03/2012

Domeny aplikacyjne, konstruktor statyczny, a platforma x86 vs x64

Home

Na początek trochę kodu. Zacznijmy od klasy testowej:
public class TestClass : MarshalByRefObject
{
    static TestClass()
    {
        Console.WriteLine(String.Format("I'm in the static constructor in the domain '{0}'.",
            AppDomain.CurrentDomain.FriendlyName));
    }

    public void Hello()
    {
        Console.WriteLine(String.Format("Hello from the domain '{0}'.", AppDomain.CurrentDomain.FriendlyName));
    }
}
Teraz kod testujący:
AppDomain domain = AppDomain.CreateDomain("Test");

TestClass t = (TestClass)domain.CreateInstanceAndUnwrap(typeof(TestClass).Assembly.FullName, typeof(TestClass).FullName);
t.Hello();
Oraz pytanie co zostanie wypisane na ekran? A w szczególności ile razy zostanie wywołany konstruktor statyczny? W głównej domenie aplikacyjnej? W domenie pomocniczej? A może w obu?

Skoro o to pytam to zapewne gdzieś tkwi haczyk. Otóż okazuje się, że wynik będzie zależał od tego czy program został skompilowany z opcję Platform target ustawioną na x86 czy x64. W przypadku x86 na ekran zostanie wypisany taki wynik:

I'm in the static constructor in the domain 'Test'.
Hello from the domain 'Test'
A w przypadku x64 taki:
I'm in the static constructor in the domain 'Test'.
I'm in the static constructor in the domain 'ConsoleApplication.vshost.exe'.
Hello from the domain 'Test'
Jeśli natomiast program zostanie skompilowany z opcją AnyCPU wynik będzie zależał od maszyny na jakiej go uruchomimy.

Konstruktor statyczny dla danej klasy wołany jest co najwyżej jeden raz w danej domenie aplikacyjnej. Jak jednak widać w zależności od platformy może zostać wywołany w jednej lub dwóch domenach. Może to mieć znaczenie kiedy jego kod będzie zawierał np.: jakiś kod inicjalizujący. Przedstawiony scenariusz nie jest zbyt częsty ale jeśli wystąpi, wykrycie błędu może być trudne, dlatego dobrze wiedzieć o tej różnicy.

Opisane zachowanie testowałem na trzech maszynach więc zakładam, że nie jest to coś lokalnego i przypadkowego.

19/02/2012

RavenDB (cz. 4) - zapytania 2

Home

Ten post to kontynuacja poprzedniego postu dotyczącego zapytań w Raven DB.

Contains == Equals !!!

Jedną z funkcjonalności jaką chciałem zaimplementować w swoim programie było wyszukiwanie wyrażeń/tłumaczeń zawierających podany ciąg znaków. Brzmi prosto ale nie obyło się bez problemów. Otóż okazało się, że Raven DB traktuje wywołanie metody String.Contains jako String.Equals, chyba że do poszukiwanego ciągu znaków dodamy gwiazdki. Na przykład zamiast s.Contains("kot") użyjemy s.Contains("kot*"), s.Contains("*kot") lub s.Contains("*kot*") w zależności czy poszukiwane wyrażenie ma znajdować się na początku, na końcu lub w środku.
string textToFind = String.Format("*{0}*", textToFind);

var res = 
 from ex in session.Query<ExpressionEntity>()
 where ex.Expression!= null && ex.Expression.Contains(textToFind) || ex.Translations.Any(t => t.Translation != null && t.Translation.Contains(textToFind))
 orderby ex.Expression
 select ex
Pozostaje jeden problem, którego niestety nie udało mi się rozwiązać. Ten kod nie zadziała jeśli szukany tekst zawiera białe znaki. Załóżmy, że na liście naszych wyrażenie mamy wyrażenie Ala ma kota. Jeśli spróbujemy wyszukać "*Ala*", "*ma*", "*kota*" otrzymamy poprawny wynik. Jeśli natomiast spróbujemy wyszukać całe wyrażenie "*Ala ma kota*" zapytanie zwróci nam pustą listę. Co dziwniejsze jeśli nie użyjemy gwiazdek czyli zlecimy wyszukanie "Ala ma kota" wyrażenie zostanie znalezione.

Ale co, jeśli szukam podwyrażenia "Ala ma" lub "ma kota"? Nie jest to duże pocieszenie ale Raven DB poradzi sobie z "Ala ma*" (z "*ma kota" już nie). Moim zdaniem sugeruje to, że wewnętrznie Raven DB do wyszukiwania używa jakiejś struktury opartej o prefixy.

Małe ostrzeżenie

W jednym z zapytań chciałem użyć metody String.Equals ale zakończyło się to wyjątkiem NullReferenceException. Sądzę, że jest to związany z użyciem metod statycznych bo kiedy użyłem metody instancyjnej np.: ex.Expression.Equals(textToFind, StringComparison.InvariantCultureIgnoreCase) obyło się bez błędów.

Stale Data

Jedną z głównych idei przyświecających twórcom Raven DB było zapewnić aby pytający o dane jak najszybciej uzyskał odpowiedź nawet jeśli oznaczałoby to zwrócenie nieaktualnych, przestarzałych danych. Używając Raven DB trzeba być na to przygotowanym nawet pracując z małą ilością danych. W moim przypadku baza danych zawiera ok. 600 dokumentów, nie całe 0.5 MB po wyeksportowaniu do pliku XML i obserwuję ten efekt.

Na przykład na początku moja aplikacja wyświetlała liczbę wyrażeń obok etykiety z nazwą języka. Jeśli jednak użytkownik importował do aplikacji wiele wyrażeń to operacja zapisywania danych, odświeżania indeksów itd. po stronie bazy danych trwała na tyle długo, że wyliczona liczba wyrażeń nie zgadzała się ze stanem faktycznym.
  • Użytkownik rozpoczyna import wyrażeń.
  • Zakończenie importu z perspektywy aplikacji/użytkownika.
  • Obliczenie liczby wyrażeń w poszczególnych językach.
  • ...
  • Faktyczne zakończenie importu po stronie bazy danych.
W tej chwili do problemu podchodzę tak, że liczbę słów w danym języku wyświetlam na życzenie. Jeśli takie zachowanie Raven DB jednak nam przeszkadza to należy explicite powiedzieć, że nie śpieszy się nam i możemy poczekać na aktualne dane np.:

var res = 
 from ex in session.Query<ExpressionEntity>().Customize(a => a.WaitForNonStaleResults())
 select ex;

Lucene

Raven DB jako silnika indeksujący używa technologii Lucene .NET. Co ciekawe umożliwia zdefiniowanie zapytań w składni jaką posługuje się ten silnik. Bezpośrednie użycie Lucene może wyglądać tak:
var res = session.Advanced.LuceneQuery<ExpressionEntity>().Where(luceneQuery);
Gdzie luceneQuery to po prostu ciąg znaków zawierający zapytanie w składni zrozumiałej dla Lucene. Może to się przydać kiedy będziemy chcieli zadać zapytanie nie obsługiwane przez "LINQ to Raven DB".

Indeksy

Jeszcze parę słów o indeksach. Termin ten przewinął sie już kilka razy. Pracując z Raven DB trzeba sobie przede wszystkim uświadomić, że tutaj indeksy różnią się od indeksów używanych w bazach relacyjnych. Moim zdaniem dwie różnice są fundamentalne. Po pierwsze Raven DB indeksuje zapytania, a nie dane. Indeks definiujemy definiując zapytanie, a nie wskazując na przykład atrybut dokumentu, który ma zostać zaindeksowany.

Po drugie Raven DB używa indeksów przy wykonywaniu każdego zapytania nawet jeśli taki indeks nie został zdefiniowany. W takiej sytuacji tworzony jest indeks dynamiczny. Druga grupa indeksów to indeksy statyczne, zdefiniowane explicite przez użytkownika na podstawie jego wiedzy o zadawanych pytaniach. Tymczasowe indeksy dynamiczne mogą zostać zamienione na stałe jeśli Raven DB zauważy, że taki indeks jest często używany.

Podsumowanie

O ile sposób realizacji podstawowych operacji dodaj/usuń/zmodyfikuj na dokumentach bardzo chwaliłem ze względu na jego prostotę i niski koszt wejścia to o raportowaniu/zapytaniach nie mogę już tego powiedzieć. Z jednej strony nie jest to żaden rocket science ale z drugiej nie jest to już "bułka z masłem" i wymaga znajomości rożnych sztuczek i specyfiki Raven DB. Jeśli uwzględnić, że zapytania jakich użyłem były bardzo proste, a pomimo to sprawiły mi sporo kłopotów to obawiam się, chociaż są to tylko moje przypuszczenia, że realizacja skomplikowanych raportów może być w Raven DB bardzo nietrywialna.

Podsumujmy co już umiemy:
  • Osadzić Raven DB w aplikacji hostującej.
  • Zainicjować Raven DB.
  • Skonfigurować dostęp do Raven Studio i API REST'owego.
  • Tworzyć obiekty POCO jakie mogą zostać umieszczone w Raven DB.
  • Dodawać/usuwać/modyfikować dokumenty.
  • Zadawać proste i te trochę bardziej skomplikowane zapytania.
  • Utworzyć indeks.
  • Skorzystać z algorytmu Map/Reduce.
  • Skorzystać z zapytań Lucene.
  • Wymusić zwrócenie przez zapytanie aktualnych danych.

13/02/2012

Problem z linkerem

Home

Na blogu nie poruszałem jeszcze tematyki C++ ale skoro nadarzyła się okazja to czemu nie. Otóż, przez mocno już zirytowanego programistę, zostałem poproszony o pomoc w rozwiązanie problemu z kompilacją programu napisanego w C++.

Rzeczony programista chciał w swojej aplikacji użyć znalezionej w sieci biblioteki kryptograficznej. W ustawieniach projektu wskazał, więc lokalizację pliku lib z bibliotekę oraz lokalizację potrzebnych plików nagłówkowych. Niestety, pomimo wielokrotnych prób, sprawdzaniu ustawień, rwania włosów z głowy itd. próba kompilacji zawsze kończyła się długą listą błędów.



Rozwiązanie okazało się tyle proste, co nieintuicyjne i trudne do znalezienia jeśli sie o tym nie wie. Otóż biblioteka kryptograficzna została skompilowana z inną wartością parametru Runtime Library tj. Multi-threaded Debug (/MTd) niż aplikacja tj. Multi-threaded Debug DLL (/MDd). Po zmianie ustawień parametru Runtime Library dla aplikacji na wartość Multi-threaded Debug (/MTd) proces kompilacji wreszcie zakończył się sukcesem.


12/02/2012

Problem z AssemblyResolve

Home

Niedawno miałem okazję pracować z aplikacją, w której zaimplementowano obsługę zdarzenia AppDomain.AssemblyResolve. Zdarzenie to pozwala załadować assembly jeśli standardowy mechanizm platformy .NET nie poradzi sobie z tym zadaniem. Aplikacja działała poprawnie aż do migracji na platformę .NET 4.0 Czemu? O tym właśnie będzie post. Zwrócę w nim uwagę na dość istotną różnicę pomiędzy platformą .NET 4.0, a jej wcześniejszymi wersjami jeśli chodzi o wspomniane zdarzenie. Różnica ta, w określonych warunkach, może napsuć krwi.

Przechodząc do meritum chodzi o to, że począwszy od .NET 4.0 zdarzenie AppDomain.AssemblyResolve generowane jest dla wszystkich assemblies, również tych z zasobami. Poniżej cytat z dokumentacji z MSDN'a.

Beginning with the .NET Framework 4, the ResolveEventHandler event is raised for all assemblies, including resource assemblies. In earlier versions, the event was not raised for resource assemblies. If the operating system is localized, the handler might be called multiple times: once for each culture in the fallback chain.

Oznacza to, że jeśli biblioteka SomeLibrary zawiera zasoby, to zdarzenie AppDomain.AssemblyResolve zostanie wygenerowane dla nie dwa razy. Raz z żądaniem załadowania assembly o nazwie SomeLibrary, a drugi raz z żądaniem załadowania SomeLibrary.resources. Fizycznie oba assembly znajdują się natomiast w dll'ce o nazwie SomeLibrary.dll.

Jeśli obsługa zdarzenia AppDomain.AssemblyResolve zaimplementowana została w uproszczony albo raczej błędy sposób mamy problem. Mam tu na myśli implementację, w której nie przewidziano, że nie uda się znaleźć dll'ki o nazwie zgodnej z nazwą assembly. W omawianym przypadku wyglądało to mniej więcej tak:
private static Assembly MyResolveEventHandler(object sender, ResolveEventArgs args)
{
 //Determine assembly path based on the configuration
 //...
 
 Assembly asm = Assembly.LoadFrom(path);
 return asm;
}
Łatwo się domyślić, że wywołanie Assembly.LoadFrom(path) dla ścieżki SOME_PATH\SomeLibrary.resources.dll zakończy się błędem bo taka ścieżka nie istnieje. W aplikacji objawi się to wyjątkiem FileNotFoundException przy próbie odczytania czegokolwiek z zasobów. Powinno to zostać zrobione wcześniej ale łatwo to naprawić:
private static Assembly MyResolveEventHandler(object sender, ResolveEventArgs args)
{
 //Determine assembly path based on the configuration
 //...
 
 try
 {
  return Assembly.LoadFrom(path);
 }
 catch(Exception ex)
 {
  //Log exception
  
  return null;
 }
}
Albo w taki sposób, jeśli chcemy obsłużyć tylko scenariusz z zasobami:
private static Assembly MyResolveEventHandler(object sender, ResolveEventArgs args)
{
 //Determine assembly path based on the configuration
 //...
 
 if(args.Name.Contains(".resources,"))
  return null;
  
 Assembly asm = Assembly.LoadFrom(path);
 return asm;
}
Zwrócenie null powoduje, że zostanie użyty standardowy mechanizm wyszukiwania i ładowania assemblies platformy .NET, a ponieważ dll'ka SomeLibrary.dll zostanie załadowana przy pierwszym żądaniu to poradzi sobie on z załadowaniem assembly z zasobami.