Showing posts with label Raven DB. Show all posts
Showing posts with label Raven DB. Show all posts

08/05/2012

RavenDB (cz. 7) - HttpListenerException

Home

Ten post będzie krótki i zwięzły. Jakiś czas temu kiedy włączyłem UAC (User Account Control) ze zdziwieniem zauważyłem, że moja aplikacja używająca Raven DB nie działa. Przy wywołaniu metody DocumentStore.Initialize rzucany był wyjątek HttpListenerException. Po wyłączeniu UAC błąd nie występował.

Z problem łatwo sobie poradzić nadając użytkownikowi, jaki uruchamia aplikację, uprawnienia do nasłuchiwania na porcie używanym przez bazę danych. Można to zrobić przy pomocy narzędzia httpcfg lub netsh tak jak to zostało opisane w tym dokumencie. Co jednak najlepsze Raven DB dostarcza gotowej klasy NonAdminHttp, w bibliotece Raven.Database.dll, która rozwiązuje ten problem. Poniżej przykład użycia.

...
NonAdminHttp.EnsureCanListenToWhenInNonAdminContext(Store.Configuration.Port);

Store.Initialize();
...

A tak w ogóle to warto zajrzeć, przy pomocy jakiegoś deasemblera, do wnętrza metody EnsureCanListenToWhenInNonAdminContext i zobaczyć jak została zaimplementowana.

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.
  • Rozwiązać kłopoty związane z IntelliTrace i Raven DB.
  • Rozwiązać problem z HttpListenerException.

29/03/2012

RavenDB (cz. 6) - małe kłopoty z IntelliTrace

Home

Ten post będzie krótki ale poruszę w nim sprawę, o której dobrze wiedzieć aby potem nie kląć pod nosem i nie wołać o pomstę do nieba, bo coś nagle przestało działać.

Otóż Raven DB, z powodów opisanych dalej, nie współpracuje dobrze z historycznym debugerem IntelliTrace pracującym w trybie rozszerzonym (IntelliTrace events and call information). Jest to tryb, w którym IntelliTrace monitoruje wywołania metod, konstruktorów, dostęp do właściwości itd. oraz dodatkowo tzw. zdarzenia diagnostyczne, które są monitorowane również w trybie podstawowym (pisałem o tym w poście).

Tak naprawdę problem nie jest związany bezpośrednio z Raven DB ale z jedną z bibliotek z jakich korzysta. Nie współpracuje to zresztą eufemizm, bo powinienem napisać nie działa, wywala się... Jeśli uruchomimy aplikację korzystającą z Raven DB pod kontrolą IntelliTrace w pewnym momencie (próba zapisu, odczytu, utworzenia indeksu) otrzymamy wyjątek VerificationException z komunikatem Operation could destabilize the runtime.. Call stack zaprowadzi nas natomiast do biblioteki Newtonsoft.Json.

IntelliTrace wstrzykuje w kod monitorowanych programów własne instrukcje i to najpewniej w tym przypadku powoduje błąd. Z problemem można sobie jednak łatwo poradzić mówiąc IntelliTrace, aby ignorował tą bibliotekę. W tym celu otwieramy okno opcji Tools -> Options, wybieramy menu IntelliTrace i dalej Modules, klikamy przycisk Add..., i w polu tekstowym wpisujemy *Newtonsoft*, a na koniec zatwierdzamy.

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.
  • Rozwiązać kłopoty związane z IntelliTrace i Raven DB.

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.

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.

07/02/2012

RavenDB (cz. 3) - zapytania

Home

Wstęp

W tej części serii poświęconej Raven DB napiszę o zapytaniach, pobieraniu danych. Temat sam w sobie jest bardzo obszerny i to, co napiszę, to tylko szczyt góry lodowej. Większość tematów po prostu zasygnalizuje, ale sądzę, że dobrze pokaże, jak to wygląda z Raven DB.

Podstawy

Podstawowe zapytania zadajemy w bardzo prosty sposób i robimy to otwierając najpierw sesję pracy z bazą np.:
using (var session = Store.OpenSession())
{
 var res = from ex in session.Query<ExpressionEntity>()
    select ex;
 ...
}
Aby pobrać listę dokumentów użyłem metody Query określając jaki typ dokumentów mnie interesuje. Dla przypomnienia klasa ExpressionEntity reprezentuje wyrażenie i jego tłumaczenia. Jest to ta sama klasa, która wcześniej posłużyła mi do umieszczenia danych w bazie.

Jeszcze jeden przykład. Tym razem pobieram listę dokumentów by na jej podstawie określić listę różnych kategorii, jakie zdefiniowano dla wszystkich wyrażeń.
var res = (from e in session.Query<ExpressionEntity>()
    where e.Category != null
    orderby e.Category
    select e.Category).Distinct();
