csiszarattila.com / Rubysztán

CouchDB - szakítás a relációs adatbázisokkal

Bácsi László(lackac) blogbejegyzésében olvastam először a CoucDB-ről, és annyira megtetszett a koncepciója, hogy gondoltam Én is teszek egy mélyebb ismerkedést vele - mintegy kikapcsólódásként két záróvizsga tétel között.

A következő bejegyzést két részre bontottam, az elkövetkezőben a CouchDB koncepcióját ismerheted meg működés közben, példákon keresztül. Míg a cikk második része megmutatja hogyan használhatjuk a CoucDB-t Rubyban.

Koncepció - röviden

A CouchDB több szempontból sem mindennapi adatbázis kezelő. Mindenekelőtt szakít a hagyományos relációs adatbázisok koncepciójával: az adatainkat séma-mentes formában tárolhatjuk benne. A táblázatok helyett dokumentumaink vannak, amelyek mezőnév/attribútum/tulajdonság és a hozzájuk tartozó adatok formájában tárolják az információkat. Mivel nincsenek előre meghatározott sémáink az egyes dokumentumok bármilyen adatpárt felvehetnek.

Másrészt a CouchDB-vel a HTTP protokollon keresztült kommonikálhatunk az adatbázisunkkal! Ráadásul a HTTP protokoll használata és az URL-k kiosztása a REST elveire épül: az adatbázisaink benne a dokumentumaikkal lesznek az erőforrásaink, a HTTP metódusok pedig a művelet célját jelölik majd. De, hogy a dolgok ne legyenek ennyire egyszerűek: az adatok kinyerése és felvitele JSON formátumban történik.

Telepítés

Ezúttal nem sok kedvem volt manuálisan, forrásból telepíteni a sok függőség miatt és mivel a kipróbáláson volt a hangsúly, így a MacPorts-on keresztül tettem fel a CouchDB-t. Ehhez először érdemes ellenőrizni, hogy naprakész-e a csomagok(portok) listája:

sudo port selfupdate
Én erről megfeledkeztem, és egy korábbi (0.7.2-es) verzió települt fel, ami persze köszönő viszonyban sem állt a dokumentációban állítottakkal, sok dolog másként működött, ugyanis az új verzió óta (0.8.1) renteget változás történt.

Majd telepítsük a couchdb csomagját:

sudo port install couchdb
Itt máris tarthatunk egy kis szünetet, ugyanis az Erlang csomag telepítése eltarthat egy ideig.

Sikeres telepítés után indítsuk el a CouchDB szervert a következő paranccsal:

sudo couchdb

Elsőként érdemes egy böngésző segítségével a CouchDB webes kezelőfelületét kipróbálni a http://localhost:5984/_utils/index.html címen. Itt létrehozhatunk új adatbázisokat és dokumentumokat, valamint feltölthetjük őket adatokkal. Érdemes a Firebug segítségével követni a felhasználói felület parancsait, hogy lássuk a háttérben milyen HTTP kéréseket küld és miképp adja meg az adatokat JSON formátumban.

Futon - a CouchDB web alapú interfésze

Ismerkedés

Mielőtt Rubyban kezdenénk el használni a CouchDB-t érdemes a HTTP vagy REST API-n átrágni magunkat, még akkor is, ha tisztában vagyunk ezeknek az elveivel, van ugyanis pár elem, ami nagyban meghatározza az elküldendő adatok formátumát. Ehhez próbáljunk parancssorból kommunikálni az adatbázissal a telnet vagy a curl segítségével.

Először hozzunk létre egy adatbázist a PUT metódussal:

curl -i -X PUT http://localhost:5984/blog/
A curl i kapcsolójával kiírathatjuk a HTTP válasz fejlécét, az X el pedig a HTTP kérés metódusát adhatjuk meg.

Hasonló választ kell, hogy kapjunk:

HTTP/1.1 201 Created
Content-Type: text/plain;charset=utf-8

Dokumentumok kezelése

