csiszarattila.com / Rubysztán

A CouchDB találkozása a Rubyval

A bejegyzés első részében láthattuk, hogy a REST API-nak köszönhetően hogyan tudjuk a CouchDB-t elérni és használni. Mindez azt jelenti, hogy erre építve Rubyban is nagyon egyszerűen tudunk az adatbáziskezelővel kommunikálni, a CouchDB dokumentációjában találunk is egy ezt megvalósító osztályt.

A HTTP kommunikációt érdemes a Ruby beépített Net::HTTP moduljára bízni. Ami fontosabb, hogy nyilván nem JSON adatformátummal, hanem Ruby osztályokkal szeretnénk dolgozni: az adatbázisból érkező adatokat Ruby objektumokként kapjuk meg, majd ugyanígy, az adatbázisba íráskor is el tudjunk menteni a Ruby objektumokat. A JSON gem ezt az "átjárást" meg is valósítja a számunkra, tehát különösebb aggályunk ezzel kapcsolatban sem lehet.

Ennek tükrében nem is csoda, hogy több Ruby interfész is készült a CouchDB-hez, amelyek elvégzik helyettünk a "piszkos munkát". A következőkben ezek használatával ismerkedhetünk meg.

CouchRest

Telepítés:

sudo gem install couchrest

A CouchRest igazán leegyszerűsíti a dolgunkat. Elég mindössze egy URL segítségével csatlakozást létrehozni a CouchDB-s adatbázisunkhoz, a csatlakozást pedig referencia-változóként felhasználva műveleteket végezni:

require 'rubygems'
require 'couchrest'

@db = CouchRest.database!("http://localhost:5984/books")

doc = @db.get("couchdb_cikk")
=> {"_rev"=>"3172282319", "_id"=>"couchdb_cikk", 
  "_attachments"=>{"csatolt.txt"=>{"content_type"=>"text/plain", "stub"=>true, "length"=>18}}}1

response = @db.save(
  {
    "id"=>"wpgtr",
    :title=>"Why's (poignant) guide to Ruby", 
    :author=>"why the lucky stiff",
    :motto=>"chunky bacon!",
    :chapters=>
    [
      "Kon’nichi wa, Ruby",
      "A Quick (and Hopefully Painless) Ride Through Ruby (with Cartoon Foxes)",
      "Floating Little Leaves of Code"
    ]
  }2
)

response
=> {"rev"=>"887962467", "id"=>"wpgtr", "ok"=>true}

@db.view('posts/find_by_title')3

Mint látható az adatokat egyszerű Ruby hashekként tudjuk kezelni12. A nézetek létrehozása és lekérdezése3 sem nehézkes hála a REST API-nak.

A CouchRest ráadásul rendelkezik egy beépített ORM megoldással a CouchRest::Model osztállyal, ha hash-ek helyett kicsit objektum-orientáltabban szeretnénk kezelni az adatokat, például modelleket szeretnénk alkalmazni.

Használatához előbb telepítsük az extlib gem-et mivel ez valahogy kimaradt a CouchRest függőségei közül.

sudo gem install extlib

Nézzünk a használatára egy teljes példát:

require 'rubygems'
require 'couchrest'

class Book < CouchRest::Model
  use_database CouchRest.database('http://localhost:5984/books')
  
  key_accessor :title, :author, :category 1
  cast :author, :as=> 'Author' 2
  
  view_by :ruby, 3
  :map=>"function(doc){ if(doc.category == 'ruby') emit(null,doc) }"
  
  unique_id :title 5
  before(:save, :convert_title) 5
  def convert_title 5
    self["title"] = title.gsub(/ /,'_')
  end
  
end

class Author < CouchRest::Model 2
  key_accessor :name
end

rails_book = Book.new

rails_book.title= "Ruby for Rails" 1
dt = Author.new() 
dt.name= "Dave Thomas" 2
rails_book.author.name= dt 1
rails_book.category= "rails" 1

rails_book.save

rails_book = Book.all[0]
rails_book.update_attributes({:author=>{:name=>"David A. Black"},:category=>"ruby"})

Book.all[0].author.name 4

