JAVA exPress > Archive > Issue 8 (2010-09-03) > CouchDB - bo dane to nie zawsze tabele

CouchDB - bo dane to nie zawsze tabele

CouchDB jest nowym rodzajem bazy danych. Nie jest to baza relacyjna, ani też obiektowa. CouchDB przechowuje dokumenty. Dokument jest czymś w rodzaju znanej z Javy kolekcji Map, ma klucze i wartości. Kluczem zawsze jest String (oczywiście unikalny w ramach jednego dokumentu), co do wartości możliwości jest znacznie więcej, o czym dalej. Kolejną ciekawą cechą CouchDB jest to, że dokumenty te są dostępne przez RESTowy interfejs i protokół HTTP, zapisane w JSONie, znanym JavaScriptowcom formacie zapisu złożonych danych (a więc obiektów). Językiem jaki został użyty do napisania CouchDB jest Erlang, autorzy wybrali go bo świetnie nadaje się do pisania aplikacji wielowątkowych, więc CouchDB jest bardzo dobrze skalowalna na wiele procesorów/rdzeni.

Instalacja

Na stronie http://couchdb.apache.org dostępny jest plik .tar.gz z bazą, dodatkowo w Ubuntu dostępny jest pakiet couchdb. W trakcie instalacji pakietu, utworzony zostaje użytkownik couchdb. Aby odpalić bazę z tym użytkownikiem:

    sudo -i -u couchdb couchdb -b

Wraz z bazą uruchamia się serwer HTTP z narzędziem administracyjnym pomocnym przy wykonywaniu operacji. Otwarcie w przeglądarce strony http://localhost:5984/ powinno skutkować pojawieniem się napisu powitalnego: {"couchdb":"Welcome","version":"0.10.0"}. Wspomniane narzędzie znajduje się pod adresem http://localhost:5984/_utils/ Nazywa się Futon. Pozwala na tworzenie i usuwanie baz danych, rekordów (dokumentów) oraz ich modyfikację.

Dokumenty

Futon jest dość intuicyjny, dlatego nie będę opisywał w jaki sposób utworzyć lub skasować bazę danych, ale przejdę od razu do opisu dokumentu. Każdy dokument w CouchDB ma id w polu o nazwie _id. Domyślnie ID są generowane jako UUID, jednak nic nie stoi na przeszkodzie, żeby wpisać inne ID podczas tworzenia dokumentu. Pamiętajmy jednak, że raz nadanego ID nie można później zmienić, więc w polu tym nie powinny być trzymane żadne informacje. Innymi słowy pole ID powinno być używane tylko do jednoznacznego określenia dokumentu, i do niczego innego.

Drugim polem którego nie można edytować po utworzeniu dokumentu jest _rev. Jak nietrudno się domyślić, oznacza ono rewizję. Każda zmiana dokumentu w CouchDB jest zapamiętywana, tzn. możemy zawsze dostać się do dokumentu sprzed zmiany, właśnie przez rewizję.

Rys. 1. Najprostszy możliwy dokument, zawierający tylko pola _id i _rev

Oczywiście do dokumentu możemy dodać też dowolną ilość innych pól. Dodawać można Stringi, liczby, listy, mapy a nawet binarne załączniki. Możliwości jest więc sporo. Wszystko to jest dość proste i intuicyjne w Futonie, trzeba tylko wiedzieć jaki format mają poszczególne typy danych.

typ danych format (przykład)
String "artykuł"
liczba 20
tablica [ "kot", "pies", "sikorka" ]
mapa { "imię": "Paweł", "nazwisko": "Stawicki" }

Dokumenty można tworzyć, przeglądać i edytować przez Futona, ale jak wspomniałem na wstępie, CouchDB posiada RESTowy interfejs HTTP. Aby go użyć, wykorzystamy narzędzie curl - w Ubuntu dostępne jako pakiet. Za pomocą curla możemy wysyłać komendy HTTP. Np:

    curl http://localhost:5984

Powinno pokazać nam znajomy tekst powitalny. 

    {"couchdb":"Welcome","version":"0.10.0"}

Jeśli mamy bazę danych o nazwie tryme, komenda

    curl http://localhost:5984/tryme

