MongoDB se stalo velice rychle oblíbenou databází a dá se pro ni vyvíjet poměrně jednoduše i z Ruby. Pokud vás MongoDB láká, přečtěte si o jeho instalaci v systému Fedora, instalaci knihoven pro práci s Mongem a vytvoření jednoduché aplikace pro správu článků za použití jen pár příkazů na příkazové řádce.

Předpoklady: O instalaci samotného frameworku Ruby on Rails ve Fedoře zde již bylo napsáno v článku Spherical Cow on Rails, a tak tento článek již kompletní instalaci Ruby on Rails předpokládá (ať už z repozitářů Fedory, nebo z RubyGems.org). Pro instalaci zde zmíněných gemů vždy uvedu postup pro instalaci RPM, nicméně vše by mělo fungovat i pokud budete instalovat gemy přes gem install z RubyGems.org.

Co je MongoDB

MongoDB je v současnosti velice populární dokumentová databáze, kterou používá třeba Craigslist nebo SourceForge. Pokud jste o dokumentových databázích ještě nešlyšeli, věžte, že jejich hlavním rozdílem je beztabulkovost a neudržování schématu (které tam tímto není potřeba). Ukládaná data jsou zkrátka dokumenty, které mohou obsahovat jiné zanořené dokumenty. A ty zase další. Není problém měnit strukturu (atributy) těchto dokumentů, migrace tedy nejsou podmínkou pro změnu struktury dat. MongoDB je zaměřeno na rychlost a výkon v distribuovaném prostředí, které zajišťuje i podporou shardingu (proces pro ukládání dat na více fyzických strojů) a map-reduce (paradigma pro efektivní mapování a redukování vybíraných dat do agregovaných výsledků).

Formát BSON

BSON je onen reklamou proklamovaný JSON-like formát používaný MongoDB pro ukládání dat, jehož plnou specifikaci najdete na www.bsonspec.org. BSON je binární formát, kde je nula nebo více párů typu klíč-hodnota ukládáno jako jednotná entita, té pak BSON říká dokument.

Pokud vezmeme jednoduchý hash {"hello": "world"} pak:

"\x16\x00\x00\x00\x02hello\x00
 \x06\x00\x00\x00world\x00\x00"

