RavenDB i C# - Czyli NoSQL w świecie .net
22.08.2013 20:55
Obecnie jestem na stażu w pewnej firmie programistycznej w Warszawie i odbyłem już prawie dwumiesięczny kawał ciekawej praktyki. Jednym z zadań, które otrzymałem było zapoznanie się z możliwościami bazy danych NoSQL – RavenDB - którą być może (ale nie na pewno) wykorzystam w pracy z kolejnymi (i niekoniecznie firmowymi) projektami.
Zagadnienie RavenDB na tyle mnie zainteresowało, że postanowiłem napisać ten tekst, a ponadto piszę go też z powodu małej liczby polskich wpisów w Internecie traktujących na tak ciekawy temat. Najpierw chciałbym zrobić szybki rys teoretyczny – którego brakowało mi, gdy dopiero poznawałem RavenDB, a następnie spróbuję to poprzeć przykładami i przykładowym programem do pobrania na końcu wpisu. Mam nadzieję, że coś takiego pomoże innym zainteresowanym szybciej poznać podstawy działania tej ciekawej bazy danych. Wpis ten zatem można potraktować jako nieoficjalny "Quick start guide".
Z góry powiem, że dla programisty dostępne są dwa API: API http z którego można korzystać z poziomu dowolnego języka i wygodne api .net-owe na przykład dla programistów języka C#.
O co chodzi z NoSQL? – rys teoretyczny
Jak sama nazwa wskazuje, w bazach NoSQL rezygnujemy z licznych relacji, tabel oraz kluczy w bazie danych. Zamiast tego w RavenDB posługujemy się dokumentami zgromadzonymi w jednym miejscu na wzór jednej tabeli. Dokument jest to dokładna reprezentacja obiektu przekazanego z programu w naszej nierelacyjnej bazie danych w formacie JSON.
Jak zatem rozwiązujemy kwestię relacji i logicznego powiązania wielu różnych obiektów? Odpowiedzią na to pytanie są wszelkiego rodzaju kolekcje umieszczone jako propercje naszego obiektu.
Skoro mamy o czynienia ze zwykłymi dokumentami JSON, to możemy wszystkie hierarchicznie reprezentować. Np.: obiekt szkoła może mieć w sobie kolekcję nauczycieli i kolekcję uczniów. Baza danych za nas to spokojnie obsłuży - Przy próbie zapisu "łyknie" dowolny obiekt, który jest również dowolnego typu.
Rozwiązanie to jest bardzo skalowalne. Założeniem twórcy było to, aby serwer bazy danych miał możliwie jak najdłuższy uptime – wszystkie operacje możliwie powinny być możliwe łącznie z przebudową struktury bazy bez downtime’u naszej aplikacji. Warto wspomnieć, że wspierany jest również Sharding, czyli rozbicie danych pomiędzy wieloma serwerami, choć z punktu widzenia zewnętrznego jest to widziane jako jedna baza danych.
Dodatkowo jednym z założeń twórcy było podejście, że lepiej aby klient otrzymał jakiekolwiek dane i szybciej, aniżeli dłużej i aktualne. Dlatego pomimo skalowalności to na programiście spoczywa obowiązek zadbania o spójność danych i w niektórych projektach może postawić pod znakiem zapytania użycie RavenDB.
Jak to działa w praktyce? Prosto! W RavenDB możemy przetrzymywać obiekty różnego typu (zwane dokumentami) i nie „zmieszają” się. Nie definiujemy w bazie żadnych kolumn, ponieważ generują się one same na podstawie wstawianego do niej obiektu. Działa to również w drugą stronę: Wypełnione są tylko pola tej klasy, którą „wybieramy” z bazy danych. Pozostaje jeszcze kwestia manipulacji danymi. Skoro rozwiązanie jest NoSQL-owe, to i nie ma starej dobrej składni SQLa. Na pomoc przychodzi programistom C# sprawdzone, dobre i - przede wszystkim - dobrze znane LINQ i Lucene Syntax, które dla większości osób może okazać się nowością.
Twórcy RavenDB zintegrowali z silnikiem bazy danych mechanizm odpytywania Lucene - który niezależnie od bazy danych może zostać zaimplementowany w dowolnych .netowych projektach.
Jak to wygląda w praktyce? Przekonajmy się!
Rys praktyczny - podstawy RavenDB
Wszystko potrzebne do pracy z RavenDB jest dostępne do pobrania pod adresem http://ravendb.net/
Rozpakowane archiwum zawiera plik wsadowy Start.cmd, który startuje serwer developerski.
Wywołane okno konsoli pozwala na dodatkowy debugging naszej aplikacji po stronie bazy danych, ponieważ wyświetla wszstkie przychodzące do serwera requesty i liczbę zwróconych wyników, a także operacje które niekoniecznie zwracają dokumenty z bazy danych. Dodatkowo wraz z wywołaniem tego okna konsoli, w przeglądarce automatycznie zostaje wywołany webowy interfejs do zarządzania bazami danych.
Skoro działa już bazodanowe serce naszych przyszłych aplikacji, to spróbujmy połączyć się z poziomu C# z Ravenem i dodać tam jakieś dane. W projekcie C# wystarczy za pomocą NuGeta pobrać API do łączenia się z RavenDB lub załączyć pliki dll dołączone do paczki pobranej ze strony RavenDB.
Potem już tylko podajemy odpowiednie referencje w kodzie:
[code=C#]using Raven.Client; using Raven.Client.Document;[/code]
Powyższe dwie linijki importują nam dwie przestrzenie nazw. Pierwsza służy ściśle do utworzenia obiektów potrzebnych do połączenia z bazą danych. Druga przestrzeń nazw udostępnia nam zestaw klas przydatnych w manipulacji danymi - dokumentami. Spróbujmy więc użyć tych klas do zapisania czegoś w bazie danych. Naturalnie, w pliku app.config dodaję connection string dla RavenDB:
[code=XML]<add name="Server" connectionString="Url = http://localhost:8080;Database=test"/>[/code]
Kolejnym krokiem prowadzącym nas do połączenia się z bazą jest stworzenie obiektu klasy DocumentStore i wywołanie metody Initialize:
[code=C#]IDocumentStore store = new DocumentStore { ConnectionStringName = "Server" }; store.Initialize();[/code]
Użyłem do tego celu interfejsu IDocumentStore, ponieważ może on być również obsłużony przez shardowane rozwiązane - w którym kilka serwerów jestwidziane jako jeden DocumentStore.
Do wszelkich operacji na bazie danych otwieramy zaś sesję. Na końcu sesji zapisywane są dane w bazie metodą SaveChanges. Ważną rzeczą jest aby pamiętać, że sami twórcy RavenDB nie zalecają używania więcej niż jednej sesji/program. Wymusza to nas odpowiednie okodowania komunikacji z bazą danych, ale ma to też zazwczaj dobre odbicie na jakości naszego kodu ;)
[code=C#]using (IDocumentSession session = store.OpenSession()) { //Tutaj się dzieje magia, a później zostaje ona zapisana :) session.SaveChanges(); }[/code]
Skoro mamy wszystko, to zdefiniujmy obiekt do zapisu w bazie:
[code=C#]class Person { public string name; public int age; public string nickname; }[/code]
I wreszcie zapis przykładowych danych przy użyciu naszej wcześniej otwartej sesji:
[code=C#]using (IDocumentSession session = store.OpenSession()) { Person jan = new Person { age = 20, name = "Jan Kowalski", nickname = "Janek16" };
Person pawel = new Person { age = 25, name = "Paweł Nowak", nickname = "pablo2" };
session.Store(jan); session.Store(pawel); session.SaveChanges(); } }[/code]
Powyższy przykład świetnie pokazuje jak łatwo można zapisywać dane przy pomocy API RavenDB. I uwierzcie mi, łyknie każdy obiekt - nawet z mnóstwem kolekcji zagnieżdżonych na różnych poziomach :)
Warto równocześnie spojrzeć co na to wszystko powiedział log naszego serwera bazy:
Skoro tak prosto poszło z zapisem, spróbujmy analogicznie odczytu. Pierwszą możliwością jest użycie Linq:
[code=C#]var results = from company in session.Query<Company>() where company.Name == "Hibernating Rhinos" select company;[/code]
Skoro Linq jest raczej dobrze znane (i lubiane), ja przedstawię za to zastosowania mechanizmu Lucene w RavenDB do wybrania danych.
[code=C#]using (IDocumentSession session = store.OpenSession()) { var results = session.Advanced .LuceneQuery<Person>() .Where("name:*wa*").ToList();
foreach (Person p in results) { Console.WriteLine(p.name); } }[/code]
Posługując się składnią podaną w linku na początku wpisu wybrałem wszystkie elementy, które w propercji Name zawierają w sobie podciąg "wa". Zatem zobaczmy wyniki programu i log:
Podsumowanie
To co przedstawiłem w tym wpisie to tylko wierzchołek góry lodowej. RavenDB pozwala na znaczenie więcej, w tym wspomniany już sharding, wyszukiwanie pełnotekstowe oraz bardzo zaawansowane odpytania.
Mam nadzieję, że mój wpis na temat nierelacyjnej bazy danych dla .net okaże się dla niektórych osób pomocny.
Jeżeli gdzieś popełniłem błąd merytoryczny - przepraszam :) Chętnie go poprawię.