Slick

Světy in-memory kolekcí a relačních databází se vždy velice rozcházely. Expresivní jazyky jako Scala mám umožňují s kolekcemi pracovat skoro až deklarativně. Na druhou stranu práce s databázemi byla vždycky odlišná. Buď to byly neohrabané SQL řetězce nebo touha všechno schovat za závojem ORM a zapomenout.

SLICK je knihovna, která spojuje tyto dva světy a umožňuje nám ve Scale pracovat s relačními databázemi, jako kdyby to byly běžné kolekce v paměti. Místo v SQL můžeme dotazy psát přímo ve Scale. Získáme tím typovou bezpečnost a kompozici dotazů, ale stále budeme mít plnou sílu relačních databází na dosah ruky.

Hlavní vlastnosti SLICKu jsou:

  • jednoduchý: nesnaží se skrýt vysokoúrovňovou relační algebru za nízkoúrovňové OOP rozhraní, ale poskytuje přímo přístup k relacím
  • explicitní: dotaz se vykoná, jenom když si to vývojář vyžádá a přenáší se jenom data, která skutečně potřebuje, neprovádí lazy loading a umí načítat méně než celé objekty, díky tomu má předvídatelný výkon
  • funkcionální: výsledkem dotazu jsou vždy neměnná data, žádné proxy objekty
  • composable: SLICK dotazy jsou na rozdíl od SQL regulární a jdou libovolně kombinovat, což zvyšuje znovupoužitelnost
  • typově bezpečný: typy argumentů a výsledků jsou předem známé a jsou kontrolované kompilátorem
  • odolný vůči SQL injection: SQL dotazy jsou sestavovány SLICKem, který se postará o potřebné escapovaní

Současná verze SLICKu (1.0.0-RC1) je založena na JDBC a může pracovat pouze s relačními databázemi, které mají odpovídající driver. V následujících verzích přibude podpora i pro nerelační zdroje dat. Jako první je v plánu MongoDB, se kterým by si měl SLICK začít rozumět v následujících měsících. Avšak u Monga plány tvůrců SLICKu nekončí a v budoucnu se setkáme s podporou dalších NoSQL databází, SQL driverů, které nejdou založené na JDBC (Android) a ostatními datovými zdroji včetně Web Services.

To všechno bude možné proto, že SLICK modeluje zdroje dat jako abstraktní kolekce a tento model odpovídá velkému množství databází (i když některé můžou podporovat jenom omezené množství funkcí).

SLICK tedy může fungovat jako pojící prvek aplikací, protože nabídne stejné rozhraní pro práci s in-memory daty a všemi databázemi, službami a API.


Pokud chceme začít používat SLICK, musíme nejprve vytvořit objekt typu Database, který abstrahuje možné způsoby připojení k databázi. Dnes jde jenom o volbu mezi jednotlivými JDBC drivery a výběr mezi DriverManager a DataSource), ale v budoucnu to bude objekt Database, který určí s jakým zdrojem dat budeme pracovat a když ho zaměníme za jiný, naše dotazy budou i nadále fungovat.

Metoda withSession si vezme jedno spojení z connection poolu, vykoná s ním blok kódu a na konci automaticky session uzavře a spojení vrátí do poolu.

Metoda withTransaction pracuje podobně jako withSession, na začátku bloku začne databázovou transakci a na konci ji automaticky potvrdí (transakce je zrušena výjimkou nebo metodou session.rollback()).


SLICK nabízí tři různé způsoby dotazování: Lifted embedding, Direct embedding a Plain SQL.

Lifted embedding

Lifted embedding je nejstarší a nejvyspělejší část SLICKu, která vychází z projektu ScalaQuery.

Tento přístup je založen na tom, že všechny hodnoty ze kterých setavuji dotaz jsou zabalené ( lifted) do typu Rep[T]. Sloupec ( Column[T]) je Rep[T], dotaz ( Query[T]) odpovídá Rep[Seq[T]]. To nám umoňuje psát dotazy, které vypadají jako že mají obyčejné aritmetické operátory, ale ve skutečnosti jsou to metody na potomcích typu Rep[T], které generují a sestavují dotaz.

Než můžeme začít, musíme ručně nadefinovat objekt reprezentující schéma tabulky. Tento objekt definuje jména a typy sloupců, primární klíče, cizí klíče a indexy a mapuje databázové řádky na TupleN nebo case class (z toho důvodu tabulka nemůže mít víc jak 22 sloupců).

Díky tomu, že je celé schéma specifikované v aplikaci, můžeme nechat SLICK, aby všechny příslušné tabulky vytvořil sám.

Definice tabulky může vypadat například následovně:

Příklad začíná importem, protože musíme použít objekt Table správné databáze. Je to kvůli tomu, že databáze se chovají jinak a toho je způsob jak schopnosti databáze vyjádřit jejím typem. Typ databáze tedy musí být známý staticky. Pokud není musíme použít cake pattern.

První objekt definuje tabulku Suppliers s unikátním klíčem, která je mapovaná na Tuple6. Druhý objekt mapuje tabulku Coffees na case class Coffee a navíc definuje cizí klíč.

Všechny sloupce, které mohou obsahovat hodnotu null musí být definovány jako Option[T]. Pokud tomu tak není a databáze vrátí null, dotaz skončí výjimkou.

Dotazování:

Pro dotazování slouží objekt Query, který ale nikdy nemusíme zmiňovat přímo, neboť je na něj implicitně zkonvertována každá tabulka.

Samotné dotazy se pak tvoří přes for comprehension.

A protože for comprehension je jenom hezčí syntaxe pro metody map, flatMap a filter, je možné stejný dotaz zapsat i takto:

Všimněte si, že se porovnání provádí pomocí zvláštního operátoru ===. To je proto, že obyčejné == porovnává rovnost objektů a není možné ho přepsat. My nechceme porovnávat objekty přímo, ale databázové sloupce, které tyto Rep objekty reprezentují.

Je důležité vědět, že takto jenom konstruujeme objekt peprezentující dotaz, ale nevykonáváme ho. To se musí provést explicitně.

Joiny:

Spojování tabulek se ve SLICKu vyjadřuje naprosto přirozeně:

Pokud jsme definovali metodu supplier v tabulce Coffees pomocí metody foreignKey můžeme použít jednodušší znovupoužitelný zápis. výsledek obou dotazů je zcela identický.

Joinovat můžeme pomocí explicitní metody. To je nutné pro left, right nebo outer join

Všechny tyto dotazy přesně v duchu SQL vracejí plochá dvojrozměrná data. Ale někdy by se nám spíš hodilo, kdyby vrátil vnořené kolekce – například seznam dodavatelů a u každého z nich vnořený seznam dodávaného zboží. Přesně to je plánováno pro následující verze SLICKu.

Existuje několik způsobů jak dotaz položit. SLICK se vždy postará, aby byl zvolen ta nejefektivnější metoda.

Agregace:

Jednoduché agregace můžeme vyjádřit velice jednoduše:

Ale ty složitejší jsou na tom hůř:

Agregace v SLICKu jsou mnohem méně přehledné než jejich SQL protějšky. Je to daň za naprostou pravidelnost SLICK dotazů a poměrně omezené možnosti for comprehension. Ale tady můžeme využít komponovatelnosti SLICK dotazů, kdy si nějaké často používané fragmenty dotazů vyčleníme do pomocných generických funkcí a budeme tyto velké ošklivé dotazy sestavovat z menších.

Query Templates:

Další zajímavý koncept jsou takzvané Query Templates, které slouží k vytvoření dotazů bind parametry.

uptoBind se chová jako obyčejná funkce a můžu ji zavolat například takto: uptoBind(4.99).

Vykonání dotazu

Metody jako map, flatMap, filter, sortBy nebo groupBy jenom postupně konstruují objekt reprezentující dotaz, ale nevykonávají ho, to dělají až metody uvedené v následujícím seznamu (všechny mají implicitní parametr typu Session):

Metody foreach, foldLeft a elements jsou líné. Nevytvoří v paměti celou kolekci, ale procházejí data tak jak jsou programem vyžadována.

User-Defined Functions and Types:

Dokonce i nestandardní databázové funkce nebo vlastní uložené funkce a procedury, můžeme ve SLICKu používat. Musíme se přenést přes nějaký ten boilerplate, ale jako bonus dostaneme typovou bezpečnost.

Insert

Dotazování by bylo k ničemu, kdybychom do tabulky nemohli nic vložit nebo upravit.

Inserty jsou zcela triviální:

Ale práce s autoincrement sloupci vyžaduje trochu pozornosti:

Definujeme si syntetickou projekci, do které když insertujeme data, dostaneme nové id, které hned použijeme, abychom zkonstruovali objekt Picture.

Update:

Způsob updatovaní se na první pohled může zdát poněkud neobvyklý, ale po bližším zkoumání zjistíme, že odpovídá tomu, jak se update provádí v SQL.

Nejprve je potřeba vytvořit dotaz, který by vrátil záznamy, které chceme změnit a pak na něm zavoláme metodu update.

Delete:

Mazání funguje stejně jako update a stejně jako delete v SQL.

Direct embedding

Direct embedding představuje další možnost jak se pokládat databázové dotazy. Tentokrát není založeno na typech Rep[T], ale na systému maker, které byly do Scaly přidány ve verzi 2.10. Makra přeloží kód dotazů na syntaktický strom a ten je pak transformován na AST SLICKu a dále pak na SQL string.

V praxi to znamená, že píšeme kód, který se tváří jako kdyby byl vykonáván přímo na kolekcích v paměti a nezabalených typech (můžeme tedy použít == místo ===), ale je přeložen a vykonán na straně databáze.

Direct embedding zatím podporuje pouze metody: map, flatMap, filter, length a následující typy a jejich operace: Any (==), Int, Double (+ < >), String (+), Boolean (|| &&).

Schéma není definováno objekty typu Table, ale anotacemi na příslušných case class.

Dotazy jsou sestavovány stejně jako u Lifted embedding, ale provádějí se jinak:

Zatím jde jenom o velice omezený prototyp, který ale má velký potenciál (mapování nejenom na case class, ale na libovolnou třídu, type providers a generování entit přímo z databáze).

Plain SQL Queries

Posledním způsobem interakce s databází, kterou SLICK nabízí jsou Plain SQL queries – neboli prosté SQL dotazy. Jde o nejjednodušší ze tří variant. Nemusíme definovat tabulky a jejich mapování předem, ale píšeme dotazy přímo jako SQL řetězce.

Dotazy můžeme tvořit přes helper StaticQuery nebo pomocí string interpolation.

Dotaz položíme stejnými metodami jako u Lifted embedding ( foreach, foldLeft, elements, map, list, to, first, firstOption).

String Interpolation

Hlavní síla Plain SQL queries se ale projeví až v kombinaci se string interpolation. Stačí přidat jeden import a pak můžeme psát dotazy, které vypadají přesně jako SQL, ale jsou naprosto odolné proti SQL injection.

Identifikátor sql před řetězcem označuje, že se jedná o SQL dotaz. Interpolované proměnné (v řetězci jsou uvozeny dolarem, popřípadě ještě ve složených závorkách) jsou automaticky vloženy jako bind parametry.

Update, insert delete dotazy můžeme psát stejně, ale musí být uvozeny interpolátorem sqlu.

Článek obsahuje 0 komentářů