pokaże nam informacje o niej:

    {
        "db_name":"tryme",
        "doc_count":9,
        "doc_del_count":0,
        "update_seq":54,
        "purge_seq":0,
        "compact_running":false,
        "disk_size":106585,
        "instance_start_time":"1265675480690743",
        "disk_format_version":4
    }

Możemy też sprawdzić, jakie bazy są dostępne:

    curl http://localhost:5984/_all_dbs

    ["tryme","loads","training"]

lub ściągnąć konkretny dokument:

    curl http://localhost:5984/tryme/61fe9c6c226b978f74b76329191806b3

    {
        "_id":"61fe9c6c226b978f74b76329191806b3",
        "_rev":"3-f1fa64bf24600ba2ab5ad508f5bc1ab6",
        "firstname":"Leszek",
        "surname":"Kowalski",
        "type":"person",
        "position":"developer",
        "salary":3000
    }

To wszystko robimy poleceniem GET, zgodnie z konwencją REST. Możemy też oczywiście użyć polecenia PUT, jak nietrudno się domyślić, w celu utworzenia dokumentu lub (jeśli mamy uprawnienia administratora) bazy danych:

    curl -X PUT http://localhost:5984/tryme/id1 -d '{ "firstname": "Leszek", "surname": "Gruchała" }'

    {
        "ok":true,
        "id":"id1",
        "rev":"1-12f229b1e1754562785b1caa6b52a819"
    }

Utworzyliśmy właśnie rekord z dwoma polami. Jego _id to "id1". Tworząc dokumenty za pomocą Futona, nie musimy podawać ID, Futon potrafi wygenerować je za nas (a właściwie pobrać z CouchDB). W przypadku korzystania z interfejsu HTTP, musimy _id podać explicite. Nie jesteśmy jednak zmuszeni do generowania go własnymi siłami. Możemy pobrać identyfikatory z CouchDB:

    curl http://localhost:5984/_uuids

    {"uuids":["e0e8bb72f42f6359f9210fa4a0bb0136"]}

Możemy też pobrać (wygenerować) więcej ID na raz:

    curl http://localhost:5984/_uuids?count=3

    {
        "uuids":["db9649f84b61cc57eb880dd34e8a0fc8",
                "db2945c01d421e37af230544e4b10dcc",
                "864c0c5ab116b95dd55222ab8b5bca61"]
    }

Identyfikatory te są unikalne. Jeśli ktoś inny w tym samym czasie pobierze identyfikator, dostanie inny niż my.

Widok przez map/reduce

JSON pozwala na zapisanie w dokumencie różnych typów danych, w tym także... funkcji. W CouchDB jest specjalny rodzaj dokumentów nazwany design documents. W takim dokumencie możemy definiować widoki właśnie za pomocą dwóch funkcji na wzór znanego programującym w językach funkcyjnych paradygmatu map/reduce. Do utworzenia widoku wystarczy funkcja map, reduce jest konieczne do wyliczania pewnych sumarycznych wartości dla wielu dokumentów. (Może nie brzmi to zbyt jasno, ale myślę że rozjaśni się po przykładzie). Na początek zajmiemy się właśnie samą funkcją map. Jej jedynym parametrem jest dokument. W funkcji tej wołamy inną funkcję, emit, do której przekazujemy klucz i wartość. I właśnie to co wyemitujemy, znajdzie się w widoku. Myślę, że nie ma co zwlekać i najwyższy czas na przykład. Załóżmy, że mamy bazę danych z trzema prostymi dokumentami:

    firstname: Paweł
    surname: Stawicki
    position: developer
    salary: 1000

    firstname: Leszek
    surname: Gruchała
    position: developer
    salary: 2000

    firstname: Ryszard
    surname: Stawicki
    position: manager
    salary: 2500

Załóżmy, że chcemy znaleźć wszystkich developerów, i zobaczyć ich dane, funkcja map będzie wyglądała tak:

    function(doc) {
      if (position == "developer") {
        emit(doc._id, doc);
      }
    }

Możemy też zrobić to inaczej, o ile nie potrzebujemy żeby kluczem było _id. Jeśli kluczem będzie position, możemy ograniczyć zwracany wynik właśnie do określonych kluczy. W tym celu jednak nasz widok trzeba zapisać pod określoną nazwą w design document. (Jest to analogiczne do tworzenia widoków w bazach SQLowych) W Futonie tworzymy Temporary view..., z następującą funkcją map:

    function(doc) {
      emit(doc.position, doc);
    }

