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.