Book.by_ruby 3
=> [{"category"=>"ruby", "author"=>{"name"=>"David A. Black", "couchrest-type"=>"Author"},
   "title"=>"Ruby_for_Rails", "_rev"=>"2415607020", "_id"=>"Ruby_for_Rails", "couchrest-type"=>"Book"}]

Ahhoz, hogy a dokumentumunk tulajdonságait metódusokkal érhessük el1, előbb definiálnunk kell őket a key_accessorral, a Ruby setter/getter segédmetódusaihoz hasonlatosan. A tulajdonságokat lehetőség van osztályokra2 is leképezni, tehát nemcsak az egyszerű Ruby típusokat (Array,String,Hash stb.) használhatjuk. A modellekhez nézeteket is definiálhatunk3 - ezek bekerülnek az adatbázisba is -, amelyeket metódusokként hívhatunk meg. Beépítettként is kapunk párat, például az 4 összes dokumentumot visszaadó all-t, de ugyanígy a tulajdonságokkal is (pl. by_title), amelyeket lehetőségünk van egymásba is ágyazni, a Rails named_scope-jaihoz hasonlatosan.

Az unique_id lehetővé teszi, hogy egy metódust megnevezzünk5, amelyben azután egyedi azonosítót generálunk, ez kerül majd az dokument _id mezőjébe - a fenti példában a könyv címét (title attribútum) használtuk fel erre.

A CouchRest::Model egy egész jól használható ORM, ráadásul sok hasonlóságot mutat az ActiveRecorddal, így használata nem jelent túl nagy gondot, ugyanakkor nem túl biztos a jövője: a fejlesztése leállt és valószínűleg hamarosan ki is kerül a CouchRest gem-ből.

Szerencsére azonban vannak alternatíváink.

CouchObject

Telepítés:

sudo gem install couchrest

A CouchObject a CouchResthez képest kibővített utasításokkal, és APIval rendelkezik. Igazából A CouchObjectet kellene CouchRestnek hívni, mivel az adatbázis-kezelést illetően a metódusok működése jobban illeszkedik a REST elvekhez. Ehhez lásd a következő példát:

require 'rubygems'
require 'couch_object'
require 'json'

db = CouchObject::Database.open("http://localhost:5984/books")

doc = db.get('wpgtr') 
=>#<CouchObject::Response:0x14db2f4 ...>

# JSON formátum
doc.body
=> "{\"_id\":\"wpgtr\",\"_rev\":\"1415663406\" ... }"
# Ruby formátum
doc.parsed_body
=> {"title"=>"Why's (poignant) guide to Ruby", 
  "chapters"=>
  [
    "Kon’nichi wa, Ruby", 
    "A Quick (and Hopefully Painless) Ride Through Ruby (with Cartoon Foxes)", 
    "Floating Little Leaves of Code"
  ]}

wpgtr_expak_one = {
  :title=>"Expansion Pak I: The Tiger’s Vest",
  :author=>"why the lucky stiff",
  :description=>"There was once a tiger who wore a vest, but that wasn't his biggest problem.
   Because the earth was going to crash into the Sun!"
}

db.put('wpgtr',JSON.unparse(wpgtr_expak_one))
=>#<CouchObject::Response:0x14d8388 ..>

A CouchObject minden kéréskor egy CouchObject::Response objektummal válaszol, amelyben megtaláljuk a HTTP fejléceket is - így kicsit könnyebb dolgunk van a hibák kezelésekor - ráadásul elérhetjük mind az eredeti JSON üzenetet ( body ), mind a Ruby objektumokra átalakítottat ( parsed_body ) egy egy metódussal.

Érdekesség, hogy a nézeteket nemcsak a get metódussal és kéréssel érhetjük el, hanem objektumokként (CouchObject::View) is definiálhatjuk őket. Példaként hozzunk létre egy _desing/books/all nevű nézetet, majd kérdezzük le az eredményét:

db = CouchObject::Database.open("http://localhost:5984/books")

CouchObject::View.create(db,"books",'{ "language": "javascript",
"views" : {"all" : {"map" : "function(doc) {emit(null,doc)}"}} }')

CouchObject::View.new(db,"books/all").query

A CouchObject rendelkezik egy speciális osztállyal(CouchObject::Document) is, amely egyszerűbbé teszi a JSON-ban kapott dokumentumok kezelését:

doc = db.get("wpgtr")

wpgtr = doc.to_document # => #<CouchObject::Document ...>