je jeho reprezentace ve formátu BSON. Základní typy jsou byte (1 byte (8-bits)), int32 (4 bajty (32-bit signed integer)), int64 (8 bajtů (64-bit signed integer)) a double (8 bajtů (64-bit IEEE 754 floating point), které tvoří terminály gramatiky BSON standardu. Ve výše zmíněném BSONu je "hello" reprezentován jako hello\x00. \x00 a ostatní reprezentují terminály (tedy konkrétní bity). Elementy, sekvence elementů a samotný dokument jsou neterminály této gramatiky. Jednoduše řečeno, MongoDB implementuje tento formát pro ukládání svých dokumentů a tento formát nepředstavuje nic jiného než zanořovaný hash, který je velice podobný tomu v Ruby.

Instalace MongoDB ve Fedoře 19

Instalace MongoDB je velice jednoduchá:

$ su -c "yum install mongodb mongodb-server"

mongodb je klient a mongodb-server obsahuje serverovou část MongoDB a konfigurační a inicializační soubory. Všimněte si, že yum spolu s mongodb nainstaluje i v8, tedy javascriptový engine z Google Chrome, na který přešlo od verze 2.4.

Po instalaci balíku mongodb-server můžeme rovnou zapnout mongodb daemona:

$ su -c "service mongod start"
[sudo] password for strzibny:
Redirecting to /bin/systemctl start  mongod.service

mongod tedy zapneme standardně příkazem service mongod start, který slouží ke spouštění inicializačních souborů systemd, tedy i většiny databázových daemonů v systému Fedora. Pro ukončení bychom použili příkaz service mongod stop.

Po spuštení systémové služby již můžeme vyzkoušet, zda MongoDB skutečně běží:

$ service mongod status # nebo systemctl status mongod.service
Redirecting to /bin/systemctl status  mongod.service
mongod.service - High-performance, schema-free document-oriented database
   Loaded: loaded (/usr/lib/systemd/system/mongod.service; disabled)
   Active: active (running) since Wed 2013-10-09 13:40:54 CEST; 23min ago
  Process: 10866 ExecStart=/usr/bin/mongod $OPTIONS run (code=exited, status=0/SUCCESS)
 Main PID: 10868 (mongod)
   CGroup: name=systemd:/system/mongod.service
           └─10868 /usr/bin/mongod --quiet -f /etc/mongodb.conf run

Oct 09 13:40:54 jstribnyntb.usersys.redhat.com mongod[10866]: about to fork child process, wa....
Oct 09 13:40:54 jstribnyntb.usersys.redhat.com mongod[10866]: forked process: 10868
Oct 09 13:40:54 jstribnyntb.usersys.redhat.com mongod[10866]: all output going to: /var/log/m...g
Oct 09 13:40:54 jstribnyntb.usersys.redhat.com mongod[10866]: log file [/var/log/mongodb/mong...]
Oct 09 13:40:54 jstribnyntb.usersys.redhat.com mongod[10866]: child process started successfu...g
Oct 09 13:40:54 jstribnyntb.usersys.redhat.com systemd[1]: Started High-performance, schema-f....

A nakonec vyzkoušíme, že funguje i klient:

$ mongo
MongoDB shell version: 2.4.6
connecting to: test
>

Z shellu klienta pak máme přístup ke všem databázím a v nich uložených objektů BSON:

> show dbs
local	0.078125GB
> use my_db
switched to db my_db
> db.products.insert( { item: "polozka" } )
> db.products.find()
{ "_id" : ObjectId("52554abf7ea67e751c1023c5"), "item" : "polozka" }
> show databases
local	0.078125GB
my_db	0.203125GB

Příkazem use přepínáme databáze, přes db. přistupujeme k aktuálně vybrané databázi a jejím kolekcím. Databázi není třeba explicitně vytvářet, není potřeba vytvářet schéma ani kolekce. Stačí jen začít ukládat a hledat objekty. Co nás na vráceném hashi nejspíše hned zaujme, je automatické pole _id s hodnotou ObjectId("52554abf7ea67e751c1023c5"). Tento identifikátor je „nejspíše jednoznačným“ identifikátorem, jakým je např. primární klíč v relačních databázích, v MongoDB je vyžadován a je přidán automaticky. Interně je generován jako 4 samostatné celky: 4 bajty reprezentující sekundy od začátku unixové epochy, 3 bajty pro označení stroje, 2 bajty pro id procesu a 3 bajtový čítač začínající náhodným číslem. V praxi tak můžeme z tohoto _id získat čas a datum vytvoření objektu třeba funkcí ObjectId.getTimestamp() v mongo shellu. Pro procvičení si můžete z výše zmíněného ObjectId zjistit, kdy jsem psal tyto řádky:

> ObjectId("52554abf7ea67e751c1023c5").getTimestamp()

Pro další možnosti mongo klienta doporučuji projít manuálovou stránku.

Oficiální Ruby klient

mongo je oficiální klient pro Ruby distribuovaný jako RubyGem. Ve Fedoře jej doinstalujeme jednoduše příkazem yum install:

$ su -c "yum install rubygem-mongo"
================================================================================
 Package               Arch           Version              Repository      Size
================================================================================
Installing:
 rubygem-mongo         noarch         1.6.4-4.fc19         fedora          61 k
Installing for dependencies:
 rubygem-bson          noarch         1.6.4-3.fc19         fedora          24 k

Všimněte si závislosti na gemu BSON. Ten je samostatnou oficiální implementací stejnojmeného formátu v Ruby (doinstalováním bson_ext se bude automaticky načítat rychlejší rozšíření napsané v jazyce C; pro verzi 0.20 a vyšší se však od použití bson_ext upustilo).

Pokud bychom tedy chtěli jen serializovat Ruby hash do formátu BSON nebo nazpátek, můžeme využít přímo statických metod z třídy BSON:

$ irb
irb(main):196:0> require 'bson' # Načteme bson gem
=> false
irb(main):197:0> hash = { "vlastnost1" => "hodnota1", 2 => 3 } # Ukázkový hash, který převedeme do BSON
=> {"vlastnost1"=>"hodnota1", 2=>3}
irb(main):198:0> BSON.serialize(hash)
=> #<BSON::ByteBuffer:0x0000000296c088 @str="%\x00\x00\x00\x02vlastnost1\x00\t\x00\x00\x00hodnota1\x00\x102\x00\x03\x00\x00\x00\x00", @cursor=4, @order=:little_endian, @int_pack_order="V", @double_pack_order="E", @max_size=4194304>
irb(main):199:0> bson = BSON.serialize(hash)
=> #<BSON::ByteBuffer:0x000000029544b0 @str="%\x00\x00\x00\x02vlastnost1\x00\t\x00\x00\x00hodnota1\x00\x102\x00\x03\x00\x00\x00\x00", @cursor=4, @order=:little_endian, @int_pack_order="V", @double_pack_order="E", @max_size=4194304>
irb(main):200:0> hash = BSON.deserialize(bson) # A zpět
=> {"vlastnost1"=>"hodnota1", "2"=>3}

Použití monga pak může vypadat nějak takto (poznámka: v nových verzích mongo klienta se již připojuje přes třídu Mongo::Client):

irb(main):003:0> mongo_client = Mongo::Connection.new("localhost", 27017) # Vytvoření připojení
=> #<Mongo::Connection:0x000000019bd560 @host_to_try=["localhost", 27017], @port=nil, @host=nil, @max_bson_size=16777216, @id_lock=#<Mutex:0x000000019bd3d0>, @primary=["localhost", 27017], @primary_pool=#<Mongo::Pool:0xcd22d8 @host=localhost @port=27017 @ping_time= 0/1 sockets available.>, @slave_ok=nil, @ssl=false, @socket_class=Mongo::TCPSocket, @auths=[], @pool_size=1, @pool_timeout=5.0, @op_timeout=nil, @connect_timeout=nil, @safe=false, @logger=nil, @read_primary=true>
irb(main):004:0> mongo_client.database_names # Vylistování všech databází
=> ["local", "my_db"]
irb(main):005:0> mongo_client.database_info.each { |info| puts info.inspect }
["local", 83886080]
=> {"local"=>83886080}
irb(main):006:0> db = mongo_client.db('local') # Vybrání databáze a předání reference do lokální proměnné db

Kromě výchozí databáze local můžeme vidět i námi vytvořenou databázi my_db. Pojďme si zkusit vypsat její kolekce:

irb(main):004:0> db = mongo_client.db('my_db')
=> #<Mongo::DB:0x00000001dbe920 @name="my_db", @connection=#<Mongo::Connection:0x00000001c62bf8 @host_to_try=["localhost", 27017], @port=nil, @host=nil, @max_bson_size=16777216, @id_lock=#<Mutex:0x00000001c62a18>, @primary=["localhost", 27017], @primary_pool=#<Mongo::Pool:0xe65f64 @host=localhost @port=27017 @ping_time= 0/1 sockets available.>, @slave_ok=nil, @ssl=false, @socket_class=Mongo::TCPSocket, @auths=[], @pool_size=1, @pool_timeout=5.0, @op_timeout=nil, @connect_timeout=nil, @safe=false, @logger=nil, @read_primary=true>, @strict=nil, @pk_factory=nil, @safe=false, @read_preference=:primary, @cache_time=300>
irb(main):006:0> Mongo::DB.instance_methods
=> [:strict=, :strict?, :name, :safe, :connection, :cache_time, :cache_time=, :authenticate, :issue_authentication, :add_stored_function, :remove_stored_function, :add_user, :remove_user, :logout, :issue_logout, :collection_names, :collections, :collections_info, :create_collection, :collection, :[], :drop_collection, :get_last_error, :error?, :previous_error, :reset_error_history, :dereference, :eval, :rename_collection, :drop_index, :index_information, :stats, :ok?, :command, :full_collection_name, :pk_factory, :pk_factory=, :profiling_level, :profiling_level=, :profiling_info, :validate_collection, :read_preference, :nil?, :===, :=~, :!~, :eql?, :hash, :<=>, :class, :singleton_class, :clone, :dup, :taint, :tainted?, :untaint, :untrust, :untrusted?, :trust, :freeze, :frozen?, :to_s, :inspect, :methods, :singleton_methods, :protected_methods, :private_methods, :public_methods, :instance_variables, :instance_variable_get, :instance_variable_set, :instance_variable_defined?, :remove_instance_variable, :instance_of?, :kind_of?, :is_a?, :tap, :send, :public_send, :respond_to?, :extend, :display, :method, :public_method, :define_singleton_method, :object_id, :to_enum, :enum_for, :==, :equal?, :!, :!=, :instance_eval, :instance_exec, :__send__, :__id__]
irb(main):010:0> db.collection_names
=> ["system.indexes", "products"]

Protože jsem zrovna nevěděl, jak se ke kolekcím dostat, nechal jsem si jednoduše vypsat metody pro instance třídy Mongo::DB a našel collection_names. Naše kolekce products zde je, byla tedy vytvořena automaticky přidáním našeho objektu v klientu mongodb příkazem db.products.insert( { item: "polozka" } ).

Nejspíše vás zaujala kolekce system.indexes, kterou jsme nevytvářeli, ale byla vytvořena automaticky. Do té ukládá MongoDB všechny indexy dané databáze. MongoDB vytváří i další systémové kolekce, a tak se doporučuje nepojmenovávat kolekce prefixem system. Pokud byste hledali další informace o API k mongo, bude jednodušší podívat se na API referenci ke konkrétní verzi, která se nachází na oficiálních stránkách mongodb.org.

Object Document Mapper pro MongoDB

Jak jsme si ukázali, oficiální klient Ruby se snaží o podobný přístup k databázi jako klient mongodb pro jazyk Ruby. V Ruby on Rails si ale již mnozí zvykli na rozsáhlé možnosti a mapování knihovny ActiveRecord a rádi by své dokumentové modely mapovali podobně jako ty relační využívající ORM (Object Relational Mapper) pro přístup k datům. Dobrou zprávou je, že takových projektů již existuje celá řada. Oficiální stránka uvádí následující:

Všechny byly postaveny právě nad oficiálním klientem Ruby a snaží se určitým způsobem nahradit ActiveRecord pro MongoDB. Mezi nejznámější a nejpoužívanější patří MongoMapper a Mongoid. Mongoid je z nich jediným, který už od použití oficiálního klienta upustil a rozhodl se napsat si vlastní. Je již také dostupný z repozitářů Fedory, a tak si jej dneska představíme.

Mongoid

Mongoid je tedy jedním z ODM pro MongoDB napsaný v Ruby. Je tedy něco jako ORM pro relační databázi. V Ruby on Rails je to právě náhrada za ActiveRecord, který mapuje pouze objekty relačních databází.

Samotná instalace je opět velice jednoduchá:

$ su -c "yum install rubygem-mongoid"
=================================================================================
 Package                 Arch           Version             Repository      Size
=================================================================================
Installing:
 rubygem-mongoid         noarch         3.1.4-1.fc19        updates         216 k
Installing for dependencies
 rubygem-moped           noarch         1.5.0-1.fc19        fedora           44 k
 rubygem-origin

Všiměte si, že spolu s gemem mongoid byly nainstalovány i další závislosti. Ty souvisí s modularizací Mongoidu, který je nyní rozdělený na tři části, které jdou používat samostatně.

Moped: Moped je Ruby driver pro MongoDB s jednoduchým a elegantním API. Právě díky němu můžeme přistupovat k objektům v MongoDB z Ruby velmi jedoduše. Skládá se ze tří části: implementace BSON specifikace, implementace Mongo Wire protokolu a samotného driveru. Právě tato knihovna nahrazuje původně používáný oficiální mongo gem. Zkusme si do naší kolekce produktů přidat nové produkty s různými vlastnostmi:

$ irb
irb(main):001:0> require 'moped'
=> true
irb(main):002:0> session = Moped::Session.new([ "127.0.0.1:27017" ])
=> <Moped::Session seeds=["127.0.0.1:27017"] database=none>
irb(main):003:0> session.use 'local'
=> #<Moped::Database:0x000000010a7200 @session=<Moped::Session seeds=["127.0.0.1:27017"] database=local>, @name="local">
irb(main):006:0> session[:products].insert({ name: "Keyboard", color: "black" })
=> nil
irb(main):007:0> session[:products].insert({ name: "Laptop", os: "Fedora" })
=> nil

Origin: Origin je DSL pro jakýkoliv objekt Ruby, kterým můžeme jednoduše tvořit databázové dotazy pro MongoDB. Díky extrakci z a nezávislosti na gemu mongoid můžeme toto DSL použít i bez mapování na Mongoid. Příklad z oficiální stránky ilustruje, jak můžeme toto DSL použít pro jakoukoliv třídu v Ruby:

irb(main):001:0> require 'origin'
=> true
irb(main):002:0> class Criteria
irb(main):003:1>   include Origin::Queryable
irb(main):004:1> end
=> Criteria
irb(main):005:0> criteria = Criteria.new
=> #<Criteria:0x0000000256f1d8 @serializers={}, @driver=:moped, @aliases={}, @selector={}, @options={}>
irb(main):006:0> criteria = criteria.where(name: "Syd").gt(age: 10).desc(:created_at)
=> #<Criteria:0x00000002596dc8 @serializers={}, @driver=:moped, @aliases={}, @selector={"name"=>"Syd", "age"=>{"$gt"=>10}}, @options={:sort=>{"created_at"=>-1}}, @strategy=nil, @negating=nil>
irb(main):007:0> criteria.selector #=> { name: "Syd", age: { "$gt" => 10 }}
=> {"name"=>"Syd", "age"=>{"$gt"=>10}}
irb(main):008:0> criteria.options  #=> { sort: { created_at: -1 }}
=> {:sort=>{"created_at"=>-1}}

Do naší třídy Criteria tedy zahrneme modul Origin::Queryable a to je vše. Takto můžeme využívat selectory a různá nastavení i v jednochuchých objektech Ruby bez napojení na MongoDB.

Mongoid: knihovna Mongoid tedy staví na Mopedu a Originu a nabízí tak ODM pro MongoDB. Pojďme si tedy vytvořit jednoduchou aplikaci v Ruby on Rails a Mongoid a ukazát si, jak se s Mongoidem pracuje.

Ukázková aplikace v Rails

Nejprve je třeba vytvořit novou aplikaci Rails:

$ rails new mongodbapp --skip-active-record --skip-bundle && cd mongodbapp

Již při vytváření nové aplikace nad Ruby on Rails můžeme díky --skip-active-record generátoru říct, aby nevytvářel závislost na knihovně ActiveRecord. --skip-bundle přeskočí bundlování závislotí. Přidání Mongoidu je otázkou přidání další závislosti do souboru Gemfile a spuštění příkazu bundle, pokud používáme Bundler pro řešení závislostí (pozn.: pokud již používáte Rails 4.0, verze mongoidu 3.1.4 nebude fungovat s knihovnou ActiveModel 4.0.0 a v době psaní tohoto článku ještě nevyšla kompatibilní verze s Rails 4.0):

$ echo "gem 'mongoid', '3.1.4'" >> Gemfile
$ bundle --local

Přepínač --local nám zajistí, že se nebudou stahovat novější verze Gemů Ruby, které v tuto chvíli nepotřebujeme, a proces proběhne mnohem rychleji. Všechny potřebné by totiž měly již být přítomné na systému.

Gem Mongoid nám umožňuje vygenerovat počáteční konfiguraci pro databázi:

$ rails g mongoid:config
      create  config/mongoid.yml

Puštěním výše zmíněného příkazu se nám tedy vygeneruje soubor config/mongoid.yml s počáteční konfigurací pro vývojové a testové prostředí. Podstatná část konfigurace spočívá v nastavení příslušného portu a názvu databáze (které můžeme jednoduše změnit). Začátek souboru již obsahuje potřebné standardní nastavení a nemusíme na něm pro začátek ani nic měnit:

development:
  # Configure available database sessions. (required)
  sessions:
    # Defines the default session. (required)
    default:
      # Defines the name of the default database that Mongoid can connect to.
      # (required).
      database: mongodbapp_development
      # Provides the hosts the default session can connect to. Must be an array
      # of host:port pairs. (required)
      hosts:
        - localhost:27017
      options:

Jména našich databází tedy budou mongodbapp_development a mongodbapp_test. V config/mongoid.yml najdeme spoustu dalších zakomentovaných řádků, kterým můžeme MongoDB nastavit. Tato nastavení jsou ale jednoduše nad rámec tohoto článku.

Modely v Rails

S Mongoidem můžeme využít scaffolding, který nám vytvoří příslušné modely v app/models; jako obvykle:

$ rails g scaffold article name:string content:text
$ cat app/models/article.rb
class Article
  include Mongoid::Document
  field :name, type: String
  field :content, type: String
end

Náš dokument bude tedy článek s titulkem a obsahem. Když se podíváme na nově vygenerovaný model, můžeme si všimnout, že již zahrnuje Mongoid::Document a pro náš jednoduchý příklad nemusíme měnit nic dalšího. V tuto chvíli máme vytvořenou jednoduchou aplikaci CRUD na správu článků v Rails s použitím databáze MongoDB. Pokud pustíme server příkazem: rails s můžeme na adrese http://localhost:3000/articles přidávat, editovat a mazat články.

Zda se nám články správně uložily, si můžeme opět ověřit třeba v shellu mongo:

$ mongo
> use mongodbapp_development
switched to db mongodbapp_development
> db.articles.find()
{ "_id" : ObjectId("526a44b03b8e6154ca000002"), "name" : "Article 1", "content" : "Content of article 1" }
{ "_id" : ObjectId("526a44bf3b8e6154ca000003"), "name" : "Article 2", "content" : "Content of article 2." }

Jak je vidět, mé články se mi uložily, takže naše malá aplikace funguje. Začít s Mongem je tedy opravdu velice jednoduché. Pojďme ještě nakonec zkusit, co se stane s naší kolekcí, pokud přejmenujeme atribut content na text v našem modelu, provedeme příslušné změny v šablonách a přidáme nový článek:

$ mongo
> db.articles.find()
{ "_id" : ObjectId("526a44b03b8e6154ca000002"), "name" : "Article 1", "content" : "Content of article 1" }
{ "_id" : ObjectId("526a44bf3b8e6154ca000003"), "name" : "Article 2", "content" : "Content of article 2." }
{ "_id" : ObjectId("526a4ede3b8e614dc8000001"), "name" : "Article 3 ", "text" : "Body of article 3." }

Databáze se neorientuje podle žádného schématu, a tak jí změna nevadí – prostě přidala nový objekt do kolekce. Naše data ale přestala být konzistentní. Dokumentové databáze zkrátka nezaručují integritu a je potřeba na to nezapomínat.