Hlavní navigace

Programování v JavaFX: JOOQ, zobrazení dat ve widgetu, mazání záznamů

7. 1. 2016
Doba čtení: 13 minut

Sdílet

Minulý článek byl zaměřen na úvod do problematiky ORM a základní popis projektu JOOQ. Také jsme si ukázali jednoduchý příklad pro výpis obsahu tabulky do konzole. Dnes se pustíme do jednoduššího způsobu uložení výsledků dotazů. Tuto variantu zobecníme tak, abychom si mohli zobrazit výsledky dotazu ve widgetu tabulky.

V minulém dílu jsme si ukázali, jak alespoň v konzole zobrazit výsledek dotazu do PG databáze pomocí JOOQ. Udělali jsme to obdobně jako již dříve v rámci SQL dotazů – jednoduchým výpisem jednotlivých položek ve vybrané tabulce. Pro výpis do konzole je to samozřejmě v pořádku, ale my víme, že naším cílem je zobrazit tabulku či výsledek dotazu v příslušném widgetu. Tento widget (TableView) má ale poměrně striktní požadavky na formát dat, které po něm chceme zobrazit. Proto by bylo vhodné najít nějakou jinou variantu, jak výsledek dotazu nejen získat, ale i uložit. V tomto snažení je nám JOOQ velmi nápomocné a umožní nám vše vyřešit pomocí pouhých dvou příkazů. My už budeme myslet na budoucí kroky a za daným účelem vytvoříme novou proceduru:

private dataView() {
        DSLContext create = DSL.using(connDB("fxguide", "fxguide"), SQLDialect.POSTGRES);
        ObservableList<Record> data = javafx.collections.FXCollections.observableArrayList(create
                .select()
                .from(UDAJE)
                .where(UDAJE.ID.lt((short) 10))
                .orderBy(UDAJE.ID)
                .fetch());
        System.out.println(data.toString()); }

První příkaz je samozřejmě úplně stejný, jako byl v minulém dílu a proceduře jooq_Query. Také samozřejmě není absolutně žádný důvod, aby tomu bylo jinak! Druhý příkaz již obsahuje značné odchylky a snaží se o to, co jsme naznačili na začátku: vytvořit výsledek dotazu v takovém tvaru a formátu, aby ho bez problémů akceptoval widget TableView. Poslední příkaz pak jednoduše zobrazí v konzole deklarovanou proměnnou, kterou pouze převede na řetězec. Výsledek výše uvedené procedury ukazuje první obrázek v galerii. Jak je z něj zřejmé, tento jednoduchý příklad zobrazí výsledky dotazu velmi přehledně i v konzole. To ale není naším cílem, a proto se velmi rychle pustíme do další práce. Naším cílem je zobrazit výsledky dotazu v příslušné tabulce a také provést základní formátování (pro začátek zarovnání jednotlivých sloupců tabulky). Jak je možné si ověřit v dřívějších dílech, budeme k tomu potřebovat tři kroky – získání výsledku dotazu nad tabulkou/tabulkami, inicializaci jednotlivých zobrazovaných sloupců a nastavení jejich zarovnání.

První krok vyřešíme velmi jednoduše – použijeme již vytvořenou proceduru výše a změníme ji na funkci s odpovídajícím typem:

private ObservableList<Record> dataView() {
        DSLContext create = DSL.using(connDB("fxguide", "fxguide"), SQLDialect.POSTGRES);
        ObservableList<Record> data = javafx.collections.FXCollections.observableArrayList(create
                .select()
                .from(UDAJE)
                .where(UDAJE.ID.lt((short) 10))
                .orderBy(UDAJE.ID)
                .fetch());
        return data; }

Rozdíly jsou velmi jednoduché – definujeme typ funkce a musíme přidat návratovou hodnotu funkce. Výpis do konzole můžeme samozřejmě vynechat. Při zadání typu funkce s výhodou využijeme typ Record, který nám nabízí za tímto účelem projekt JOOQ (pro srovnání jenom připomeneme, že pro SQL příkazy měla stejná funkce typ ObservableList<ObservableList>. V rámci funkce pak probíhalo naplnění jednotlivých řádků/záznamů do typu ObservableList<Object> a jejich následné ukládání do návratové proměnné funkce.). V konkrétním příkladě JOOQ už vlastně máme „vnitřní“ proměnnou k dispozici a můžeme si tedy ostatní kroky ušetřit. Rozdíl je samozřejmě také v tom, že aktuální funkce nemá žádné vstupní parametry (ta minulá měla dva – SQL příkaz a pole s požadovanými typy jednotlivých sloupců). Funkce pro získání a uložení dat z tabulky/dotazu tedy máme a můžeme se pustit do dalšího kroku – inicializaci sloupců tabulky.