Widok ten zapisujemy (Save As...), podając nazwę design documentu i widoku. Załóżmy, że zapisaliśmy ten widok w design documencie o nazwie it, zaś sam widok nazwany został by-position. Dostać się do niego możemy przez następujący URL:

    http://localhost:5984/tryme/_design/it/_view/by-position
    {"total_rows":3,
     "offset":0,
     "rows":[ 
      {"id":"61fe9c6c226b978f74b76329191806b3",
       "key":"developer",
       "value":
       {"_id":"61fe9c6c226b978f74b76329191806b3",
        "_rev":"4-7d0ecd6ca4c65ee368fc88e51fa6a3b5",
        "firstname":"Leszek",
        "surname":"Grucha\u0142a",
        "type":"person",
        "position":"developer",
        "salary":2000
       }
      }, 
      {"id":"eb3873f48bb581df13762324b8ec0313",
       "key":"developer",
       "value":
       {"_id":"eb3873f48bb581df13762324b8ec0313",
        "_rev":"6-b2c4d3dd7da321d286b16662ecb1f082",
        "firstname":"Pawe\u0142",
        "surname":"Stawicki",
        "type":"person",
        "position":"developer",
        "salary":1000
       }
      }, 
      {"id":"1",
       "key":"manager",
       "value":
       {"_id":"1",
        "_rev":"9-e3a554d649e755e1bed0d10537971e9b",
        "surname":"Stawicki",
        "firstname":"Ryszard",
        "type":"person",
        "position":"manager",
        "salary":2500
       }
      }
     ]}

Teraz mamy stanowisko jako klucz. Jeśli chcemy wyświetlić samych developerów, dostępni będą pod adresem:

    http://localhost:5984/tryme/_design/it/_view/by-position?key="developer"
    {"total_rows":3,
     "offset":0,
     "rows":[ 
      {"id":"61fe9c6c226b978f74b76329191806b3",
       "key":"developer",
       "value":
       {"_id":"61fe9c6c226b978f74b76329191806b3",
        "_rev":"4-7d0ecd6ca4c65ee368fc88e51fa6a3b5",
        "firstname":"Leszek",
        "surname":"Grucha\u0142a",
        "type":"person",
        "position":"developer",
        "salary":2000
       }
      }, 
      {"id":"eb3873f48bb581df13762324b8ec0313",
       "key":"developer",
       "value":
       {"_id":"eb3873f48bb581df13762324b8ec0313",
        "_rev":"6-b2c4d3dd7da321d286b16662ecb1f082",
        "firstname":"Pawe\u0142",
        "surname":"Stawicki",
        "type":"person",
        "position":"developer",
        "salary":1000
       }
      }
     ]}

Powiedzmy, że w bazie danych mamy wielu developerów, managerów, administratorów i innych pracowników IT oraz ich wynagrodzenia. Aby poznać łączne wynagrodzenie dla każdej grupy zawodowej musimy wykorzystać funkcję reduce. Funkcja ta przyjmuje 3 parametry: klucze, wartości i parametr typu boolean rereduce. Z reduce zwracamy wynik, który powinien być skalarem, najczęściej liczbą. Czyli dla określonych kluczy, zwracamy liczbę, wyliczoną na podstawie wartości związanych z tymi kluczami. Klucze i wartości które trafiają do reduce, to te same klucze i wartości, które wyemitowaliśmy z map. Jest jeszcze trzeci parametr, rereduce, ale o tym później.

Nasza funkcja reduce do badania wynagrodzeń w poszczególnych grupach, powinna wyglądać tak:

    function(keys, values, rereduce) {
      var salaries = 0;
      for(i = 0; i < values.length; i++) {
        salaries += values[i].salary;
      }
      return salaries;
    }

Za pomocą takiej funkcji reduce możemy też obliczyć łączne wynagrodzenia wszystkich pracowników. Jeśli dodamy ją do widoku it, i zapiszemy go pod nową nazwą salaries, będzie można się do niego dostać przez url:

    http://localhost:5984/tryme/_design/it/_view/salaries?group_level=1
    {"rows":[ 
     {"key":"developer", "value":3000}, 
     {"key":"manager", "value":2500}
    ]}