Jak widać podstawowe zapytania wykonuje się bardzo łatwo.

Stronicowanie

Przy dużej liczbie dokumentów przydatne okaże się stronicowanie. To też nie jest trudne do zrealizowania:
var res = (from ex in session.Query<ExpressionEntity>()
    orderby ex.Expression
    select ex).Skip(index * pageSize).Take(pageSize).ToList();
Użyta w kodzie zmienna index to indeks strony do pobrania (w numerowaniu od zera), a pageSize to oczywiście wielkość strony liczona w liczbie dokumentów. Metoda Skip pozwala więc na przeskoczenie do konkretnej strony, a metoda Take na pobranie takiej liczby dokumentów jaka mieści się na stronie.

Robi się trudniej

Jedną z funkcjonalności, jakie chciałem mieć w swoim programie LanguageTrainer, było zliczanie liczby wyrażeń posiadających tłumaczenie w danym języku. Brzmi prosto, prawda? Pierwsza moja próba wyglądała tak:
 var count = 
 (from ex in session.Query<ExpressionEntity>()
 from t in ex.Translations
 where t.Language == selectedLang && !String.IsNullOrEmpty(t.Translation) 
 select 1).Count();
Zmienna selectedLang zawiera interesujący nas język. Wykonanie takiego zapytania zakończy się wyjątkiem NotSupportedException z komunikatem Method not supported: SelectMany. A więc może coś takiego:
var count =
 (from ex in session.Query<ExpressionEntity>()
 where ex.Translations.Any(t => t.Language == selectedLang && !String.IsNullOrEmpty(t.Translation))
 select 1).Count();
Tym razem zakończy się wyjątkiem z komunikatem Method not supported: IsNullOrEmpty. No cóż IsNullOrEmpty łatwo zastapić zwykłym porównaniem. Spróbujmy więc jeszcze raz:
var count = 
 (from ex in session.Query<ExpressionEntity>()
 where ex.Translations.Any(t => t.Language == selectedLang && t.Translation != null && t.Translation != String.Empty)
 select 1).Count();
To też nie zadziała i znowu zakończy sie błędem, tym razem z komunikatem Node not supported: Constant. Jeszcze jednak próba i w końcu zadziałało:
var count = 
 (from ex in session.Query<ExpressionEntity>()
 where ex.Translations.Any(t => t.Language == selectedLang && t.Translation != null && t.Translation != String.Empty)
 select ex).Count();
Nie jest to skomplikowane ale wymaga znajomości kilku "trików", nie jest do końca intuicyjne.

MapReduce

Do opisanego powyżej problemu można też podejście w Raven DB w inny sposób, a mianowicie stosując algorytm MapReduce. W ten sposób wykonując jedno zapytanie otrzymamy wyniki dla wszystkich języków za jednym razem. W Raven DB robimy to definiując indeks (jeszcze o tym napiszę):
public class TranslationsCounter : AbstractIndexCreationTask<ExpressionEntity, TranslationsCounter.ReduceResult>
{
 public class ReduceResult
 {
  public Languages Lang { get; set; }
  public int Count { get; set; }
 }

 public TranslationsCounter()
 {
  Map = docs => from doc in docs
   from t in doc.Translations
   select new { Lang = t.Language, Count = String.IsNullOrEmpty(t.Translation) ? 0 : 1 };

  Reduce = results => from t in results
   group t by t.Lang
   into g
   select new { Lang = g.Key, Count = g.Sum(x => x.Count) };
 }
}
i zadanie zapytania przy jego użyciu np.:
public IDictionary<Languages,int> CountExpressionsByLanguage()
{
 using (var session = Store.OpenSession())
 {
  var dict = new Dictionary<Languages, int>();

  foreach(var res in session.Query<TranslationsCounter.ReduceResult, TranslationsCounter>())
  {
   dict.Add(res.Lang, res.Count);
  }

  return dict;
 }
}


To jeszcze nie koniec. Do tematu wrócę w kolejnym poście.

22/01/2012

RavenDB (cz. 2) - podstawowe operacje na dokumentach

Home

Wstęp