Most hogy létrehoztuk az adatbázist, adjunk hozzá egy dokumentumot, és helyezzünk el benne különféle adatokat.

curl -i -X PUT http://localhost:5984/blog/couchdb_cikk \
-d '{ "author":"Csiszar Attila", "title":"CouchDB cikk"}'

Ezután kérdezzük le a GET metódussal:

Ezúttal a telnet-et használtam mivel a curl valamiért nem akaródzott a HTTP válasz törzsét kiolvasni.
$ telnet localhost 5984

GET http://localhost:5984/blog/couchdb_cikk/ HTTP/1.0

HTTP/1.1 200 OK
Content-Type: text/plain;charset=utf-8
Etag: 2482599723

{"_id":"couchdb_cikk","_rev":"2482599723","author":"Csiszar Attila","title":"CouchDB cikk"}

Mint látjuk a CouchDB két speciális mezőt rendelt a dokumentumunkhoz: az idt, mint azonosítót amely felvette az erőforrásunk címét - ezt a kérésünkkel adtuk meg -, valamint hozzárendelt egy rev tulajdonságot is.

A _ jellel kezdődő tulajdonságok a CouchDB számára fentartott értékek, nem módosíthatjuk őket.

Az azonosítóknak adatbázis szinten egyedinek kell lenniük, hiszen ezt használjuk az URL-kben is - gyakorlatilag bármilyen karaktersorozatot felvehetnek. Ha nem szeretnénk az egyedi azonosítók megadásával bajlódni, használhatjuk a POST metódust dokumentumok létrehozására, mivel ez egyedi azonosítót fog rendelni a dokumentumhoz. Mivel POST metódus esetén nem konkrét erőforrásra, hanem erőforrás-kollekcióra szoktunk hivatkozni, a címünknek ezúttal az adatbázis elérését kell megadnunk:

curl -i -X POST http://localhost:5984/blog/ \
-d '{ "author":"Csiszar Attila", "title":"Másik CouchDB cikk"}'

A válasszal visszakapjuk a létrehozott dokumentum (erőforrás) azonosítóját:

HTTP/1.1 201 Created
Content-Type: text/plain;charset=utf-8

{"ok":true,"id":"19BF2F4BF267E8CCF120A2B3EB444C19","rev":"3269339116"}

A másik fontos mező, a rev érték: ez jelöli a dokumentum aktuális változatát. Ez még fontos lesz a változtatások felvitelekor, ugyanis azt meg kell adnunk az adataink között, lássuk:

curl -i -X PUT http://localhost:5984/blog/couchdb_cikk \
-d '{ "_rev":"2482599723", "body": "Ez hosszu cikk lesz..." }'

Ha a megadott azonosító nem egyezik az adatbázisban lévő dokumentum aktuális értékével, akkor a változtatások nem kerülnek érvényre és egy 409 Conflict választ kapunk vissza. A rev értéke megjelenik az Etag HTTP fejlécben is, így remekül használhatjuk konfliktuskezelésre.

A fentiekből látható, hogy a CouchDB REST APIjában a dokumentumaink - tehát az adataink elvi tárolói - lesznek az erőforrásaink, az elérésükhöz(URL) az azonosítóikat használjuk fel, és a megfelelő HTTP metódusokkal tudunk rajtuk műveleteket végezni.

Mellékletek

A CouchDB további érdekessége, hogy a dokumetumaink mellé lehetőségünk van mellékleteket csatolni, csakúgy, mint az e-mailek esetében. Ezt a speciális _attachments tulajdonsággal tehetjük meg és az adatoknak base64 enkódolásúaknak kell lenniük.

curl -i -X PUT http://localhost:5984/blog/couchdb_cikk \
-d '{ "_id":"couchdb_cikk", "_rev": "2812696990", "_attachments": { "csatolt.txt": { "content_type":"text\/plain", "data":"Q3NhdG9sdCBkb2t1bWVudHVt"} } }'
Érdekes működés, hogy bár az URL-ben szerepel a dokumentum azonosítója azt ugyanúgy meg kell adnunk az átadott JSON adatban is.