A cóż to za parametr group_level=1? Oznacza on, na jakim poziomie grupować klucze. W tym wypadku mamy tylko jeden poziom kluczy, dodamy zatem naszym pracownikom działu IT jeszcze projekty. Dodajmy pole project z wartością dla Pawła "travel", dla Leszka "houses", i dla Ryszarda też "houses". Dodajmy jeszcze jednego pracownika do projektu "houses":

    firstname: Maciej
    surname: Majewski
    position: developer
    project: houses
    salary: 1800

Teraz zmodyfikujemy nieco funkcję map tak, żeby emitować klucz będący tablicą:

    emit([doc.position, doc.project], doc);

Klucz jest teraz dwuelementową tablicą, ma dwa poziomy. I możemy go według tych poziomów grupować. Najlepiej zilustrować to na przykładzie. Wywołanie widoku z group_level=2 daje następujący wynik:

    {"rows":[
     {"key":["developer","houses"],"value":3800}, 
     {"key":["developer","travel"],"value":1000}, 
     {"key":["manager","houses"],"value":2500} 
    ]}

A to wynik dla group_level=1:

    {"rows":[
     {"key":["developer"],"value":4800},
     {"key":["manager"],"value":2500} 
    ]}

A co się stanie jak pominiemy parametr group_level? Dostaniemy wynik zredukowany do zerowego poziomu, czyli łączne wynagrodzenie wszystkich pracowników.

    {"rows":[ 
     {"key":null,"value":7300}
    ]}

Widać zatem, że wynik zwracany z reduce odnosi się do grupy kluczy, i to zgrupowanych na różnych poziomach. A co robi parametr rereduce? Tak naprawdę, nasza funkcja reduce zawiera błąd i nie zadziałałaby dla dużej liczby dokumentów. To z tego powodu, że może być ona wywoływana rekurencyjnie, to znaczy najpierw przekazywana jest do niej jakaś grupa dokumentów, a potem jej wynik znowu przekazywany jest do reduce. Rereduce ma wartość false w pierwszym wypadku, a w kolejnych wywołaniach true. Teoretycznie mogłoby się tak stać w przypadku tutaj rozpatrywanym kiedy klucze są zgrupowane (group_level=1). Kolejne wywołania reduce mogłyby wyglądać następująco:

  • reduce (['developer'], <cały dokument Paweł>, false) => 1000
  • reduce (['developer'], <cały dokument Leszek>, false) => 2000
  • reduce (['developer'], [1000, 2000], true) => 3000

W pierwszym i drugim przypadku do reduce jako parametr values trafiają dokumenty, ale w trzecim przypadku trafiają tam wyniki wcześniejszych wywołań. Nie są to dokumenty ale tablica liczb, więc trzeba ją inaczej obsłużyć. Poprawna funkcja reduce powinna wyglądać tak:

    function(keys, values, rereduce) {
      var salaries = 0;
      if (rereduce) {
        salaries += sum(values);
      } else {
        for(i = 0; i < values.length; i++) {
          salaries += values[i].salary;
        }
      }
      return salaries;
    }

Oprócz group_level, do dyspozycji mamy jeszcze między innymi parametry:

  • startkey - klucz pierwszego dokumentu w widoku.
  • endkey - analogicznie, klucz ostatniego dokumentu. Przy pomocy tych dwóch parametrów możemy ograniczyć liczbę zwracanych w widoku danych.
  • key - za pomocą tego parametru pobierzemy tylko jeden dokument, o podanym kluczu.
  • group=true - w ten sposób możemy ustawić group_level na maksimum.
  • revs_info=true - wyświetla informacje o rewizjach dokumentu. Ma zastosowanie tylko kiedy pobieramy pojedynczy dokument (parametr key).

Design documents

Design documents służą do czegoś więcej niż tylko przechowywanie widoków map/reduce. Dzięki design documents możemy pobierane z bazy obiekty JSON "otoczyć" odpowiednim HTMLem tak, żeby zamiast kodu JavaScript zobaczyć pod adresem obiektu miłą dla oka stronę. Dobry przykład można znaleźć pod adresem  http://caprazzi.net:5984/fortytwo/_design/fortytwo/index.html.  Jest to w pełni funkcjonalna aplikacja webowa, w której jedynym serwerem jest serwer CouchDB. W design documencie, oprócz widoków, mogą być funkcje walidujące zapisywane dokumenty, dokonujące dodatkowych operacji na dokumencie zapisywanym do bazy, formatujące dokument do miłego dla oka HTMLa. Zajmiemy się nimi wszystkimi po kolei.