wpgtr.title # => "Why's (poignant) guide to Ruby"
wpgtr.author # => "why the lucky stiff"

wpgtr["characters"] = ["The elf with a pet ham","Trady Blix", "Starmonkey"]

wpgtr.save(db)

Emlékezz, hogy a CouchObject a kérések válaszaiként nem egy dokumentumot, hanem mindig egy Response objektumot ad vissza, így ez tartalmazza a a JSON választ is. Ebből egyszerűen a to_document metódussal tudunk egy CouchObject::Document objektumot visszaadni, ezzel a Hash-ekhez képest némileg egyszerűbben tudjuk a dokumentum tulajdonságait elérni/szerkeszteni.

Egy másik hasznos megoldás a CouchObject::Persistable modul, amellyel Rubys objektumainkat tudjuk CouchDB dokumentumokká alakítani. Használatához elég a modult be 'mixin-elni' az osztályunkba, és máris elérhetővé válnak a CouchDB-t elérő metódusok - így lehetővé válik az objektumok dokumentumokká alakítása majd lekéréskor a visszaalakításuk:

class Book
  include CouchObject::Persistable
  database 'http://localhost:5984/books' 1

  attr_accessor :author, :title, :year

  def initialize(author, title, year)
    @author, @title, @year = author, title, year
  end

  def self.from_couch(attributes)
    new attributes["author"], attributes["title"], attributes["year"]
  end
end

data = Book.new("Ruby for Rails", "David A. Black", 2005).save() 
=> {:revision=>"374633541", :id=>"6f2f22fc0e80a76fd253365201e243c1"}

rfr = Book.get(data[:id]) 
=> #<Book:0x14d5fd4 ...>

Az osztályunkban opcionálisan megadhatjuk mely adatbázist használja1 mentéskor illetve lekéréskor, így nem kell minden alkalommal a save és get metódusoknak megadnunk az elérést. A CouchObject a mentés illetve a visszaalakítás segítésére két metódust a to_couch ill. a from_couch-t is a rendelkezésünkre bocsájtja. Ezekkel kicsit bonyolultabb logikát rendelhetünk az objektumok dokumentumokból való visszaállításakor és fordítva. Ha a konstruktorunk például paramétereket is vár, akkor utóbbit mindenképpen használnunk kell máskülönben a CouchObject nem tudja majd az objektumot létrehozni a dokumentumból.

A CouchObject is lehetővé teszi, hogy adott attribútumokat további osztályokra képezzünk le - a CouchResthez hasonlóan.

class Book
  ...
  def initialize(author, title, year)
  @author = Author.new(author)1
  @title, @year = title, year
  end
  ...
end

class Author
  include CouchObject::Persistable

  def initialize(name)
    @name = name
  end
end

rfr = Book.get('18c7df416f4f5cea94b1e420c614d4a1') 
rfr.author
=> #<Author ...>
A példát tekintsd úgy, mintha az előző példa Book osztályát felüldefiniáltuk volna.

Ebben az esetben az attribútum értéke közvetlenül a dokumentumban tárolódik1, de a CouchObject - a CouchResthez képest pluszként - lehetővé teszi, hogy más dokumentumokra is hivatkozhassunk. Így a relációs adatbázisoknál megszokott kapcsolatokat (N-N, 1-N, 1-1) is létrehozhatunk az egyes dokumentumok és osztályok között - mint látni fogjuk ez a funkció nagyon hasonlít az ActiveRecord megoldásához. Nézzünk erre is egy példát:

class Book
  include CouchObject::Persistable
  database 'http://localhost:5984/books'

  belongs_to :publisher, :as => :books
end

class Publisher
  include CouchObject::Persistable
  database 'http://localhost:5984/books'

  has_many :books
end

book = Book.get('18c7df416f4f5cea94b1e420c614d4a1') 
book.publisher = Publisher.new()
book.save

book.publisher.books[0] == book 
=> true
Ismét csak tekintsd úgy, mintha az előző példánk osztályait kiterjesztettük volna.

Mint talán észrevetted a kapcsolatok a:

belongs_to :kapcsolat_neve, :as => :has_many_kapcsolat_neve
has_many :has_many_kapcsolat_neve