Az átadott JSON adat kicsit áttekinthetőbben:

{ "_id":"couchdb_cikk",
  "_rev": "2812696990",
  "_attachments": 
  { 
    "csatolt.txt": 
      { 
        "content_type":"text\/plain", 
        "data":"Q3NhdG9sdCBkb2t1bWVudHVt"
      } 
  }
}

Ismét csak a telnet segítségével kérdezzük le a dokumentumot.

GET /blog/couchdb_cikk/ HTTP/1.0

{
  "_id":"couchdb_cikk",
  "_rev":"3172282319",
  "_attachments":
  {
    "csatolt.txt":
    {
      "stub":true,
      "content-type":"text\/plain",
      "length":18
    }
  }
}

A csatolmányokat nem kapjuk vissza a dokumentum lekérésekor automatikusan - ezt jelzi a stub:true érték - azokat külön URL-ken, a nevükön keresztül érhetjük el a dokumentum alatt, például így: /blog/couchdb_cikk/csatolt.txt.

GET /blog/couchdb_cikk/csatolt.txt HTTP/1.0

HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 18

Csatolt dokumentum

Ezt az URL elérést felhasználhatjuk a dokumentumok módosítására, sőt feltöltésére is. Így nem kell a JSON formátummal és base64 enkódolással bajlódni, mindössze a Content-Type HTTP fejléccel kell jeleznünk az adatunk formátumát.

PUT /blog/couchdb_cikk/kep.jpg
Content-Type:image/jpeg
Content-Length:123

<JPEG adat>

A csatolmányok egyik érdekes felhasználása lehet, hogy képeket tároljunk adatbázisszinten, amelyet azután egyszerűen meg tudunk jeleníteni a weboldalainkon a HTTP-s elérésnek köszönhetően.

Nézetek

A CouchDB-ben a nézetek felelnek meg a relációs adatbázisok lekérdezéseinek. A nézetek JavaScript nyelven írt kódok.

A nézetek kipróbálására érdemes a fentebb már említett http://localhost:5984/_utils/index.html címet megkeresni, ha belépünk az adatbázisunkba jobb oldalt felül találunk egy Select View legördülő listát innen pedig válasszuk a Custom query... pontot - így kicsit interaktívabb módon ellenőrizhetjük a változtatásaink közvetlen eredményét.

Nézzünk egy egyszerűbb nézetet:

function(doc) {
  map(null, doc)
}

Amint látható a nézetek egyszerű JavaScript funkciók, paraméterként pedig megkapják a bejárás során épp aktuális dokumentumot - doc változó. A funkción belül bármilyen logikát elhelyezhetünk, egy a lényeg, ha szeretnénk egy dokumentumot visszaadni a nézetben akkor az emit funkciót kell használnunk. Ennek első paramétereként megadhatjuk a dokumentum egy tulajdonságát, a nézet dokumentumait ez alapján fogja sorba rendezni, második paraméterként a dokumentum visszaadott tulajdonságait szűkíthetjük le. Lásd a következő példát:

function(doc) {
  emit(doc.title, {"Cim":doc.title, "Datum":doc.date, "ID":doc.id })
}

Ebben a nézetben a címek alapján lesz rendezve az összes dokumentum, és csak a megadott tulajdonságok (Title, Date, Id) szerepelnek mellettük, a megadott nevekkel.

A nézeteket kétféleképpen "érhetjük" el a HTTP API-n keresztül. Egyrészt POST kérésként átadhatjuk a megírt nézet függvényt az /{dbnév}/_slow_view URL-nek.

curl -X POST  \
-H 'Content-Type: text/javascript' \
-d 'function(doc){map(null, {"Title":doc.title, "Date":doc.date, "Id":doc.id }) }' \
http://localhost:5984/blog/_slow_view

A visszakapott válasz:

{
  "total_rows":3,
  "offset":0,
  "rows":
  [
    {
      "id":"19BF2F4BF267E8CCF120A2B3EB444C19",
      "key":null,
      "value":
      {
        "Title":"CouchDB cikk"
      }
    },
  
    {
      "id":"B7CFF70741ED178736D044F342B085EE",
      "key":null,
      "value":
      {
        "Title":"Masik CouchDB cikk"
      }
    }
  ]
}

A CouchDB terminológusában ezeket ideiglenes nézeteknek nevezzük, és inkább csak tesztelési célokra ajánlott használatuk - erre is utal a _\_slow\_view_ URL elnevezés. Ennél sokkal hatékonyabb, ha a nézeteinket is dokumentumokként, az adatbázisban tároljuk el: ekkor azok minden adatbázisban történt változás hatására - tehát például ha módosítunk egy dokumentumot - automatikusan lefutnak, az eredményeik pedig előre eltárolódnak! Ez sokkal gyorsabb működést eredményez.

A nézetek mint dokumentumok a következő formában tárolódnak:

{
  "_id": "_design/posts",
  "_rev": "3294989902",
  "language": "javascript",
  "views":
  { 
    "all_by_title":
    { 
      "map" : "function(doc){ emit(doc.title, {'Title':doc.title, 'Date':doc.date, 'Id':doc.id })}"
    },
  
    "all_by_author":
    {
      "map" : "function(doc){ emit(doc.author, doc)}"
    }
  }
}

A kulcs a nézet-dokumentumok elnevezésében adódik: mindegyiket a _design/ formával kell kezdenünk. A nézet-dokumentumok nemcsak egy, hanem tetszőleges számú nézetet tartalmazhatnak, elkülönítésükben az egyedi elnevezésük segít. A fenti példában például az első, all_by_title cím szerint, míg a második, all_by_author szerző szerint fogja visszadni a dokumentumjainkat.

A fenti nézet-dokumentumot egyszerűen hozhatjuk létre az adatbázisunkban:

curl -X PUT \
-d '{ "language": "javascript", "views":{ "all_by_title": { "map" : "function(doc){ emit(doc.title,{'Title':doc.title, 'Date':doc.date, 'Id':doc.id })}" }, "all_by_author":{ "map" : "function(doc){ emit(doc.author, doc)}" } }}' \
http://localhost:5984/blog/_design/posts

Válaszként pedig valami hasonlót kell kapnunk:

{"ok":true,"id":"_design\/posts","rev":"3294989902"}

A tárolt nézetek eredményét ezután egyszerűen egy GET kéréssel olvashatjuk ki, de erre már a _view/ útvonalat kell használnunk. Mivel eredetileg a _design/posts azonosítót adtuk a nézetet tároló dokumentumnak, a teljes elérést a _view/posts/all_by_title URL fogja szolgáltatni.

GET http://localhost:5984/blog/_view/posts/all_by_title HTTP/1.0 

{
  "id":"4fbdaedba5b66d4894191b8a52895c76",
  "key":"CouchDB cikk",
  "value":
  {
    "Title":"CouchDB cikk"
  }
},
{
  "id":"5126ff560acb43f444149b502fd1484b",
  "key":"CouchDB cikk",
  "value":
  {
    "Title":"CouchDB cikk"
  }
}

Összegzés

A fenti csak néhány példa volt a CouchDB REST API-jának a használatára, a teljesség igénye nélkül próbáltam bemutatni a főbb működést. Részletesebb információkért érdemes átrágni magunkat a CouchDB teljes referenciáján, ha szeretnénk megismerni az összes lehetőséget.

Mindezt azonban érdemes volt összefoglani, mielőtt rátérnénk arra, hogy hogyan is használhatjuk a CouchDB-t Rubyban.

A bejegyzés folytatását: A CouchDB találkozása a Rubyval ezen a linken keresztül érheted el.

További olvasnivalók

CouchDB kipróbálása Firebugban

Érdekes projektek és sok-sok link a CouchDB-ről

Nyolc részes cikksorozat szintén a CouchDB használatáról Rubyban