Walidacja

W design documencie możemy zdefiniować funkcję walidującą. Nie jest ona konieczna. Jeśli jej nie będzie, każdy dokument będzie uznawany za poprawny. Design documentów może być więcej, a w każdym z nich może być funkcja walidująca. Jeśli tak jest, dokument musi być zaakceptowany przez wszystkie z nich. Funkcja taka ma nazwę validate_doc_update, i następującą deklarację:

    function(newDoc, oldDoc, userCtx) {}

Funkcja ta nie zwraca żadnej wartości. Jeśli uznamy dokument za niepoprawny, powinniśmy rzucić wyjątek:

    throw({forbidden : "Dokument niepoprawny"});

Jeśli dokument próbuje zapisać użytkownik, któremu nie chcemy na to pozwolić ze względu na autoryzację, możemy też rzucić:

    throw({unauthorized : "Dokument niepoprawny"});

W tym drugim przypadku baza poprosi użytkownika o nazwę i hasło.

Zajmijmy się teraz parametrami funkcji. Pierwszych dwóch nietrudno się domyślić, jest to nowa i stara wersja dokumentu. Jeśli funkcja wywoływana jest dla nowego dokumentu, którego jeszcze nie ma w bazie, oldDoc przyjmuje wartość null. userCtx to obiekt zawierający dane dotyczące użytkownika, np. userCtx.name.

"Dekorowanie" dokumentu

Czasem dobrze by było przesłać nie tyle dokument w formacie JSON, ale np. część dokumentu, w XMLu, CSV lub innym formacie tekstowym. CouchDB i to nam umożliwia, udostępniając funkcję show, dzięki której możemy określić co ma być zwracane zamiast "surowego" JSONa. Parametry funkcji to dokument i request:

    function(doc, req) {}
    Funkcja ta może zwracać zwykły tekst, wtedy jest on ładowany jako treść odpowiedzi, np:
    function(doc, req) { 
      return '<h1>' + doc.name + '</h1><p>' + doc.bio + '</p>';
    }

Możemy też określić inne parametry, np. nagłówki

    function(doc, req) {
      return {
        body : '<h1>' + doc.title + '</h1>', 
        headers : { 
          "Content-Type" : "application/xml", 
          "X-My-Own-Header": "you can set your own headers"
        }
      }
    }

Z założenia funkcja show dla takich samych parametrów, zwraca zawsze takie same wyniki, dzięki czemu można je cache'ować, co może znacznie przyspieszyć działanie bazy.

W jednym design documencie może być więcej funkcji show, różniących się nazwami.

Rys. 2. Wiele funkcji show w jednym design documencie.

Pobrać dokument "udekorowany" przez taką funkcję można za pomocą następującego URLa:

    http://localhost:5984/tryme/_design/it/_show/show_id/133fec564f0b68f30a7b7d63fe8235d2

Gdzie _design/it określa design document, a _show/show_id  funkcję o nazwie show_id, zaś 133fec564f0b68f30a7b7d63fe8235d2 to ID dokumentu, który chcemy sobie obejrzeć.

Ciekawym rozwiązaniem jest to, że funkcję show można także wywołać bez ID dokumentu. Wtedy na miejsce argumentu doc trafia do funkcji null. Drugim parametrem funkcji show jest request, z niego możemy wyciągnąć parametry przekazane w requeście HTTP. Robi się to w sposób najprostszy z możliwych:

    req.param1

Wyciągnie nam parametr o nazwie param1.

CouchDB implementuje też mechanizm template'ów, i można ich używać w funkcjach show. Możemy trzymać w osobnych plikach wzór HTMLa jakim chcielibyśmy otoczyć dokument i użyć ich w naszej funkcji. Dokładny opis korzystania z tego mechanizmu wykracza jednak poza zakres tego artykułu, odsyłam do książki online (http://books.couchdb.org/relax/)

"Dekorowanie" widoku