elnevezést követik, és ez ugyanígy alkalmazható az 1-1 kapcsolatokhoz is - itt ugye a has_one, belongs_to párost kell használnunk. Ezzel már tulajdonképpen elég kényelmes kis kapcsolati hálót tudunk létrehozni az osztályok és így az objektumok között, komplex dokumentumok kezelésénél ez mindenképp jól jöhet.

Ugyanakkor arra figyeljünk, hogy mind a CouchObject, mind a CouchRest 'teleszemeteli' a dokumentumokat az tulajdonságokhoz tartozó osztályok nevével és ez nem féltétlenül hasznos akkor, ha a Rubyban írt alkalmazáson kívül is szeretnénk az adatainkat lekérdezni és használni.

Végül harmadik versenyzőként a RelaxDB-t nézzük meg.

RelaxDB

Telepítés

Ha még nem tettük volna, ezzel a paranccsal adjuk hozzá a gemforrások listájához a GitHubot:

gem sources -a http://gems.github.com

Majd a gem telepítéséhez adjuk ki a következő parancsot:

sudo gem install paulcarey-relaxdb

A RelaxDB sokkal inkább egy ORM megoldás, mint egy szimpla Ruby interfész a CoucDB-hez - ami egyfelől jó, mert mindent a kezünkbe ad, de túlkomplikálttá is teheti a dolgokat amikor valami egyszerűbb megoldásra vágyunk.

Elsőként lássuk az előző példa megoldását a RelaxDB segítségével:

require 'rubygems'
require 'relaxdb'

RelaxDB.configure :host => 'localhost', :port => 5984
RelaxDB.use_db 'books'


class Book < RelaxDB::Document
  property :author
  property :title
  property :year, :validator => lambda{ |y| y > 2005}, :validation_msg => "2005 előtt nem is volt Rails"

  belongs_to :publisher, :class => 'Publisher'
end

class Publisher < RelaxDB::Document

  has_many :books
end

Book.new({:title=>"Agile Web Development with Rails", :author=>"Dave Thomas", :year=>2006, '_id'=>"awdwr"}).save

book = Book.new({:title=>"Yet Another Rails book", :year=>2002 })
book.save
=> false
book.errors
=> {:year=>"2005 előtt nem is volt Rails"}

book.publisher = Publisher.new()

A RelaxDB esetében explicite meg kell adnunk a modellünk tulajdonságait, viszont mindjárt rendelhetünk melléjük validátorokat is, ez elég kezdetleges megoldás de működik. A kapcsolatok létrehozása is a már szokott módon történik - a RelaxDB amúgy is meglehetősen az ActiveRecord konvencióit követi, úgyhogy sok meglepetés nem érhet bennünket.

A RelaxDB hátránya ugyanakkor, hogy abszolút nincs dokumentálva. Az elinduláshoz jó alapot nyújt a gem README fájlának az elolvasása, de egyébként a forráskód tanulmányozására lesz szükségünk, ha minden funkcióját szeretnénk megtalálni. Ebben a programhoz írt tesztek átfutása jó szolgáltatot tehet. Arra azonban ügyeljünk, hogy a legújabb gem már a CouchDB trunk (0.9-es) változatára épít, így sok funkciót nem érhetünk el a MacPortson keresztül telepített 0.8.1-es CouchDB-ben.

Sajnos nekem bővebb lehetőségem nem volt elmerülni a RelaxDB-ben, mindenesetre ígéretesnek tűnik, mégha kicsit más koncepcióban - inkább ORM - közelíti is meg a CouchDB elérését.

Összefoglaló

Remélem ez a két hosszúra nyúlt bejegyzés nem vette el senki kedvét és tesz egy próbát a CouchDB-vel. A koncepciója kissé szokatlan, de a világos és tiszta REST API-nak köszönhetően nagyon gyorsan beletanulhatunk. Ráadásul, mint láttuk Rubyban is egyszerűen használhatjuk: a változatos interfészek közül még válogathatunk is - Én a fentiek közül a CouchObjectre tenném a voksom - , vagy akár belevághatunk egy saját implementálásába is. Ezután pedig jöhetnek a CouchDB-ben írt alkalmazások, nekem már van is egy ötletem...

További olvasnivalók

A bejegyzés első része: CouchDB - szakítás a relációs adatbázisokkal

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

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