W drugim poście poświęconym Raven DB opiszę jak wykonywać podstawowe operacje (zapisz/usuń/zmień) na dokumentach. Ale czym jest dokument? Nie wiem jaka jest formalna definicja ale ja na dokumenty patrzę po prostu jak na obiekty zapisane (zserializowane) w formacie JSON. Poniżej przykład takiego dokumentu z mojego pet project, który odpowiada obiektowi klasy reprezentującej wyrażenie i jego tłumaczenia.
{
  "Category": "Geografia",
  "Expression": "wioska",
  "Translations": [
    {
      "Language": "Language1",
      "Translation": "Dorf",
      "GoodAnswers": 0,
      "BadAnswers": 1,
      "WasLastAnswerGood": false
    },
    {
      "Language": "Language2",
      "Translation": "village",
      "GoodAnswers": 1,
      "BadAnswers": 0,
      "WasLastAnswerGood": true
    }
  ]
}
Klasa, której obiekty chcemy wrzucić do Raven DB może być zwykłym POCO z dokładnością do jednej rzeczy. Musi posiadać taką właściwość:
public int Id { get; set; }
To przykład konwencji. Jeśli chcemy zmienić sposób przechowywania identyfikatora, należy skorzystać z DocumentStore.Conventions.FindIdentityProperty. Użyty przeze mnie EmbeddableDocumentStore dziedziczy z DocumentStore, a więc jeśli znudzi mi się Id to zawsze mogę to zmienić.

Jeśli chcemy aby Raven DB ignorował niektóre właściwości w czasie zapisywania obiektu to powinniśmy oznaczyć je atrybutem Newtonsoft.Json.JsonIgnore, który działa w taki sam sposób jak XmlIgnore dla XmlSerializer.

Zapisz/Zmień

Operację wstawienia nowego dokumentu do bazy lub aktualizacji już istniejącego wykonuję w taki sam prosty sposób np.:
public void Save(ExpressionEntity ex)
{
 using (var session = Store.OpenSession())
 {
  session.Store(ex);
  session.SaveChanges();
 }
}
Zaczynam, więc od otworzenia sesji pracy z RavenDB. Następnie przy pomocy metody Store wrzucam do bazy nowy dokument lub aktualizuję już istniejący. Jak widać nie przejmuję się, którą operację właściwie wykonuję. Jeśli obiekt będzie miał pusty/zerowy Id to zostanie utworzony nowy dokument, a właściwość Id zostanie uaktualniona. Jeśli Id zostanie wypełnione Raven DB spróbuje odszukać istniejący dokument i go uaktualnić. Jeśli nie znajdzie dokumentu o podanym identyfikatorze utworzy go. Metoda SaveChanges to coś w rodzaju Commit'a: wysyła zmiany do serwera.

Usuń

Usuwanie dokumentów z bazy również jest bardzo proste np.:
public void Remove(ExpressionEntity ex)
{
 using (var session = Store.OpenSession())
 {
  session.Delete<ExpressionEntity>(session.Load<ExpressionEntity>(ex.Id));
  session.SaveChanges();
 }
}
Jak widać przed usunięciem ładuję obiekt z bazy na podstawie identyfikatora. Jest to potrzebne ponieważ inaczej dostaniemy błąd z informacją, że obiekt nie jest doczepiony do sesji (is not associated with the session, cannot delete unknown entity instance). Nie znalazłem innego sposobu rozwiązania problemu.

Ważna uwaga

Powyżej przedstawiłem dwie przykładowe metody Save oraz Remove, które odpowiednio tworzą/aktualizują jeden dokument oraz usuwają jeden dokument z bazy danych. W przypadku wykonywania operacji na wielu dokumentach takie podejście będzie zbyt kosztowne. Jeśli zachodzi potrzeba pracy z wieloma dokumentami to powinniśmy zrezygnować z takiego mikro zarządzania i jak najwięcej operacji wykonywać w ramach jednej sesji.

Podsumowanie

Dzisiaj pokazałem jak wykonywać podstawowe operacje na dokumentach. Jak widać jest to bardzo proste. Właściwie nie musimy nic robić aby nakłonić nasze obiekty do współpracy z Raven DB. Nie potrzebujemy również mappera relacyjno-obiektowego. To jest właśnie to co bardzo mi się spodobało w Raven DB.

Definiuję sobie klasę, tworzę jej instancje i po prostu wrzucam je do bazy danych. Nie piszę komend INSERT, UPDATE lub DELETE. Nie instaluję dodatkowych komponentów. Nie konfiguruję mapowań pomiędzy tabelami i obiektami. Nie definiuję schematu bazy danych. Jest prosto, szybko i przyjemnie czyli tak jak powinno być!

Na koniec znowu podsumujmy co już mamy/umiemy:
  • Bazę Raven DB osadzoną w aplikacji hostującej.
  • Kod inicjalizujący Raven DB.
  • Dostęp do Raven Studio i API REST'owego.
  • Obiekty POCO jakie mogą zostać umieszczone w Raven DB.
  • Kod dodający/zmieniający/usuwający dokumenty.

19/01/2012

RavenDB (cz. 1)

Home

Raven DB to dokumentowa baza danych, przedstawiciel trendu NoSQL, opracowana przez Ayende i jego zespół. Całkiem niedawno bo w październiku 2011 Ayende był zresztą w Polsce i opowiadał o swoim dziecku. Kilka miesięcy temu w jednym ze swoich pet project postanowiłem zmienić technologię dostępu do danych i wybór padł właśnie na Raven DB.