Innym rodzajem funkcji w CouchDB jest funkcja list. Pełni ona podobne zadanie co show, ale nie dla pojedynczego dokumentu, ale całego widoku. Przyjmuje dwa parametry:

    function(head, req) {
      while(getRow()) {
        send(...)
      }
    }

head zawiera informacje potrzebne do page'ingu, czyli ile dokumentów (wierszy) pobrać oraz od którego zacząć.

req to natomiast wszelkie dane o requeście i nie tylko. Zawiera następujące parametry:

  • info - informacje o bazie (te same co pod URLem bazy danych, np. http://localhost:5984/tryme/)
  • verb - GET lub POST
  • query - parametry dołączone do wywołania
  • path - ścieżka wywołania rozbita na części (separator: "/")
  • headers
  • cookie
  • body - przydatne jeśli wysłane było POSTem
  • form - to co w body, ale w formacie JSON
  • userCtx - informacje o użytkowniku, takie same jak przesłane do funkcji validate_doc_update
  • getRow() pobiera następny dokument z dekorowanego widoku (null po pobraniu wszystkich dokumentów), a send()  wysyła HTML do przeglądarki.

Na końcu funkcja list zwraca String, który również jest wysyłany do przeglądarki (może to być więc np. footer).

Aby udekorować sobie widok funkcją list, wysyła się żądanie pod następujący URL:

    http://localhost:5984/tryme/_design/it/_list/basic/info

Gdzie basic to nazwa funkcji list, a info to nazwa widoku.

Bezpieczeństwo

Bezpośrednio po instalacji, z bazą danych każdy może zrobić wszystko. Pod warunkiem, że pracuje na tej samej maszynie na której stoi baza. Możemy jednak udostępnić ją światu, jednocześnie tworząc użytkowników z prawami administratorów. Jeśli jest choć jeden admin, inni użytkownicy mają już znacznie ograniczone uprawnienia. Aby utworzyć konto administratora, należy wyedytować plik /etc/couchdb/local.ini, i w sekcji [admins] dodać:

    franek = haslo

Po uruchomieniu bazy, hasło zostanie zahashowane. Administratora można też dodać odpowiednim poleceniem HTTP:

    curl -X PUT http://localhost:5984/_config/admins/franek -d '"haslo"'

Jeśli administrator jest już dodany, następnych może dodawać tylko on.

Jeśli chcemy wykonać jakąś operację jako administrator, nazwa użytkownika i hasło muszą być dodane do URLa:

    curl http://franek:haslo@localhost:5984/tryme

Oczywiście nie jest to bezpieczne. Nazwa użytkownika i hasło, niezaszyfrowane są przekazywane przez sieć do serwera. Wystarczy podsłuchać...

Jeśli chcielibyśmy aby dostęp do naszej bazy danych mieli także użytkownicy z innych komputerów (nie tylko localhosta), trzeba jeszcze wyedytować bind_address w pliku /etc/couchdb/local.ini. Jeśli ustawimy tam wartość 0.0.0.0, do bazy będzie można się podłączyć z dowolnego komputera.

Obecnie rozwiązania bezpieczeństwa są intensywnie rozwijane w projekcie CouchDB, należy się więc spodziewać, że mechanizmy te zostaną znacznie ulepszone w niedalekiej przyszłości. Prawdopodobnie zwiększona zostanie elastyczność i będzie można ustawić jaki użytkownik do czego ma dostęp i co może z danym zasobem zrobić.

Replikacja

Kolejną zaletą CouchDB, o której dotąd nie wspominałem, jest łatwość replikacji. Za pomocą Futona można skopiować zawartość bazy danych do innej bazy praktycznie bez żadnego przygotowania. Replikację można robić zarówno z bazy lokalnej na zdalną, jak i ze zdalnej na lokalną. Nie ma tu żadnych ograniczeń.

Rys. 3. Replikacja za pomocą narzędzia "Futon"

Replikacja jest jednokierunkowa. To znaczy, że jeśli replikujemy z bazy A do bazy B, to wszystkie dokumenty które mamy w bazie A zostaną skopiowane do B, ale jeśli w B mieliśmy jakieś dokumenty których nie ma w bazie A, to nie zostaną one skopiowane do A, ani skasowane. Wynika z tego, że nie zawsze po replikacji obie bazy będą identyczne. Aby tak było, musielibyśmy zrobić dwie replikacje: A -> B i B -> A. Co ciekawe, jeśli jakiś dokument był na bazie źródłowej, ale został skasowany, przy replikacji zostanie on także skasowany na bazie docelowej.

Jeśli na bazie docelowej jest ustawione konto administratora, warto dodać jego nazwę/hasło do URLa. Inaczej dokumenty do utworzenia których uprawnienia ma tylko administrator (np. design document) nie zostaną skopiowane. W takim przypadku URL powinien wyglądać tak: http://franek:haslo@remote.com:5984/tryme

Replikację możemy przeprowadzać także lokalnie, tzn. replikować bazę danych na inną bazę danych na tym samym serwerze, tworząc w ten sposób kopię zapasową. Dzięki temu bez obaw można testować "ryzykowne" operacje, bo gdyby coś poszło nie tak, zawsze możemy łatwo i szybko przywrócić stan poprzedni.

Jedną z ciekawszych funkcji replikacji w CouchDB jest "replikacja ciągła" (continous replication). Po skopiowaniu danych CouchDB nadal nasłuchuje na zmiany na bazie źródłowej, i Kiedy takowe się pojawiają, wysyła je na bazę docelową. Nie wysyła jednak od razu, tylko w najbardziej dogodnym momencie. Dlatego nie można nigdy zakładać że jeśli mamy ciągłą replikację, to bazy zawsze będą takie same. Tym niemniej jeśli jesteśmy w stanie zaakceptować niewielkie opóźnienia w replikacji, i niewielkie różnice w danych, możliwość ta może być bardzo przydatna. Aby włączyć ciągłą replikację:

    curl -X POST http://127.0.0.1:5984/_replicate
        -d '{"source":"tryme", "target":"http://remote.com:5984/tryme", "continuous":true}'

CouchDB a Java

Do CouchDB powstało kilka javowych bibliotek. Jedną z nich jest CouchDB4J, jednak za jej pomocą nie udało mi się pobrać dokumentów z widoku. Być może moje dokumenty były specyficzne, jednak biblioteka nie jest rozwijana od czerwca 2009, więc prawdopodobne jest że nie będzie działać z nowymi wersjami CouchDB :(

JRelax

Druga biblioteka jaką wypróbowałem to JRelax. Tutaj bolączką jest brak dokumentacji, ale podglądająć źródła i dysponując IDE z podpowiadaniem da się to przeżyć. Tak wygląda pobranie wszystkich baz za pomocą tej biblioteki:

    DefaultResourceManager manager = new DefaultResourceManager("http://localhost:5984");
    List<String> dbs = manager.listDatabases();
    for(String dbName : dbs) {
        System.out.println(dbName);
    }

Dość zaskakujący był dla mnie brak metody typu listDocuments(String dbName), aby pobrać wszystkie dokumenty (ich id) z danej bazy, trzeba użyć widoku tymczasowego:

    ViewResult<String, Object> res = manager.executeTemporaryView("tryme",
            "function(doc) {emit(doc._id, null)}", null, String.class, Object.class);
    
    for(ViewResult.ViewResultRow<String, Object> row : res.getResultRows()) {
        System.out.println(row.getKey()); 
    }

Parametry metody executeTemporaryView to kolejno: baza danych, funkcja map, funkcja reduce, klasa klucza widoku, klasa wartości widoku. Typ klucza i wartości widoku musi być też podany jako typ generyczny ViewResult<K, V>, będącego rezultatem wywołania tej metody. Można tam wstawić odpowiednio przygotowane POJO, czy to zgodne z konwencją javowych beanów, czy też z annotacjami. Do mapowania JRelax używa Jacksona (parser JSONa), i na jego stronę odsyłam po szczegóły, na jakie klasy potrafi on JSONa przemapować (http://wiki.fasterxml.com/JacksonDataBinding, http://wiki.fasterxml.com/JacksonAnnotations).

Tak natomiast można zapisać dokument do bazy:

    Document doc = new Document("tryme", "2", "{\"firstname\": \"pawel\"}");
    manager.saveDocument(doc);

Nie znalazłem niestety możliwości podania nazwy i hasła użytkownika, więc JRelax można używać tylko do "otwartych" baz, lub tylko do odczytu. Nie znalazłem też możliwości wygenerowania id z bazy, ani przekazania parametrów startKey, endKey do wywołania widoku. Wygląda więc na to, że możliwości JRelax są dość ograniczone.

jcouchdb

Kolejną Javową biblioteką do CouchDB jest jcouchdb. Podobnie jak w przypadku JRelax, jest ona umieszczona w repozytorium Mavena, które wystarczy dodać do poma, aby Maven za nas załatwił wszystkie zależności. Pobieranie baz danych jest proste i wygodne:

    Server srv = new ServerImpl("localhost");                   
    List<String> databases = srv.listDatabases();              
    for(String dbName : databases) {                           
        System.out.println(dbName);                            
    }

Równie proste jest pobranie id wszystkich dokumentów w bazie:

    Database db = new Database("localhost", "tryme");
    ViewResult<Map> res = db.listDocuments(null, null);
    for(ValueRow<Map> doc : res.getRows()) {
        System.out.println(doc.getId());
    }

Metoda listDocuments przyjmuje dwa parametry, pierwszy z nich to obiekt Options, w którym możemy ustawić parametry takie jak group, startKey, endKey itd. Drugi parametr to JSONParser, co oznacza, że możemy przekazać własny parser, który zmapuje nam JSONa na nasze obiekty, a nie tylko na mapę parametrów. Właśnie, używając domyślnego parsera  możemy w dość prosty sposób odczytać dokument jako mapę:

    Map<String, Object> doc = db.getDocument(Map.class, "1");
    for(String paramName : doc.keySet()) {
        System.out.println(paramName + ": " + doc.get(paramName));
    }

Równie wygodnie można pobrać widok:

    ViewResult<Map> viewRes = db.queryView("it/by-position", Map.class, null, null);
    for(ValueRow<Map> row : viewRes.getRows()) {
        System.out.println(row.getKey() + ": " + row.getValue());
    }

Muszę przyznać, że właśnie ta biblioteka przypadła mi osobiście najbardziej do gustu.

Dlaczego CouchDB?

Rozważmy, dlaczego ktoś miałby używać nierelacyjnej bazy danych, takiej jak CouchDB, zamiast tradycyjnej, relacyjnej SQLowej. Pierwszy, dość oczywisty przykład zastosowania, to dane, które nie zawsze są takie same. Np. jeśli mamy bazę danych pacjentów, każdy ma przypisanego lekarza, ale tylko niektórzy położną, niektórzy mają przepisane leki, inni dietę, inni określone zabiegi itd. Oczywiście w bazie relacyjnej także dałoby się to osiągnąć, ale byłoby to bardziej skomplikowane, trzebaby użyć wielu tabel, obsłużyć powiązania pomiędzy nimi itd.

Inny argument przemawiający za CouchDB to elastyczność. Zwłaszcza na etapie developmentu schemat bazy danych często się zmienia. W CouchDB wprowadzanie takich zmian jest znacznie łatwiejsze - w każdej chwili możemy dopisać coś do dokumentu, nie trzeba martwić się o relacje pomiędzy tabelami.

Kolejnym argumentem przemawiającym na korzyść CouchDB jest prosty protokół HTTP/REST. Nie trzeba korzystać z żadnych sterowników (driverów) JDBC, wystarczy request HTTP żeby wykonywać operacje na bazie.

Przydatna może się także okazać funkcja wersjonowania. Przypadkowe usunięcie lub nadpisanie danych z dokumentu nie jest problemem, wystarczy przywrócić wcześniejszą wersję.

CouchDB ma też inne zalety, o mniejszym znaczeniu dla programistów pracujących z bazą danych, lecz ułatwiające życie administratorom. Jest to skalowalność i łatwa replikacja. Wszystko wskazuje na to, że kolejne procesory będą miały coraz więcej rdzeni. CouchDB nie powinna mieć problemu z wykorzystaniem dużej ich ilości. Możliwość ciągłej replikacji natomiast ułatwia zbudowanie klastra.

CouchDB, a także inne nierelacyjne bazy danych, stają się coraz bardziej popularne. Czas pokaże, czy zagoszczą na dobre na naszych serwerach.

Nobody has commented it yet.

Only logged in users can write comments

Developers World