Jak je možné ověřit v předchozích díle, jsou pro inicializaci tabulkových sloupců (což vlastně zajistí zobrazení v nich uložených hodnot) potřebné dvě věci – nastavení formátu či typu příslušných widgetů a příslušná procedura. Pro porovnání uvedeme opět předchozí formát widgetů:

@FXML private TableView<ObservableList<ObservableList>> table1;
@FXML private TableColumn<ObservableList<String>, String> col1;

Vybrali jsme pouze dva, kde je vlastní tabulka a první ze sloupců. Ostatní sloupce jsou pak definovány stejně. V aktuální úloze opěr využijeme možnosti projektu JOOQ a nastavíme widgety následovně:

@FXML private TableView<Record> table1;
@FXML private TableColumn<Record, String> col1;

Je vidět, že jsme i zde použili typ, který nám JOOQ dává k dispozici. Je také samozřejmě možné (a představili jsme si to již v původní zkušební úloze) zadat typy jednotlivých sloupců tak, jak jsou definovány v generovaných třídách. Pak bychom ale také museli použít pro inicializaci jednotlivých sloupců zvláštní procedury. My si ale ukážeme jedinou proceduru, která to zvládne najednou. Její odlišnost od té původní spočívá ve zjednodušení na základě použití JOOQ typu Record. Výsledek bude přece jenom přehlednější, než byla minulá verze dané procedury:

private void initTableColumn(final TableColumn[] column) {
        for (int i=0;i<column.length;i++) {
            final int finalI = i;
            column[i].setCellValueFactory(new Callback<TableColumn.CellDataFeatures<Record, String>, ObservableValue<String>>() {
                @Override
                public ObservableValue<String> call(TableColumn.CellDataFeatures<Record, String> param) {
                    return new SimpleObjectProperty(param.getValue().getValue(finalI));
                } }); } }

Je dobré si povšimnout i změny, která je v řádku s definicí návratové hodnoty. Nejjednodušší bude uvést její předchozí tvar:

return new SimpleStringProperty(param.getValue().get(finalI));

Posledním krokem je nastavení zarovnání jednotlivých sloupců. To jsme v minulosti prováděli pomocí ještě složitější funkce, než je ta pro inicializaci sloupců. Nyní si ukážeme ještě jinou možnost, která se nám nabízí. Konkrétně se bude jednat o kombinaci kódu v proceduře a CSS souboru. Otevřeme si tedy soubor main.css v adresáři GUI-Files a přidáme do něj tři nové řádky:

.align-left { -fx-alignment: center-left;}
.align-center { -fx-alignment: center;}
.align-right { -fx-alignment: center-right;}

Tyto příkazy nám nastaví všechny potřebné varianty zarovnání, na které se můžeme odkázat z vlastního kódu aplikace. Pro jistotu dáváme do přílohy aktuální verzi CSS souboru main.css. Obecně se dá říct, že nám k tomu poslouží poměrně jednoduchý příkaz:

columnName.getStyleClass().add("název položky v CSS souboru");

Pro první sloupec tabulky, který chceme zarovnat na střed, by pak konkrétní příkaz vypadal takto:

col1.getStyleClass().add("align-center");

Bylo by samozřejmě možné udělat zarovnání odděleně pro všechny sloupce, ale my si to opět zobecníme a vytvoříme proceduru, která vše zvládne při jednom volání:

private void alignTableColumn(final TableColumn[] column, final String[] align) {
        String pos = null;
        for (int i=0;i<column.length;i++) {
            switch (align[i]) {
                case "LA": pos = "align-left"; break;
                case "CA": pos = "align-center"; break;
                case "RA": pos = "align-right"; break; }
            column[i].getStyleClass().add(pos); } }