Kilka słów wstępu

Projekt ten to program LanguageTrainer wspomagający naukę słówek. Pomysł jego napisania narodził się ponad rok temu kiedy "ponownie" rozpocząłem naukę języka niemieckiego. W sieci nie znalazłem programu, który spełniałby moje oczekiwania. Zdecydowałem więc, że napiszę coś szytego na miarę. Początkowo rozpoczęło się niewinnie: listę słówek i ich tłumaczeń trzymałem w pliku. Dokładniej mówiąc był to po prostu wynik serializacji XML'owej. Podejście to przestało się sprawdzać kiedy do głowy zaczęły mi przychodzić kolejne pomysły: A może dodać bardziej zaawansowane wyszukiwanie, jakieś statystyki itd.

Początkowo pomyślałem o przejściu na bazę relacyjną, ale stwierdziłem, że tutaj nie nauczę się niczego nowego. Pomyślałem, że fajnie będzie wypróbować coś zupełnie nowego, z czym nie miałem jeszcze do czynienia, a ponieważ wcześniej czytałem trochę o Raven DB i wiedziałem, że ma .NET'owe API, wybrałem właśnie ją.

Z bazą tą już trochę pracuję, z braku czasu niezbyt intensywnie, ale zebrałem już trochę doświadczeń i postanowiłem sie nimi podzielić. Planuję serię postów, w których opiszę jak pracuje się z Raven DB na przykładzie swojego pet project. Nie będzie to jednak typowy tutorial omawiający wszystkie zagadnienia. Poruszę tylko te, z którymi miałem okazję się zapoznać.

Zaczynamy

LanguageTrainer to typowa aplikacja grubego klienta (WPF + MVVM). Nie chciałem jednak uzależniać się od połączenia z serwerem. Dlatego zdecydowałem się na hostowanie Raven DB w procesie aplikacji. Po ściągnięciu paczki z binariami na swoje potrzeby skopiowałem więc katalog EmbeddedClient. W tym momencie zaznaczę jeszcze, że warstwę dostępu do danych mam ukrytą za dobrze zdefiniowanym interfejsem, a więc aplikacja nie wie z jakim źródłem danych pracuje. Poniżej fragment kodu z klasy implementującej ten interfejs, odpowiedzialny za zainicjowanie Raven DB.

public EmbeddableDocumentStore Store { get; private set; }

...

public void Init(string dir)
{
 Store = new EmbeddableDocumentStore
 {
  DataDirectory = dir,
  UseEmbeddedHttpServer = true,
 };

 Store.Initialize();
}
 


Instancja EmbeddableDocumentStore posłuży nam później do otwierania sesji pracy z Raven DB, wykonywania zapytań itd. W kodzie tym ustawiam tylko dwie właściwości. DataDirectory wskazuje katalog roboczy, w którym Raven DB utworzy odpowiednią strukturę katalogów i gdzie będzie trzymał dane. Za pierwszym razem inicjalizacja zajmie więc trochę więcej czasu.

Istotna jest też właściwość UseEmbeddedHttpServer. Dzięki ustawieniu jej na true mam dostęp do napisanej w Silverlight aplikacji Raven Studio do zarządzania dokumentami oraz, co mniej ważne z mojej perspektywy, dostęp REST'owy do dokumentów. Oczywiście ponieważ Raven DB jest hostowany przez mój program, aplikacja Raven Studio będzie dostępna tylko wtedy, kiedy uruchomiony jest LanguageTrainer.

Numer portu z jakiego będzie korzystał serwer HTTP ustawiamy w pliku konfiguracyjnym aplikacji dodając następujący wpis:
<appSettings>
    <add key="Raven/Port" value="8888" />
</appSettings>

To jednak nie wszystko. Jeśli w tym momencie spróbujemy uruchomić Raven Studio otrzymamy od serwera HTTP taki błąd:

Could not find file Raven.Studio.xap, which contains the Raven DB Studio functionality. Please copy the Raven.Studio.xap file to the base directory of RavenDB and try again.

Plik Raven.Studio.xap znajdziemy w dystrybucji Raven DB i musimy umieścić go w katalogu z jakiego uruchamiamy aplikację. Ja zrobiłem to w ten sposób, że dodałem go do projektu, Build Action ustawiłem na Content, a Copy to Output Directory na Copy if newer. Testowałem w przeglądarkach Chrome, Firefox oraz IE i działa.

Podsumowanie

Tyle na dzisiaj. Podsumujmy co już mamy/umiemy:
  • Bazę Raven DB osadzoną w aplikacji hostującej.
  • Kod inicjalizujący Raven DB.
  • Dostęp do Raven Studio i API REST'owego.
W następnej części opiszę jak wykonywać podstawowe operacji na dokumentach.