8.9.2015
Víte, co mají společného Vladivostok, malé město na Urale a zapadlá ves na Vysočině? Celou oblast obsluhuje malinký tým prodejců, v české republice se sejdou jednou za čas, a připojení k internetu ve většině lokalit rozumně nefunguje, nebo je pekelně drahé. V takovéto situaci nás oslovila jedna firma, jestli by nebylo možné sdílet údaje o zákaznících, podmínkou ovšem byla offline práce a synchronizace jen jednou za čas. Naše první odpověď byla celkem jasná - dokážeme vám udělat webovou aplikaci, ale sdílet data bez připojení k iternetu po nás chtít nemůžete. To nejde.
Jenomže požadavek zněl docela rozumně. To se opravdu nikdo ničím podobným nikdy nezabýval? Vážně to nejde? No a tak jsme objevili databázi CouchDB.
Nad databází jsme postavili distribuovaný CRM systém Deko. Nejrozsáhlejší instalaci z pohledu množství počítačů máme u nás ve firmě - databáze je nainstalovaná na pěti počítačích. Kromě databáze na serveru jsou to většinou různé pracovní stanice nebo notebooky. V databázi shromažďujeme pracovní výkazy, úkoly, kontakty a poznámky k různým projektům - telefonáty, maily, smlouvy a další podklady pro fakturaci. Databáze na serveru slouží jako "společný bod" - je stále k dispozici, je přístupný odkudkoliv a jeho adresa se nemění. Navíc se z databáze na serveru čerpají podklady pro zákazníky - na fakturu napíšu pouze URL adresu, na které najdou podrobnější informace. Svůj pracovní stroj synchronizuji několikrát do týdne, notebook pak pouze pokud se chystám někam ven. Kolega to dělá podobně.
Další naší větší aplikací s databází CouchDB je komunikační systém pro zdravotnická zařízení. Tam využíváme toho, že databáze je velmi jednoduchá k nastavení - opravdu stačí databázi nainstalovat a okamžitě ji lze používat.
Na některé úlohy se CouchDB hodí dobře, na jiné zase ne. Špatně se s CouchDB řeší úlohy, které potřebují referenční integritu, obsahují velké množství dat nebo vyžadují složité filtrování pro potřeby statistiky a podobně. S CouchDB tak například nemonitorujeme třicet fotovoltaických elektráren, neukládáme zde milióny měření denně a neděláme nad těmito daty rozsáhlé statistické výpočty.
CouchDB se nehodí na úlohy, které vyžadují, aby váš kolega měl ve své databázi přesně ta samá data, ke kterým máte přístup vy. S CouchDB tak neřešíme ani účetnictví, protože by se nám hodně špatně zajišťovaly spojité řady dokumentů v prostředí, kde je každý účetní úplný mimoň. Jednotlivým účetním by to možná bylo jedno - ve větší účtárně se stejně všichni starají jen o svou část dat. Těžkou hlavu by z toho mohl mít jen jejich šéf, až by přišla kontrola z finančáku. Na druhou stranu, když se mohou používat účetnictví postavená nad Foxkou...
Máte v databázi tabulky tohoto typu?
|
|
|
Myslíte si, že podobnou situaci ukočírujete? Já jsem si to myslel několikrát taky, ale potom na mé místo nastoupil idiot, který nejenže neocenil genialitu mého datového návrhu, on s ním vůbec nedokázal pracovat. A řeknu vám, ani ty databáze nejsou tak rychlé, jak bych moje úlohy potřebovaly.
S podobnými konstrukcemi je potíž - pracuje se s nimi špatně, SQL příkazy mají tendenci být hodně složité a relační databáze pro podobná řešení nejsou stavěné, nedokážou s nimi pracovat efektivně.
Než si vývojaři spálí prsty, sahají po tomto řešení poměrně často, takže tato datová struktura dostala i svůj vlastní název: EAV antipattern (Entity-Attribute-Value antipattern).
Pokud vypadají vaše datové struktury převážně právě takto, může být pro vás CouchDB lepší volbou, než obvyklá MySql.
Databáze je vyvíjená jako otevřený a svobodný software pod Apache licencí. V praxi to znamená, že databázi můžete používat ve svých projektech podobně jako jiný software se svobodnou licencí (linux, apache, mysql, php). Podrobnosti najdete přímo v textu licence: http://www.apache.org/licenses/
Databáze je napsaná v jazyce Erlang. To mi ovšem vůbec nic mi neříká a pro používání databáze to ani není podstatné. V databázi se hodně používá JavaScript a Json. V JavaScriptu se tvoří view - hodně vzdálená obdoba databázových view a indexů známých z relačních databází.
S databází se komunikuje protokolem HTTP, používá se jednoduché rest api s příkazy get, put, post a delete. Veškerá data se předávají ve formátu Json (až na vyjímky, na které při normálním používání nenarazíte).
Součástí databáze je i webové rozhraní pro správu databáze. V něm se dají dělat všechny administrátorské činnosti - zakládat a rušit databáze, vytvářet dokumenty, view, uživatele a podobně.
Tabulku v klasické databázi si můžete představit jednoduše jako jeden list v tabulkovém kalkulátoru. Sloupce jsou pevně dané pro všechny záznamy (řádky) a změnit formát jednoho záznamu je prakticky nemožné. Rozšíření tabulky o nové sloupce nebo odstranění sloupců se vždy bude týkat celé tabulky.
V databázi CouchDB žádné tabulky neexistují. Každý uložený záznam (dokument) je nezávislý na ostatních záznamech a jeho formát je prakticky libovolný - samozřejmě tedy jen pokud se budete držet formátu Json.
Jeden záznam může vypadat například takto:
{ _id: identifikator1, _rev: abcd-1, jmeno: "abcd" }
A další záznam může vypadat zase takto:
{ _id: identifikator2, _rev: abcd-1, jmeno: "xyz", popis: "couchdb je prima", barvy: [ "#ff0000", "#00ff00", "#0000ff" ] }
Společné pro všechny dokumenty jsou pouze položky _id a _rev. Ty jsou povinné - podle _id databáze rozeznává záznam a podle _rev databáze rozeznává verzi tohoto záznamu.
Pro spoustu z vás bude tato část nošením dříví do lesa, ale než jsem se setkal s databází CouchDB, opravdu jsem nevěděl, k čemu je Json dobrý. Protože Json považuji za velmi podstatnou součást databáze CouchDB, raději formát Json trochu rozvedu.
Protože nejsem zrovna webař - většinu kódu generuji v C++, před tím, než jsem se seznámil s databází CouchDB, jsem o formátu Json ani netušil, že existuje. Teda... viděl jsem ho, ale pokrčil jsem rameny: "Hmmm, jednoduché" a ani jsem netušil, že jsem vystihnul nejdůležitější vlastnost Json formátu.
V minulosti jsem spoustu věcí dělal v xml. Vyjadřovací schopnost formátu Json je podobná XML, ale s formátem Json je vše mnohem jednodušší. Odhaduji, že právě proto se pro programování webů prosadil právě Json a ne xml.
Json je textový formát. Dají se v něm definovat proměnné, objekty a pole. Vezměme příklad shora:
{ nějaké položky }
je definice objektu (třídy v objektovém programování).
jmeno: "xyz"
je definice jedné položky v rámci objektu a konečně
barvy: [ "#ff0000", "#00ff00", "#0000ff" ]
je definice pole pojmenovaného "barvy" se třemi položkami.
Podobně jako je jednoduchý formát Json, jsou jednoduché i jeho parsery. Používám především knihovnu Qt a jazyk C++, pro parsování pak knihovnu QJson. V C++ je výstupem parseru asociativní pole - to sice není součástí jazyka C++, ale není zde problém vytvořit vlastní implementaci operátoru [], takže se práce v C++ třeba od PHP zásadně neliší:
jmeno = data["jmeno"].toString();
U webových prohlížečů je práce s Json ještě jednodušší - formát vychází z přímo jazyka JavaScript. Parsování a serializaci objektů obstará třída JSON:
data = JSON.parse(json); var json = JSON.stringify(data);
Kompletně celá databáze je přístupná přes protokol HTTP. Používají se čtyři příkazy protokolu HTTP: get, put, post a delete. Přes webové rozhraní se dokumenty do databáze ukladají, modifikují i mažou, podobně i samotné databáze. Přes webové rozhraní je jako dokument dostupná i konfigurace serveru. Každý dokument má vlastní URL adresu.
Při práci s databází CouchDB je užitečným pomocníkem curl - na příklady s curlem narazíte prakticky v každém popisu databáze CouchDB.
Dokument z databáze na povelové řádce získáte například takto:
curl -X GET http://localhost:5984/jmeno-databaze/identifikator1
a výsledkem je
{ _id: identifikator1, _rev: abcd-1, jmeno: "abcd" }
Jde o dokument z příkladu výše.
Příkazem put pak můžu dokument modifikovat:
curl \ -X PUT \ -d '{ _id: "identifikator1", _rev: abcd-1, jmeno: "bububu" }' \ -H "Content-type: application/json" \ http://localhost:5984/jmeno-databaze/identifikator1
Podobně se dokument i maže:
curl -X DELETE \ http://localhost:5984/jmeno-databaze/identifikator1?rev=abcd-1
Ke každému dokumentu je možné přibalit libovolný počet příloh. Přílohou může být cokoliv - obrázek, dokument LibreOffice... prostě libovolný formát. Přílohu z databáze získáte velmi snadno přes http protokol, URL přílohy vypadá například takto:
http://localhost:5984/jmeno-databaze/id-dokumentu/priloha.jpg
Vkládání příloh do dokumentu je trochu komplikovanější - příloha se vkládá jako base64 položka Json dokumentu spolu s ostatními potřebnými údaji (mimetype, jméno).
Ačkoliv jednotlivé dokumenty v databázi spolu nemusejí souviset, obvykle se stejně ukládá množství různých dokumentů jednoho typu. Od databáze se pak požaduje, aby například dokázala vypsat všechny dokumenty tohoto typu. V databázi CouchDB k tomu slouží views. Každé view je tvořené jednoduchým (případně složitým) kódem v javascriptu, takzvanou map funkcí:
function(doc) { if (doc.doctype == "task") { emit (doc._id, doc); } }
Databáze CouchDB označuje tuto funkci termínem "map funkce".
Z pohledu programátora view projde všechny dokumenty a do výsledného Json dokumentu vypíše všechny údaje uvedené ve funkci emit(). Ve skutečnosti se celé view ukládá na disk a je aktualizováno při každé změně v databázi. Výstup z view pak vypadá například takto:
{"total_rows":1,"offset":0,"rows":[ {"id":"4e6354","key":["4e6354"],"value":{ "name":"PŘÍPRAVA DŘÍVÍ K ŠTÍPÁNÍ", "begindate":"2013-11-16T00:00:00", "enddate":"2013-11-17T00:00:00"} }, ]}
Funkci emit() se v prvním parametru předává klíč, v druhém parametru hodnota. Klíčem může být i pole - toho se dá využít při různých agregacích. View je vždy seřazené podle klíče, v případě vícesložkového klíče je view setřízené podle všech jeho složek.
K URL adrese view lze připojit množství různých parametrů, například:
Při programování může být někdy na obtíž, že databáze vrací záznamy po kouskách - z view s deseti tisíci záznamů dostanete třeba jen první půltisícovku.
Databáze umí pracovat s vícesložkovými klíči. Parametr pak vypadá například takto:
key=[12, 394]
Bohužel výběr jen podle části klíče je trochu šílený:
startkey=[12,0]&endkey=[12,9999]
Kromě map funkce můžeme u view zadat i "reduce funkci". Obvyklým ekvivalentem redukčních funkcí v SQL je klauzule "group by". Typickým výstupem view s reduce funkcí je třeba aritmetický průměr.
Parametry reduce funkce jsou jiné, než parametry map funkce. Funkce v příkladu spočítá součet všech hodnot podle klíče:
function (key, values, rereduce){ return sum(values); }
Funkce dostává pro každý klíč hodnoty ze všech vět vrácených funkcí map. Uvedený příklad vrací součet všech hodnot předaných funkci.
U reduce funkce je typické, že dostává v poli values pouze část dat - redukce se provádí v několika krocích. Funkce proto musí být schopná zpracovat i své vlastní výstupy - předávané hodnoty se v takovém případě mírně liší a rozeznat je lze podle nastaveného bool parametru rereduce. Zároveň to znamená, že reduce funkce musí být zcela bezestavová, není možné přechovávat jakékoli hodnoty pro použití jinde či jindy.
Pro relačního databázového programátora možná právě reduce funkce znamená nutnost největšího posunu v uvažování. Přesto jsou reduce funkce obvykle velmi jednoduché a zdaleka se svou náročností nepřibližují postupům používaným při programování podobných úloh v Cuda nebo OpenCL na grafických kartách.
CouchDB má některé vlastnosti, díky kterým se vám bude líbit.
Každý dokument může vypadat jinak. Přitom však zůstává možnost řazení a výběru dokumentů z view.
Používáte git? Jednu dobu jsme používali git pro správu firemních dokumentů. CouchDB funguje podobně jako jednovětvový git - každý uživatel může mít vlastní kopii dat, kterou synchronizuje s ostatními až v momentě, kdy je to potřeba. Není problém mít jednu databázi rozloženou přes několik počítačů a data synchronizovat jednou za pár dní - což asi není nejvhodnější řešení pro účetnictví, ale je ideální třeba pro CRM - sdílení kontaktů, poznámek a podobných dat.
V databázi rozložené přes několik počítačů neexistuje žádný oficiální stav - v každé databázi je obvykle v daný moment něco trochu jiného a vůbec to nevadí. Když vytvářím faktury pro svoje zákazníky, obvykle mám ve své databázi potřebné podklady a data o zákaznících mého kolegy mě v daný moment nezajímají - je proto jedno, že je budu mít k dispozici až za pár dní, kdy kolega databázi sesynchronizuje.
Stejně tak obchodník na služební cestě po Sibiři může do své databáze vkládat spoustu poznámek a je celkem jedno, že jeho spolupracovníci v Česku se k jeho poznámkám dostanou až po několika dnech či týdnech, až obchodník z Ruska přijede s novými kontrakty a objednávkami. V zásadě ani nic nebrání tomu, aby byli obchodníci na Sibiři dva a synchronizovali svá data jen mezi sebou - a pokud se jeden vrátí do civilizace, může data druhého obchodníka přivézt do Česka ve své databázi s sebou.
Při synchronizaci dat několika různých databází zákonitě vznikají konflikty - jeden dokument je upravený na několika databázových instancích zároveň. CouchDB disponuje jednoduchým mechanismem pro řešení konfliktů.
Ke konfliktům v praxi kupodivu dochází poměrně málo. Jednak lze konfliktům předejít vhodným návrhem databáze a druhak mají uživatelé velmi často ve zvyku pracovat v okruhu několika málo dokumentů a s ostatními uživateli většinou žádné dokumenty intenzivně nesdílejí.
Při změně ukládá databáze vždy novou verzi dokumentu. Neznamená to, že by se dala databáze využít pro ukládání historie dokumentů - při pročištění databáze se staré, nepotřebné verze smažou. Ale dokumenty, u kterých se při replikaci vyskytnul konflikt, mají uložené všechny konfliktní verze v databázi, dokud se konflikt nevyřeší. Díky tomu je možné i konfliktní dokumenty v databázi měnit a vyřešit konflikty později. Konfliktní dokument má obvykle dvě různé větve (případně více větví), konflikt se řeší až na konci těchto větví.
S databází můžete pracovat i bez ohledu na konflikty, ale hrozí, že část dat neuvidíte, dokud konflikty nevyřešíte. Pro řešení konfliktů je potřeba vytvořit vlastní jednoduchou úlohu - prakticky stačí prohlédnout kolidující údaje, rozhodnout, který údaj je platný a ostatní smazat.
U jedné instance databáze je řešení konfliktů mnohem přímočařejší - s dokumentem ukládáte i číslo verze, kterou modifikujete. Pokud mezi přečtením dokumentu a jeho zápisem do databáze došlo ke změně, dokument nelze uložit. Řešením je přečíst dokument znovu, modifikovat novou, přečtenou verzi a pokusit se o opětovný zápis do databáze.
Databázi CouchDB je velmi jednoduché nainstalovat a používat. Databázový server je součástí prakticky jakékoliv distribuce a pro základní používání jej stačí opravdu jen nainstalovat. Jakmile je databázový server nainstalovaný a spuštěný, je k dispozici pro kohokoliv v počítači localhost zcela bez omezení. Pokud chcete používat server v síti, je samozřejmě potřeba jej nastavit pečlivěji.
V databázi CouchDB nespočítáte ani jedna plus jedna. Ne že by to nešlo, ale v CouchDB neexistuje ekvivalent prostého SQL dotazu:
select a+1 from tabulka;
Pokud chcete po CouchDB základní počty z první třídy, musíte si naprogramovt vlastní view.
Stejně jako v CouchDB neexistuje možnost jednoduchých dotazů s byť i jen nejzákladnějšími výpočty, neexistuje zde ani ekvivalent jiného SQL dotazu:
delete from table where datum < '2015-01-01';
Pokud chcete v CouchDB smazat hromadně záznamy podle nějakého výběrového kritéria, jste namydlení. Nezbývá, než vytvořit view, načíst si jeho výsledky a potom záznamy jeden po druhém každý extra smazat.
Úplně stejně narazíte, pokud budete chtít hromadně opravit některou položku v záznamech, například opravit datum.
S nemožností přistupovat k záznamům hromadně souvisí i rychlost. Řečeno v termínech relačních databází se každý záznam v CouchDB zpracovává ve vlastní transakci. Takže zatímco celý výše uvedený SQL příkaz delete je proveden v řádu desítek či stovek milisekund, v databázi CouchDB budete mazat a mazat a mazat... třeba hodinu. Nebo dvě. Nebo tři, když nainstalujete databázi CouchDB do počítače BeagleBone nebo Raspberry Pi.
Vede to v některých případech k nechutně komplikovaným postupům - v našem komunikačním serveru ukládáme do databáze různé chybové stavy. Úloha "smaž všechny záznamy o chybách starší než měsíc" běží v samostatném vlákně, protože jinak by byl celý komunikační systém obyčejnou, nedůležitou údržbou zablokovaný na hodně dlouho (desítky minut, hodiny)!
Relační databáze jsou velmi specializovaný nástroj - vztahy mezi daty jsou zde tak důležitou vlastností, že tyto databáze mají tuto vlastnost obsaženou přímo v názvu - říkáme o nich, že jsou relační.
S CouchDB musíte vztahy mezi řešit sami, na jakoukoliv automatickou podporu zapomeňte. Smažete-li v databázi CouchDB zaměstnance, jeho děti musíte ze svého světa vymazat ručně.
Vztahy mezi dokumenty se v CouchDB dají udržovat dvojím způsobem: buď uvedete přímo do dokumentu identifikátory dalších dokumentů ve vztahu (vede to k častějším konfliktům), nebo vytvoříte pro dva dokumenty třetí, vazební dokument (pracnější, vede to častěji ke "zmizení" vazby).
Ať už budete řešit vazby mezi dokumenty tak nebo onak, v distribuovaném prostředí se budete občas dostávat do situací, kdy se vazby mezi dokumenty znásobí nebo kompletně zmizí - je to způsobené vícenásobnými zásahy do dat z několika různých míst a jejich následným sehráním do jednoho společného stavu. Je prakticky nemožné tomu zamezit.
Aplikace postavená nad CouchDB proto obvykle potřebuje nějakého "údržbáře", který bude narušené vztahy mezi dokumenty aktivně vyhledávat a opravovat.
Zdálo by se, že jednoduchým řešením je ukládat všechna relevantní data spolu s dokumentem, tj. součástí dokumentu "zaměstnanec" je i list se seznamem jeho dětí - ale to vede k velkému množství konfliktů, takže nakonec ještě rádi rozsekáte dokumenty na menší kusy a dopíšete kód, který se vám bude o vazby starat.
CouchDB se od relačních databází liší. Je řada aplikací, kde je nasazení CouchDB na místě a oproti obvyklým relačním databázím vám její nasazení přinese užitek - například v jednoduchosti nastavení, ve snadné spolupráci s webem či v možnosti rozložit databázi přes několik počítačů. Doufám, že vám můj článek vnesl do problematiky trochu více světla.
Ani běžný LAMP programátor by s CouchDB neměl mít zásadní problém - stejně jako CouchDB, ani běžný LAMP programátor nezná referenční integritu, agregace řeší v cyklu a na join říká "žjóóóva". Označení "běžný LAMP programátor" zde chápejte podobně jako "běžný prací prášek" či "běžný čistící prostředek" - věřím, že vy všichni jste značkoví, už jen proto, že jste se prokousali tímto článkem až na konec.