Procedura je poměrně jednoduchá – má dva parametry (pole s názvy sloupců/widgetů a řetězcové pole s názvy/typy zarovnání), deklaraci proměnné s typem zarovnání, přepínač, který na základě vstupního parametru s typem zarovnání nastavuje příslušnou proměnnou a výkonný příkaz pro jednotlivé sloupce. Od definice přepínače vše probíhá ve smyčce pro všechny sloupce, které jsou zadané v parametru procedury. Je samozřejmé, že je třeba definovat všechny sloupce, které chceme zarovnávat a k tomu dodat seznam typů zarovnání se shodným počtem prvků! Pokud bychom aktuální proceduru pro zarovnání sloupců porovnali s minulou verzí, rozdíl by byl docela zásadní ve prospěch verze aktuální. Už tedy máme vše připravené a můžeme vytvořit konečnou výkonnou proceduru, která nám zobrazí data z tabulky v příslušném widgetu:

private void viewTable() {
        ObservableList<Record> data = dataView();                 //1
        final TableColumn[] tc = new TableColumn[]{col1, col2, col3, col4, col5, col6}; //2
        initTableColumn(tc);                                //3
        alignTableColumn(tc, new String[]{"CA", "RA", "RA", "RA", "LA", "CA"});     //4
        table1.setItems(data);  }                           //5

Výkonná procedura se moc neliší od té minulé, přesto si jí stručně představíme:

  1. řádek – deklaruje se proměnná příslušného typu a ukládá se do ní výsledek funkce pro získání dat z tabulky
  2. řádek – definuje se pole sloupců tabulky, které mají být následně inicializovány a zobrazeny
  3. řádek – volá se procedura pro inicializaci sloupců s parametrem výše definované proměnné se seznamem sloupců
  4. řádek – volá se procedura pro zarovnání sloupců, kde se opět jako parametr použije seznam sloupců a také pole s požadovaným způsobem zarovnání pro každý sloupce
  5. řádek – do tabulky se vloží získaná data z příslušné funkce k zobrazení

Ještě pro porovnání s minulým způsobem upozorníme na to, že zde samozřejmě chybí definice SQL příkazu. Ten je v aktuálním případě vlastně uložen ve funkci, která získává data z tabulky či dotazu. Abychom mohli zobrazení vyzkoušet, vložíme volání výkonné procedury pro zobrazení dat do procedury initialize a spustíme aplikaci. Druhý obrázek galerie nám ukazuje, že při otevření příslušné zkušební úlohy (č. 5) skutečně dojde k zobrazení všech sloupců tabulky včetně jejich nastaveného zarovnání. Pokud bychom chtěli zobrazit všechny záznamy v tabulce, velmi jednoduše upravíme funkci dataView a zakomentujeme řádek, který v dotazu omezuje počet zobrazených záznamů:

//.where(UDAJE.ID.lt((short) 10))

Víc už není třeba a třetí obrázek v galerii nám ukazuje, že jsou opravdu zobrazeny všechny záznamy z tabulky. Pokud se na obrázek zaměříme podrobněji, tak uvidíme jednu odlišnost v obou sloupcích, které obsahují desetinná čísla. V prvním z nich jsou obsažena „velká“ čísla ve formátu se 4 desetinnými místy. Další položka (malá desetinná čísla ve formátu 8,6) je zobrazena správně. Zde je hlavní rozdíl v tom, že při řešení v minulých dílech jsme pomocí výkonné funkce malá čísla odfiltrovali a ta nebyla vůbec zobrazena. Tyto záležitosti ale nebudeme v rámci naší série dále rozebírat.

Trochu se budeme věnovat rozdílům mezi oběma metodami. Pro SQL příkazy se nám podařilo vytvořit poměrně obecný způsob, jak tabulková data získávat, formátovat a zobrazovat. JOOQ nám sice velmi zjednodušil práci (včetně výborné kontextové nápovědy), ale za cenu větší konkrétnosti a svázanosti s konkrétní tabulkou nebo operací nad ní. Nikde samozřejmě není řečeno, že by nebylo možné použitý dotaz zobecnit a pomocí parametrů ho nastavit programově dle potřeby. V naší sérii se tomu ale nebudeme nijak věnovat ani naznačovat možnosti v tomto směru.

Další viditelnou změnou je to, že v aktuální úloze prakticky není nutné využívat příkazy pro zachytávání výjimek. Je to samozřejmě dáno tím, že máme vytvořené aplikační procedury a v nich jsou definované všechny proměnné a jejich typy. Můžeme si to vyzkoušet na jednoduchém příkladu, kde se pokusíme zadat špatný název položky v dotazu:

.orderBy(UDAJE.IDd)

