Obsah
1. Programovací jazyk Clojure a databáze (1.část)
2. Knihovna org.clojure/java.jdbc
3. Vytvoření testovací databáze s jednou tabulkou
4. První demonstrační příklad – provedení dotazu (SELECT) do SQLite databáze
5. Druhý demonstrační příklad – zachycení výjimky a předání parametrů do dotazu SELECT
6. Třetí demonstrační příklad – zpracování výsledků dotazu
7. Čtvrtý demonstrační příklad – příkaz INSERT (naivní varianta)
8. Pátý demonstrační příklad – příkazy INSERT provedené v transakci
9. Repositář s demonstračními příklady
1. Programovací jazyk Clojure a databáze (1.část)
V předchozích šesti částech [1][2][3][4][5][6] seriálu o nástroji Leiningen i o programovacím jazyku Clojure jsme se již zabývali několika tématy: samotným použitím Leiningenu, tvorbou webových aplikací využívajících knihovnu Clojure Ring a taktéž i knihovnou Hiccup, kterou je možné použít pro relativně snadné a přitom (alespoň podle mého názoru) velmi elegantní generování dynamických HTML a XHTML stránek. V mnoha aplikacích se však setkáme i s dalším dosti typickým požadavkem. Tím je přístup k relačním či (v některých případech) nerelačním databázím. Pro programovací jazyk Clojure vzniklo v poměrně krátké době již velké množství knihoven a frameworků usnadňujících přístup k různým typům databází, setkat se dokonce můžeme i s několika typy doménově specifických jazyků (DSL) nahrazujících klasické SQL. My se dneska seznámíme s nízkoúrovňovým přístupem k relačním databázím, který je realizován knihovnou org.clojure/java.jdbc.
2. Knihovna org.clojure/java.jdbc
Jednou z mnoha knihoven umožňujících provádění operací s relačními databázemi je knihovna nazvaná org.clojure/java.jdbc. Zjednodušeně řečeno je možné říci, že tato knihovna obaluje původní Javovské rozhraní JDBC, které je pro většinu reálných aplikací nevyhovující (což do značné míry souvisí i se stářím tohoto API). V knihovně org.clojure/java.jdbc nalezneme funkce pro provádění základních databázových operací SELECT, INSERT, UPDATE i DELETE, přičemž je sémantika těchto funkcí navržena takovým způsobem, aby odpovídala filozofii programovacího jazyka Clojure. V praxi to znamená, že se namísto těžkopádného ResultSet vrací výsledky ve formě sekvence map, jsou jednoduše podporovány transakce, není zapotřebí se starat o uvolňování prostředků, příkaz SELECT je založen na třídě PreparedStatement (což je umně skryto), takže lze předávat parametry dotazům bez nutnosti ručního skládání řetězce s SQL příkazem atd. Pro komplikovanější operace není však tato knihovna příliš vhodná, což však nevadí, protože k ní existuje poměrně velké množství alternativních řešení, z nichž některé si popíšeme dále.
3. Vytvoření testovací databáze s jednou tabulkou
Některé základní vlastnosti knihovny java.jdbc si budeme ukazovat na pětici demonstračních příkladů, které budou přistupovat k tabulce nazvané obsazeni uložené v souborové databázi SQLite. Předpokladem tedy je, že na systému je nainstalován balíček sqlite či sqlite3, což je většinou splněno, neboť se tato souborová databáze používá v několika populárních desktopových aplikacích. Zmíněná tabulka obsazeni má velmi jednoduchou strukturu, což je ostatně patrné i z následující deklarace:
create table obsazeni ( id integer primary key, postava text not null, jmeno text not null );
Tabulka je naplněna pěti záznamy – jmény pěti postav z jednoho známého divadelního představení:
insert into obsazeni(id, postava, jmeno) values (1, 'inspektor', 'Trachta'); insert into obsazeni(id, postava, jmeno) values (2, 'praktikant', 'Hlavacek'); insert into obsazeni(id, postava, jmeno) values (3, 'tovarnik', 'Bierhanzel'); insert into obsazeni(id, postava, jmeno) values (4, 'tovarnik', 'Meyer'); insert into obsazeni(id, postava, jmeno) values (5, 'stevard', 'Vaclav Kotek');
Tuto databázi s jedinou tabulkou vytvoříme následujícím příkazem využívajícím skript create_table.sql, který je součástí všech pěti demonstračních příkladů (viz též kapitolu číslo 9):
cat create_table.sql | sqlite3 test.db
Výsledkem bude binární soubor nazvaný test.db, který musí být umístěn v adresáři s projektem, přesněji řečeno v tom adresáři, ve kterém se nachází i soubor project.clj. Obsah tabulky si můžeme snadno vypsat příkazem:
sqlite3 test.db "select * from obsazeni;"
Na standardním výstupu by se mělo objevit pět řádků:
1|inspektor|Trachta 2|praktikant|Hlavacek 3|tovarnik|Bierhanzel 4|tovarnik|Meyer 5|stevard|Vaclav Kotek
Nyní již je vše připravené pro použití této databáze v demonstračních příkladech.
4. První demonstrační příklad – provedení dotazu (SELECT) do SQLite databáze
Pojďme si nyní vyzkoušet provést jednoduché operace s právě vytvořenou databází. Nejprve vygenerujeme kostru nového projektu již známým příkazem:
lein new sqltest1
Projektový soubor nazvaný project.clj je nutné upravit způsobem, který již známe z předchozích částí tohoto seriálu. Musíme totiž doplnit knihovny, na nichž demonstrační příklad závisí. V tomto případě se jedná o knihovny org.clojure/java.jdbc (vrstva nad JDBC) a taktéž o driver k databázi SQLite představovaný knihovnou org.xerial/sqlite-jdbc. Projektový soubor by měl vypadat následovně (dva nové řádky jsou zvýrazněny):
(defproject sqltest1 "0.1.0-SNAPSHOT" :description "FIXME: write description" :url "http://example.com/FIXME" :license {:name "Eclipse Public License" :url "http://www.eclipse.org/legal/epl-v10.html"} :dependencies [[org.clojure/clojure "1.5.1"] [org.clojure/java.jdbc "0.3.5"] [org.xerial/sqlite-jdbc "3.7.2"]] :main ^:skip-aot sqltest1.core :target-path "target/%s" :profiles {:uberjar {:aot :all}})
Poté již můžeme všechny závislé knihovny stáhnout:
lein deps
Povšimněte si, že se do podadresáře target stáhnuly i binární (resp. přesněji řečeno nativní) soubory – drivery nutné pro práci s databází SQLite.
Zajímavější je zdrojový kód demonstračního příkladu. Nejprve si tento kód uvedeme celý a poté si jednotlivé funkce vysvětlíme podrobněji:
(ns sqltest1.core (:gen-class)) (require '[clojure.pprint :as pprint]) (require '[clojure.java.jdbc :as jdbc]) ; Struktura reprezentujici zpusob pripojeni do vybrane databaze (def test-db {:classname "org.sqlite.JDBC" :subprotocol "sqlite" :subname "test.db" }) (defn read-whole-table "Nacteni obsahu vybrane tabulky." [db-spec table-name] (jdbc/query db-spec (str "select * from " table-name))) (defn list-table "Vypis obsahu vybrane tabulky." [db-spec table-name] (let [results (read-whole-table db-spec table-name)] (pprint/pprint results))) (defn -main "Vstupni bod do aplikace." [& args] (list-table test-db "obsazeni"))
Popis jednotlivých funkcí a datových struktur:
# | Funkce | Význam |
---|---|---|
1 | -main | vstupní bod do aplikace zavolaný příkazem lein run; případné argumenty se ignorují |
2 | list-table | získá výsledky ve formě sekvence map z databáze a vytiskne je v čitelné podobě na standardní výstup |
3 | read-whole-table | připojí se k databázi a provede zadaný příkaz SELECT |
4 | clojure.java.jdbc/query | provedení příkazu SELECT, vyžaduje specifikaci databáze a tvar SELECTu (zde v řetězcové podobě) |
5 | test-db | datová struktura (mapa) popisující způsob připojení k databází; v Javě by se jednalo o kombinaci jména driveru a connection stringu |
Obsah datové struktury test-db se bude v případě jiných databází než je SQLite lišit, protože mnoho databází očekává zadání jména a hesla, odkazu na databází atd.
5. Druhý demonstrační příklad – zachycení výjimky a předání parametrů do dotazu SELECT
Ve druhém demonstračním příkladu si ukážeme dvě změny provedené ve zdrojovém kódu příkladu prvního. První změnou je předání parametru (či parametrů) do dotazu SELECT. Ve skutečnosti se totiž v knihovně java.jdbc pro každý dotaz interně vytváří objekt typu PreparedStatement, který reprezentuje jak vlastní tvar dotazu, tak i jeho případné parametry. Díky této vlastnosti není nutné příkaz SELECT s parametry skládat ručně, což je nepohodlné a taktéž i náchylné na chyby popř. v extrémních případech na útoky typu SQL Injection. Při volání funkce java.jdbc/query je možné druhý parametr, který byl původně reprezentován řetězcem, nahradit za vektor, jehož první prvek je opět řetězec a další prvky jsou hodnoty jednotlivých parametrů reprezentovaných v dotazu otazníky. Druhé vylepšení spočívá v zachycení výjimek, které mohou být vyhozeny (throw) při komunikaci s databází. V Javě se používá konstrukce try-block-finally, která je v programovacím jazyku Clojure nahrazena za speciální formu (try expr* catch-clause* finally-clause?):
(ns sqltest2.core (:gen-class)) (require '[clojure.pprint :as pprint]) (require '[clojure.java.jdbc :as jdbc]) ; Struktura reprezentujici zpusob pripojeni do vybrane databaze (def test-db {:classname "org.sqlite.JDBC" :subprotocol "sqlite" :subname "test.db" }) (defn read-whole-table "Nacteni obsahu vybrane tabulky." [db-spec table-name] (jdbc/query db-spec [(str "select * from " table-name " where postava=? order by id") "tovarnik"])) (defn list-table "Vypis obsahu vybrane tabulky." [db-spec table-name] (try (let [results (read-whole-table db-spec table-name)] (pprint/pprint results)) (catch Exception e (println "error accessing database '" (:subname db-spec) "'!")))) (defn -main "Vstupni bod do aplikace." [& args] (list-table test-db "obsazeni"))
Ze zdrojového kódu je patrné, že blok catch je vnořen do struktury speciální formy try. Totéž by platilo i pro případný blok finally.
6. Třetí demonstrační příklad – zpracování výsledků dotazu
V prvním i ve druhém demonstračním příkladu se výsledky dotazu jednoduše vypsaly na standardní výstup s využitím funkce clojure.pprint/pprint, která vrácenou sekvenci pro lepší čitelnost naformátovala. V reálných aplikacích je však nutné sekvenci představující výsledek dotazu většinou zpracovat odlišným způsobem. Nabízí se dvě základní možnosti – použití generátorové notace seznamu reprezentované makrem for (což je čistě funkcionální a tudíž i preferovaný přístup) popř. použití programové smyčky realizované například s využitím makra doseq. Pro jednoduchost si v dnešním třetím demonstračním příkladu ukážeme právě využití zmíněného makra doseq, kterým se postupně projde výsledkem dotazu a pro každý výsledek (řádek) se zavolá funkce (println (format …)), které se předají prvky uložené pod klíči :postava a :jmeno. Povšimněte si způsobu získání hodnot těchto prvků, kde jsme se elegantně zbavili nutnosti volání funkce get, které většinou není pro Clojure idiomatické (get je výhodné použít v případě, že je nutné nahradit hodnotu neexistujícího prvku mapy či sekvence nějakou speciální hodnotou):
(ns sqltest3.core (:gen-class)) (require '[clojure.pprint :as pprint]) (require '[clojure.java.jdbc :as jdbc]) ; Struktura reprezentujici zpusob pripojeni do vybrane databaze (def test-db {:classname "org.sqlite.JDBC" :subprotocol "sqlite" :subname "test.db" }) (defn read-whole-table "Nacteni obsahu vybrane tabulky." [db-spec table-name] (jdbc/query db-spec (str "select * from " table-name " order by postava, jmeno"))) (defn list-table "Vypis obsahu vybrane tabulky." [db-spec table-name] (try (let [results (read-whole-table db-spec table-name)] (doseq [result results] (println (format "%-10s %-20s" (:postava result) (:jmeno result))))) (catch Exception e (println "error accessing database '" (:subname db-spec) "'!")))) (defn -main "Vstupni bod do aplikace." [& args] (list-table test-db "obsazeni"))
7. Čtvrtý demonstrační příklad – příkaz INSERT (naivní varianta)
Jak jsme si řekli již ve druhé kapitole, podporuje knihovna org.clojure/java.jdbc kromě operace SELECT i ostatní tři základní databázové operace, tj. INSERT, DELETE a UPDATE. V dnešním čtvrtém demonstračním příkladu je ukázáno použití operace INSERT, která je několikrát zavolána ve funkci nazvané fill-in-table. Nejprve je nutné upozornit na to, že operace INSERT je představována funkcí clojure.java.jdbc/insert!, kde vykřičník na konci názvu upozorňuje na fakt, že se provedením této operace změní stav aplikace (povšimněte si, že i všechny podobné funkce v Clojure knihovnách dodržují stejnou konvenci, typickým příkladem jsou funkce reset!, swap! či conj!). Data ukládaná do databáze mohou být funkci org.clojure/java.jdbc předána různými způsoby. Zde využíváme ten nejjednodušší způsob – každý záznam je představovaný mapou, takže je zřejmé, které sloupce tabulky odpovídají jednotlivým prvkům v mapě (není tedy nutné ručně psát příkaz INSERT v jazyku SQL):
(ns sqltest4.core (:gen-class)) (require '[clojure.pprint :as pprint]) (require '[clojure.java.jdbc :as jdbc]) ; Struktura reprezentujici zpusob pripojeni do vybrane databaze (def test-db {:classname "org.sqlite.JDBC" :subprotocol "sqlite" :subname "test.db" }) (def table-name "obsazeni") (defn read-whole-table "Nacteni obsahu vybrane tabulky." [db-spec table-name] (jdbc/query db-spec (str "select * from " table-name " order by postava, jmeno"))) (defn list-table "Vypis obsahu vybrane tabulky." [db-spec table-name] (try (let [results (read-whole-table db-spec table-name)] (doseq [result results] (println (format "%-10s %-20s" (:postava result) (:jmeno result))))) (catch Exception e (println "error accessing database '" (:subname db-spec) "'!")))) (defn fill-in-table "Zapis dat do tabulky." [db-spec table-name] (jdbc/insert! db-spec table-name {:postava "inspektor" :jmeno "Trachta"}) (jdbc/insert! db-spec table-name {:postava "praktikant" :jmeno "Hlavacek"}) (jdbc/insert! db-spec table-name {:postava "tovarnik" :jmeno "Bierhanzel"}) (jdbc/insert! db-spec table-name {:postava "tovarnik" :jmeno "Meyer"}) (jdbc/insert! db-spec table-name {:postava "stevard" :jmeno "Vaclav Kotek"})) (defn -main "Vstupni bod do aplikace." [& args] (fill-in-table test-db table-name) (list-table test-db table-name))
8. Pátý demonstrační příklad – příkazy INSERT provedené v transakci
Použití funkce clojure.java.jdbc/insert! způsobem naznačeným v předchozí kapitole sice vypadá velmi jednoduše, ovšem za tuto jednoduchost platíme mnohdy až neúměrně dlouhým časem provádění této operace. Ve chvíli, kdy se databáze naplňuje množstvím nových záznamů je nutné zajistit větší rychlost vkládání dat do tabulek. To je relativně snadné zařídit takovým způsobem, že se příkazy INSERT volají v transakci a v dnešním pátém a současně i posledním demontračním příkladu je naznačeno, jak lze takové transakce relativně snadno implementovat. Všechny operace, které se mají provádět v transakci, jsou umístěny do funkce nazvané clojure.java.jdbc/with-db-transaction. Navíc je nutné u příkazů INSERT pomocí nepovinného parametru :transaction? true specifikovat, že se skutečně mají provést v transakci. Mimochodem, název funkce clojure.java.jdbc/with-db-transaction opět plně odpovídá konvencím Clojure, kde nalezneme množství podobných názvů, například ve chvíli, kdy se pracuje se soubory atd. Funkce with- například dovolují izolovat změnu stavu aplikace, pracovat s nějakým prostředkem (souborem, transakcí) apod.:
(ns sqltest5.core (:gen-class)) (require '[clojure.pprint :as pprint]) (require '[clojure.java.jdbc :as jdbc]) ; Struktura reprezentujici zpusob pripojeni do vybrane databaze (def test-db {:classname "org.sqlite.JDBC" :subprotocol "sqlite" :subname "test.db" }) (def table-name "obsazeni") (defn read-whole-table "Nacteni obsahu vybrane tabulky." [db-spec table-name] (jdbc/query db-spec (str "select * from " table-name " order by postava, jmeno"))) (defn list-table "Vypis obsahu vybrane tabulky." [db-spec table-name] (try (let [results (read-whole-table db-spec table-name)] (doseq [result results] (println (format "%-10s %-20s" (:postava result) (:jmeno result))))) (catch Exception e (println "error accessing database '" (:subname db-spec) "'!")))) (defn delete-table-content [db-spec table-name] (jdbc/delete! db-spec table-name [])) (defn fill-in-table "Zapis dat do tabulky." [db-spec table-name] (jdbc/with-db-transaction [connection db-spec] (jdbc/insert! connection table-name {:postava "inspektor" :jmeno "Trachta"} :transaction? true) (jdbc/insert! connection table-name {:postava "praktikant" :jmeno "Hlavacek"} :transaction? true) (jdbc/insert! connection table-name {:postava "tovarnik" :jmeno "Bierhanzel"} :transaction? true) (jdbc/insert! connection table-name {:postava "tovarnik" :jmeno "Meyer"} :transaction? true) (jdbc/insert! connection table-name {:postava "stevard" :jmeno "Vaclav Kotek"} :transaction? true))) (defn -main "Vstupni bod do aplikace." [& args] (delete-table-content test-db table-name) (fill-in-table test-db table-name) (list-table test-db table-name))
9. Repositář s demonstračními příklady
Všech pět dnes popsaných demonstračních příkladů bylo, podobně jako v předchozí části tohoto článku, uloženo do GIT repositáře dostupného na adrese https://github.com/tisnik/clojure-examples (dříve popsané příklady budou přidány později). V tabulce zobrazené pod tímto odstavcem naleznete na jednotlivé příklady přímé odkazy:
10. Odkazy na Internetu
- java.jdbc API Reference
https://clojure.github.io/java.jdbc/ - Hiccup
https://github.com/weavejester/hiccup - Clojure Ring na GitHubu
https://github.com/ring-clojure/ring - A brief overview of the Clojure web stack
https://brehaut.net/blog/2011/ring_introduction - Getting Started with Ring
http://www.learningclojure.com/2013/01/getting-started-with-ring.html - Getting Started with Ring and Compojure – Clojure Web Programming
http://www.myclojureadventure.com/2011/03/getting-started-with-ring-and-compojure.html - Leiningen: nástroj pro správu projektů napsaných v Clojure
http://www.root.cz/clanky/leiningen-nastroj-pro-spravu-projektu-napsanych-v-clojure/ - Leiningen: nástroj pro správu projektů napsaných v Clojure (2)
http://www.root.cz/clanky/leiningen-nastroj-pro-spravu-projektu-napsanych-v-clojure-2/ - Leiningen: nástroj pro správu projektů napsaných v Clojure (3)
http://www.root.cz/clanky/leiningen-nastroj-pro-spravu-projektu-napsanych-v-clojure-3/ - Leiningen: nástroj pro správu projektů napsaných v Clojure (4)
http://www.root.cz/clanky/leiningen-nastroj-pro-spravu-projektu-napsanych-v-clojure-4/ - Leiningen: nástroj pro správu projektů napsaných v Clojure (5)
http://www.root.cz/clanky/leiningen-nastroj-pro-spravu-projektu-napsanych-v-clojure-5/ - Leiningen: nástroj pro správu projektů napsaných v Clojure (6)
http://www.root.cz/clanky/leiningen-nastroj-pro-spravu-projektu-napsanych-v-clojure-6/ - Unit Testing in Clojure
http://nakkaya.com/2009/11/18/unit-testing-in-clojure/ - Testing in Clojure (Part-1: Unit testing)
http://blog.knoldus.com/2014/03/22/testing-in-clojure-part-1-unit-testing/ - API for clojure.test – Clojure v1.6 (stable)
https://clojure.github.io/clojure/clojure.test-api.html - Leiningen: úvodní stránka
http://leiningen.org/ - Leiningen: Git repository
https://github.com/technomancy/leiningen - leiningen-win-installer
http://leiningen-win-installer.djpowell.net/ - Clojure 1: Úvod
http://www.root.cz/clanky/clojure-aneb-jazyk-umoznujici-tvorbu-bezpecnych-vicevlaknovych-aplikaci-pro-jvm/ - Clojure 2: Symboly, kolekce atd.
http://www.root.cz/clanky/clojure-aneb-jazyk-umoznujici-tvorbu-bezpecnych-vicevlaknovych-aplikaci-pro-jvm-2-cast/ - Clojure 3: Funkcionální programování
http://www.root.cz/clanky/clojure-aneb-jazyk-umoznujici-tvorbu-bezpecnych-vicevlaknovych-aplikaci-pro-jvm-3-cast-funkcionalni-programovani/ - Clojure 4: Kolekce, sekvence a lazy sekvence
http://www.root.cz/clanky/clojure-aneb-jazyk-umoznujici-tvorbu-bezpecnych-vicevlaknovych-aplikaci-pro-jvm-4-cast-kolekce-sekvence-a-lazy-sekvence/ - Clojure 5: Sekvence, lazy sekvence a paralelní programy
http://www.root.cz/clanky/clojure-a-bezpecne-aplikace-pro-jvm-sekvence-lazy-sekvence-a-paralelni-programy/ - Clojure 6: Podpora pro paralelní programování
http://www.root.cz/clanky/programovaci-jazyk-clojure-6-futures-nejsou-jen-financni-derivaty/ - Clojure 7: Další funkce pro paralelní programování
http://www.root.cz/clanky/programovaci-jazyk-clojure-7-dalsi-podpurne-prostredky-pro-paralelni-programovani/ - Clojure 8: Identity, stavy, neměnné hodnoty a reference
http://www.root.cz/clanky/programovaci-jazyk-clojure-8-identity-stavy-nemenne-hodnoty-a-referencni-typy/ - Clojure 9: Validátory, pozorovatelé a kooperace s Javou
http://www.root.cz/clanky/programovaci-jazyk-clojure-9-validatory-pozorovatele-a-kooperace-mezi-clojure-a-javou/ - Clojure 10: Kooperace mezi Clojure a Javou
http://www.root.cz/clanky/programovaci-jazyk-clojure-10-kooperace-mezi-clojure-a-javou-pokracovani/ - Clojure 11: Generátorová notace seznamu/list comprehension
http://www.root.cz/clanky/programovaci-jazyk-clojure-11-generatorova-notace-seznamu-list-comprehension/ - Clojure 12: Překlad programů z Clojure do bajtkódu JVM I
http://www.root.cz/clanky/programovaci-jazyk-clojure-12-preklad-programu-z-clojure-do-bajtkodu-jvm/ - Clojure 13: Překlad programů z Clojure do bajtkódu JVM II
2) http://www.root.cz/clanky/programovaci-jazyk-clojure-13-preklad-programu-z-clojure-do-bajtkodu-jvm-pokracovani/ - Clojure 14: Základy práce se systémem maker
http://www.root.cz/clanky/programovaci-jazyk-clojure-14-zaklady-prace-se-systemem-maker/ - Clojure 15: Tvorba uživatelských maker
http://www.root.cz/clanky/programovaci-jazyk-clojure-15-tvorba-uzivatelskych-maker/ - Clojure 16: Složitější uživatelská makra
http://www.root.cz/clanky/programovaci-jazyk-clojure-16-slozitejsi-uzivatelska-makra/ - Clojure 17: Využití standardních maker v praxi
http://www.root.cz/clanky/programovaci-jazyk-clojure-17-vyuziti-standardnich-maker-v-praxi/ - Clojure 18: Základní techniky optimalizace aplikací
http://www.root.cz/clanky/programovaci-jazyk-clojure-18-zakladni-techniky-optimalizace-aplikaci/ - Clojure 19: Vývojová prostředí pro Clojure
http://www.root.cz/clanky/programovaci-jazyk-clojure-19-vyvojova-prostredi-pro-clojure/ - Clojure 20: Vývojová prostředí pro Clojure (Vimu s REPL)
http://www.root.cz/clanky/programovaci-jazyk-clojure-20-vyvojova-prostredi-pro-clojure-integrace-vimu-s-repl/ - Clojure 21: ClojureScript aneb překlad Clojure do JS
http://www.root.cz/clanky/programovaci-jazyk-clojure-21-clojurescript-aneb-preklad-clojure-do-javascriptu/