Okamžitě po zadání dalšího znaku IJI hlásí chybu a tak ani není možné jí vyvolat při běhu aplikace. To nám určitě zjednodušuje a usnadňuje práci. K chybám a výjimkám se vrátíme později, až to bude mít větší opodstatnění. Nyní se pustíme dále, abychom ukázali možnosti CRUD pomocí JOOQ. Dalším na řadě je tedy mazání tabulkových záznamů. My jsme v předchozím příkladu použili vytvořenou PG funkci. Tu bychom samozřejmě mohli použít i v aktuálním příkladu, protože JOOQ s PG funkcemi a procedurami nakládat umí (ještě se k tomu vrátíme později): JOOQ Stored Procedures

Druhou možností je přímé zadání SQL příkazu, který by vybraný záznam smazal. Vzhledem k obecnému zaměření JOOQ se pustíme touto cestou. V případě SQL je možné vybraný záznam smazat nějak takto:

DELETE FROM udaje WHERE id=22;

JOOQ mám dává k dispozic dva prakticky ekvivalentní příkazy, které provádějí úplně stejnou operaci:

create.deleteFrom(UDAJE).where(UDAJE.ID.equal((short) 22)).execute();
create.delete(UDAJE).where(UDAJE.ID.equal((short) 22)).execute();

S touto znalostí můžeme vytvořit výkonnou proceduru, která smaže vybraný záznam. Budeme opět postupovat v několika úrovních a teď si uvedeme první variantu procedury:

private void deleteRec() {
        DSLContext create = DSL.using(connDB("fxguide", "fxguide"), SQLDialect.POSTGRES);
            create
                .deleteFrom(UDAJE)
                .where(UDAJE.ID.equal((short) 22))
                .execute(); }

Je vidět, že jsme přidali pouze příkaz pro připojení k databázi a dotaz na smazání vybraného záznamu jinak formátovali. K výkonné proceduře si připravíme ještě další, odkud se bude ta výkonná volat:

private void onDelete() {
        deleteRec();
        viewTable();
        tabPane.getSelectionModel().select(0); }

Zde jsme vymazání záznamu doplnili novým načtením a zobrazením tabulkových dat a automatickým přechodem na první záložku formuláře, kde je samotná tabulka. Tuto proceduru přiřadíme k akci příslušného tlačítka a můžeme vyzkoušet její funkci. Výsledek je pak viditelný na čtvrtém obrázku galerie. Než se pustíme do dalších kroků v rámci mazání záznamů, tak se krátce vrátíme k zachytávání chyb. Co by se např. stalo, kdybychom do výkonného příkazu zadali číslo záznamu, který v tabulce není a nikdy nebyl?

.where(UDAJE.ID.equal((short) 32))

Krátkou zkouškou zjistíme, že se nestalo vůbec nic! Neobjevila se žádná chybová hláška a záznamy v tabulce zůstaly beze změny. Pokud bychom chtěli podobnou chybu nějak řešit, museli bychom to udělat přímo v kódu aplikace. To ale dělat nebudeme a přejdeme k další úrovni mazání záznamů a přidáme si výběr záznamu z tabulky pomocí myši, jeho zobrazení v editačních polích a následné smazání. K tomu použijeme některé funkce a procedury, které jsme si deklarovali v minulé zkušební úloze:

  • zkopírujeme deklaraci proměnné pro uložení hodnoty klíčového pole při výběru záznamu z tabulky
    Integer pid = null;
  • do procedury initialize zkopírujeme handler s reakcí na výběr záznamu v tabulce
  • zkopírujeme procedury tf_RO a tf_RW pro nastavení editačních polí
  • zkopírujeme proceduru tableClick s výkonnou částí výběru a zobrazení záznamu

Při posledním kopírování si můžeme povšimnout, že nám IJI nečekaně hlásí chybu! A to tam, kde bychom to asi nečekali – při přenosu údajů z řádku tabulky do editačních polí. Je to o to větší záhada, že v minulé úloze bylo vše bez problémů! Nebudeme se složitě pátrat po příčině a zjednáme jednoduchou nápravu. Dále uvádíme starý a nový kód, který je již bez chyb:

pid = Integer.valueOf(String.valueOf(table1.getSelectionModel().getSelectedItems().get(0).get(0)));
        vs
pid = Integer.valueOf(String.valueOf(table1.getSelectionModel().getSelectedItems().get(0).getValue(0)));

Následně můžeme upravit původní výkonnou proceduru tak, že přidáme do jejího volání celočíselný parametr pro hodnotu klíčového pole vybraného záznamu a změníme převod tohoto celočíselného parametru na požadovaný typ Short. Uvedeme zde pouze změněné řádky:

private void deleteRec(final Integer rec) {
...
.where(UDAJE.ID.equal(rec.shortValue()))

Logicky musíme změnit i způsob volání výkonné procedury v proceduře onDelete:

deleteRec(pid);

Pak už můžeme spustit aplikaci, vybrat si jeden záznam (viz pátý obrázek v galerii) a zkusit ho vymazat. Jak ukazuje šestý obrázek galerie, tak se to opravdu povedlo a záznam č. 23 je pryč. My zkusíme jít ještě dál a výkonnou „mazací“ proceduru si ještě více zobecnit. Zobecnění provedeme tak, že zadáme nejen hodnotu klíčového pole pro mazání, ale také název tabulky a sloupce, ve kterém se má daná hodnoty hledat. Ostatně tak jsme to měli již v předchozím příkladu s použitím PG funkce. Vzhledem k mírným komplikacím raději uvedeme kompletní kód nové výkonné procedury jako celek:

private void deleteRec(final String tabname, final String colname, final Integer recval) {
        DSLContext create = DSL.using(connDB("fxguide", "fxguide"), SQLDialect.POSTGRES);
            create
                .deleteFrom(DSL.tableByName(tabname))
                .where(DSL.fieldByName(colname).equal(recval.shortValue()))
                .execute(); }

Vstupní parametry asi nijak nepřekvapí a jsou poměrně logické. Problém je ale v obou řádcích dotazů, kam se předávají vstupní parametry. Obě použité funkce DSL.tableByName a DSL.fieldByName jsou v současné verzi JOOQ označeny jako deprecated (neschválené, odmítnuté). Aktuálně jsou sice funkční, ale jak to bude do budoucna, je otázka. Vzhledem k této skutečnosti o nich není možné najít informace v manuálu a musí se hledat na webu. Jenom krátká poznámka: na výpisu kódu v IJI jsou takové funkce označeny přeškrtnutým typem písma. Také při překladu kódu s takovými funkce nás IJI upozorní, že jsou použity. Abychom prokázali funkčnost, musíme samozřejmě změnit i volání výkonné procedury. Mohlo by se to zdát triviální, ale není to úplně pravda:

deleteRec("udaje", "id", pid);

Vtip je totiž v tom, že zde musíme název tabulky a sloupce zadávat tak, jak jsou deklarované ve struktuře původní TABULKY údaje, nikoliv tak, jak jsou generovány v aplikačních procedurách!!! Když už máme vše připravené, spustíme aplikaci, vybereme další záznam a na posledním obrázku si můžeme ověřit, že i záznam č. 24 zmizel v nenávratnu. K mazání záznamů na závěr uvedeme pouze dvě stručné poznámky:

UX DAy - tip 2

  • bylo by samozřejmě možné podle vzoru z minulé úlohy velmi jednoduše do procedury onDelete přidat zobrazení uživatelského dialogu se souhlasem či nesouhlasem se smazáním vybraného záznamu
  • bylo by také možné ještě více zobecnit možnosti mazání záznamů tím, že by se v parametru zadávala hodnota v nějakém sloupci pomocí obecného typu, třeba String. K tomu by se přidal další parametr, který by definoval typ sloupce, ve kterém se hodnota hledá. V proceduře by se pak hodnota vstupního parametru převedla na zadaný typ a ten by se použil v příkazu pro vyhledání zadané hodnoty

Ani jednu z věcí nebudeme nyní realizovat a na závěr dnešního dílu dáme do přílohy příslušnou verzi třídy ukázkové úlohy: samexam5.java.

V dnešním dílu jsme si ukázali jednodušší způsob uložení výsledků dotazů. Tuto variantu jsme zobecnili tak, abychom si mohli zobrazit výsledky dotazu ve widgetu tabulky. Také si ukázali další součást CRUD – mazání záznamů několika způsoby. V příštím dílu ukončíme kapitolu o projektu JOOQ ukázkou aktualizace záznamů a vkládání nových záznamů do tabulky. Kromě toho si ukážeme ještě nějaké obecné možnosti projektu.

Byl pro vás článek přínosný?