Hlavní navigace

Programování v JavaFX: zobrazení a formátování dat z databázové tabulky

12. 11. 2015
Doba čtení: 11 minut

Sdílet

Minulý článek byl zaměřen na vytvoření ukázkové úlohy, která patří do balíku uživatelských změn záznamů v databázi – přidávání nových, mazání stávajících a změny aktuálních záznamů a hodnot. V dnešním dílu se budeme věnovat dalším operacím ze systému CRUD, které nám ještě zbývají, konkrétně mazání záznamů.

Pro implementaci dalších dvou operací CRUD musíme nejprve upravit a doplnit FXML soubor. V JFXSB si tedy otevřeme soubor samExam4.fxml a uděláme několik změn:

  • v hierarchii widgetů si najdeme TabPane a přidáme jeho název – tabPane
  • na druhou záložku přidáme GridPane, upravíme ho na 2 × 6 řádků a velikost nastavíme na 600× 400 bodů
  • šířku prvního sloupce GridPane upravíme na 125, druhého na 450 bodů
  • přiřadíme widgetu název grid1
  • do levého sloupce přidáme widget Label. Přidání přímo do zobrazení widgetu není možné, takže to provedeme v hierarchii widgetů
  • provedeme duplikaci widgetu Label tak, aby jich bylo celkem 6
  • všechny widgety se bohužel uloží do levé horní buňky (souřadnice 0, 0). Proto musíme u 5 widgetů tyto souřadnice ručně změnit (položka RowIndex na záložce Layout) a umístit je do příslušných buněk GridPane
  • u jednotlivých widgetů Label změníme zobrazovaný text tak, aby odpovídal názvům sloupců tabulky v první záložce formuláře
  • můžeme si všechny tyto widgety označit a hromadně nastavit vlastnosti fontu. My to ale zatím neuděláme a ukážeme si později, jak to vyřešit centrálně pomocí CSS souboru
  • do GridPane opět v hierarchii přidáme widget TextField a provedeme podobné operace, jako u Labelu: duplikace na počet 6 a umístění do příslušných buněk ve druhém sloupci
  • všem widgetům TextField přiřadíme názvy, které se budou skládat z předpony u_ a názvu daného sloupce v tabulce (nebo jinak dle libosti)
  • napravo od názvů a editačních polí umístíme 3 widgety Button
  • všechna tlačítka vnoříme do VBox a nastavíme hodnotu Spacing na 45
  • do jednotlivých tlačítek postupně vložíme popisný text – Mazat záznam, Uložit změny, Zrušit změny
  • pro jednotlivá tlačítka vložíme jejich názvy pro kostru kontroléru
  • všechny změny uložíme

Základní kostru GUI pro operace mazání a úpravy záznamů již máme. Než se ale pustíme do jejího přenosu do JavaFX aplikace, vrátíme se k výše zmíněnému CSS. Jeho propojení s FXML souborem nám totiž umožní nastavení vlastností jednotlivých typů widgetů centrálně a hlavně jednotně. Abychom mohli srovnávat, tak si na prvním obrázku první galerie ukážeme, jak vypadá výsledek naší dosavadní práce. Otevřeme si soubor main.css z adresáře GUI-Files a přidáme do něj jeden řádek:

.label {-fx-font-size: 12pt; -fx-font-weight: bold; }

Tento příkaz nám zajistí nastavení velikosti fontu (zadána přímo velikost v bodech a tučné provedení) u všech widgetů typu Label, které se v GUI vyskytnou. Pokud by to bylo nutné nebo vhodné, můžeme toto nastavení „přebít“ individuálním nastavením přímo při tvorbě FXML souboru nebo při jeho volání v aplikaci programově. Po uložení změn v CSS souboru jsou okamžitě viditelné změny, které nám ukazuje druhý obrázek v první galerii. V dalším kroku si nastavíme vlastnosti widgetů TextField. Abychom měli nějaké srovnání, tak si do editačního pole Cele vložíme jednoduchý text, jak to ukazuje třetí obrázek první galerie. Pak přidáme do CSS souboru další řádek:

.text-field {-fx-font-size: 14pt; -fx-font-weight: bold; -fx-background-color: white; }

Pomocí tohoto příkazu jsme nastavili velikost fontu i jeho tučnost a také barvu pozadí editačních polí. Výsledek je pak po uložení změn v CSS vidět na čtvrtém obrázku v první galerii. Nastavení fontu je asi pochopitelné, ale proč se zatěžovat ještě pozadím pole? Zde vycházíme z následující úvahy:

  • v konkrétní operaci se chystáme provádět změny záznamů v tabulce
  • k provedení změny vždy vybereme jeden řádek z tabulky
  • při jeho mazání nás pozadí polí nijak moc nezajímá
  • při provádění aktualizace by ale mohlo být zajímavé a hlavně přínosné si nastavit, která editační pole uživateli zpřístupníme a umožníme mu jejich změnu
  • bylo by asi vhodné tento typ polí nějak viditelně označit, aby bylo na první pohled zřejmé a odlišné od těch ostatních

V našem konkrétním příkladě bude takovým polem ID, které je primárním klíčem tabulky a nebylo by moudré umožnit uživateli jeho změnu. Pro nastavení této vlastnosti je třeba udělat dva kroky:

  1. do CSS souboru přidáme další řádek
    .text-field:readonly { -fx-background-color: yellow; }
    Ani uložení změn se na vzhledu formuláře nijak neprojeví. Proto pokračujeme dále
  2. vybereme si příslušné editační pole (souřadnice 1,0) a v záložce Properties odškrtneme položku Editable. Po uložení změn se vzhled okamžitě mění, jak to ukazuje pátý obrázek první galerie.

Tímto opatření jsme nejen dali uživateli jasně najevo, že pole ID je nějak odlišné, ale také jsme mu zamezili jakýkoliv přístup do něj z aplikace (názvosloví je zde trochu matoucí, ale vlastně jsme nastavili editačnímu poli status ReadOnly – pouze pro čtení). Už nám zbývá nastavit font u popisu tlačítek, což provedeme asi očekávaným řádkem do CSS souboru:

.button {-fx-font-size: 20pt; -fx-font-weight: bold;}

Výsledek je patrný ze šestého obrázku v první galerii. Tímto máme GUI pro další operaci připravené a posledním krokem by měla být kopie kostry kontroléru do příslušné třídy v aplikaci. Bylo by ale zbytečné kopírovat kostru celou. Jednoduše řečeno zkopírujeme pouze ty řádky, které ve třídě zatím nejsou (jsou to všechny widgety, kterým jsme přiřadili nějaký název). Jedná se o TabPane, GridPane, tlačítka a editační pole, celkem 11 položek. Pro některé nové widgety je nutné importovat příslušnou třídu, ale v tom je nám nápovědný systém IJI vždy k ruce. Změny ve třídě uložíme, aplikaci přeložíme a spustíme. Výsledný vzhled záložky je vidět na posledním obrázku první galerie.

Vše je tedy připravené k tomu, abychom si mohli vybrat záznam, který budeme chtít smazat nebo upravit. V minulém dílu jsme si připravili handler, které reaguje na levý dvojklik myši nad vybraným záznamem. Také máme jednoduchou proceduru, která na dvojklik reaguje zápisem hodnoty pole ID v příslušném řádku do konzole. My tuto proceduru využijeme a vložíme do ní (na začátek, původní příkaz necháme jako poslední zatím beze změn) dva nové příkazy:

u_ID.setText(String.valueOf(table1.getSelectionModel().getSelectedItems().get(0).get(0)));
tabPane.getSelectionModel().select(1);
  1. řádek – došlo vlastně k nahrazení zápisu do konzole přiřazením obsahu editačního pole (u_ID) do příslušné vlastnosti (setText). Zbytek příkazu je stejný jako u původního, a tak můžeme klidně použít jeho upravenou kopii
  2. řádek – aby nebylo nutné to dělat ručně, po uložení hodnoty do příslušného editačního pole aplikace rovnou přechází do druhé záložky (jejich číslování začíná nulou). To byl důvod, proč jsme na začátku přiřazovali název widgetu TabPane

To by prozatím mohlo stačit a my si můžeme uložit změny, přeložit a spustit aplikaci. Výsledek je viditelný na prvním obrázku druhé galerie (použili jsme opět náš oblíbený řádek č. 22…). Následně do procedury pomocí kopie prvního řádku přidáme dalších 5 a upravíme je velmi jednoduše:

  • změníme postupně názvy editačních polí, kde nám IJI velmi dobře napovídá.
  • změníme čísla „sloupců“ v posledním příkazu řádku get(X), kde X je postupně 0 ..5. Musíme samozřejmě dbát na to, aby se zachovalo pořadí a příslušný sloupec tabulky byl přenesen do odpovídajícího editačního pole.

Výsledek stejného výběru v nové verzi nám ukazuje druhý obrázek ve druhé galerii. Z obrázku je patrné, že došlo k přenesení všech polí z příslušného řádku včetně prázdného pole Male des.. Na poli ID je možné si vyzkoušet, že je skutečně ReadOnly a k ničemu nás nepustí. Naopak v poli Cele je možné původní obsah měnit, mazat a doplňovat asi tak, jak bychom od editačního pole očekávali. Tímto úkonem máme tedy připravený vybraný řádek z tabulky k tomu, abychom s ním mohli něco provést. Jak už asi napoví názvy tlačítek, bude se jednat o jeho smazání či aktualizaci. Začneme první operací, která je technicky jednodušší – smazáním záznamu. Pro tento účel bychom mohli samozřejmě použít jednoduchý SQL dotaz

DELETE FROM udaje WHERE id=xy;

Tento příkaz bychom mohli „procedit“ přes nějakou obecnou proceduru k provádění SQL dotazů a bylo by vystaráno. Bylo by samozřejmě nutné doplnit programově název tabulky, sloupce i jeho hodnoty. To by se ale mohlo vlastně zdát zbytečné, protože z minulých dílů máme připravenou PG funkci, která tuto operaci provádí. Přesto tuto variantu uvedeme, protože nám může sloužit i jindy a jinde jako obecná možnost provádění SQL příkazů, které nevracejí žádné hodnoty. Jako první si tedy vytvoříme výkonnou proceduru, která bude jako parametr přebírat SQL příkaz a následně ho provede.

private void sqlQuery(final String query){              //1
    CONN = connDB("fxguide", "fxguide");                //2
    PreparedStatement pStat = null;                 //3
        try { CONN.setAutoCommit(false);                //4
            pStat = CONN.prepareStatement(query);       //5
            pStat.executeUpdate();              //6
            CONN.commit();                  //7
        } catch (SQLException e) { e.getMessage();
            try { CONN.rollback(); }                    //8
            catch (SQLException e1) { e1.getMessage(); }
        } finally { if (pStat != null) {
            try { pStat.close(); }
            catch (SQLException e) { e.getMessage(); } }
            try { CONN.setAutoCommit(true); }               //9
            catch (SQLException e) { e.getMessage(); }
            try { CONN.close(); }
            catch (SQLException e) { e.getMessage(); }} }

Uvedená procedura se trochu liší od těch, které jsme dosud použili, a tak si k ní řekneme pár podrobností:

  1. řádek – klasické volání procedury, kde parametrem je SQL dotaz
  2. řádek – přihlášení k databázi již vícenásobně použité
  3. řádek – definice lokální proměnné také již použitého typu
  4. řádek – začátek samotného příkazu včetně zachycení výjimek. První příkaz vyřazuje automatické provedení dotazů
  5. řádek – příprava dotazu
  6. řádek – provedení příkazu
  7. řádek – potvrzení a provedení dotazu
  8. řádek – v případě chyby či výjimky se vrátí stav databáze zpět před provedení dotazu
  9. řádek – automatické provedení dotazů se opětovně nastaví na původní hodnotu

K výkonné proceduře si přidáme ještě prováděcí proceduru, kterou navážeme na příslušné tlačítko (kvůli tomu se musíme vrátit do JFXSB a přidat ke všem tlačítkům akce na jejich stisk a vzniklý kód překopírovat do procedury v aplikaci.):

private void onSQL() {
    String sql = "DELETE FROM udaje WHERE id=1;";
    sqlQuery(sql);
    showFmt();
    tabPane.getSelectionModel().select(0); }

První dva řádky v této proceduře jsou asi jasné: definujeme SQL příkaz pro vymazání prvního záznamu v dané tabulce (udaje) tím, že nastavíme hodnotu příslušného pole (id) na 1. Třetí řádek nám znovu volá proceduru pro zobrazení údajů z tabulky, aby došlo k aktualizaci našeho datového zobrazení. Poslední řádek pak automaticky přejde na první záložku widgetu TabPane, aby byl výsledek okamžitě viditelný. A že je, nám ukazuje třetí obrázek druhé galerie. Jak je asi každému jasné, pořád nám funguje přenos vybraného řádku do editačních polí na druhé záložce. My jsme tuto možnost zatím ale nevyužili a nastavili přímo v SQL příkazu řádek, který chceme vymazat. Provedeme tedy náš příkaz s pomocí této funkce. K tomu nám stačí udělat dva kroky:

  1. vybrat řádek pro smazání. My vybereme hned ten první, který má hodnotu ID = 2
  2. upravit definici SQl příkazu následovně:
String sql = "DELETE FROM udaje WHERE id="u_ID.getText()+";";

Místo přímého zadání hodnoty byla přidána hodnota, které se přenesením údajů z tabulky nachází v příslušném editačním poli. Pro vyjádření této hodnoty byla využita odpovídající funkce widgetu TextField, která vrací text ve widgetu „uložený“. Na čtvrtém obrázku ve druhé galerii pak vidíme, že se dílo podařilo a první dva záznamy jsou z tabulky nenávratně pryč. Vzhledem k té „nenávratnosti“ by asi bylo vhodné příkazy nějak upravit, aby uživatel neměl mazání záznamů tak jednoduché a byl na něj upozorněn. K tomu nám velmi dobře poslouží již dříve probíraný dialog s dotazem. Necháme ho tedy zobrazit s tím, že uživatel buď mazání potvrdí (a to se provede) nebo odmítne (a nestane se vůbec nic). Definici SQL příkazu ponecháme beze změny a zbytek prováděcí procedury změníme následovně:

Integer quest = questMessage("Je vybrána položka ID =" +u_ID.getText()+" , opravdu chcete vybranou položku smazat?", "Mazání záznamu");
        if (quest==0) {
           sqlQuery(sql);
           showFmt();
           tabPane.getSelectionModel().select(0); } }

Úprava je velmi jednoduchá a spočívá ve volání příslušného dialogu. V něm jsou statické texty s varováním a dotazem na potvrzení akce. Pokud je vybráno potvrzení, tak se provedou tři již známé příkazy. Pokud opět vybereme první aktuální záznam (ID = 3), tak na posledním obrázku druhé galerie vidíme „kladný“ výsledek. Jako poslední věc v tomto dílu si ukážeme možnosti využití PG funkce, kterou jsme v minulých dílech vytvořili pro účely mazání záznamů z tabulek. Ta je vytvořena natolik obecně, že stačí zadat pouze tři uvedené parametry a je vše hotové. Za daným účelem vytvoříme novou proceduru:

private void deleteRec(final String tablename, final String colname, final Integer item){   //1
        CONN = connDB("fxguide", "fxguide");                            //2
        try { calStat = CONN.prepareCall("{call delrow(?,?,?)}");               //3
            calStat.setString(1,tablename);                         //4
            calStat.setString(2,colname);                           //5
            calStat.setInt(3,item);                             //6
            calStat.execute();                                  //7
            calStat.close();                                    //8
        } catch (SQLException e) { e.getMessage();
        } finally {
            if (calStat != null) {
                try { calStat.close(); }
                catch (SQLException e) { e.getMessage(); }
            try { CONN.close(); }
            catch (SQLException e) { e.getMessage(); } } } }

Pro komentář je zajímavých pouze prvních 8 řádků, zbytek je pak pouze tradiční zpracování možných chyb a výjimek.

  1. řádek – funkce se volá se třemi parametry, které již byly uvedeny – název tabulky, ze které se má záznam vymazat, název sloupce, podle kterého se bude záznam v tabulce vybírat a celočíselná hodnota, která se má ve daném sloupci hledat. Obecnost příkazu i PG funkce není samozřejmě úplná, ale hledání celočíselných hodnot, které se často používají jako primární klíče v tabulkách, je docela logické. Bylo by samozřejmě možné napsat PG funkci i proceduru ještě obecněji, ale taková komplikace pro nás není zajímavá.
  2. řádek – navazuje se spojení do databáze pomocí jména a hesla
  3. řádek – připravuje se příkaz pro volání PG funkce. Je třeba si všimnout jiného způsobu jejího volání vzhledem k tomu, že má nějaké vstupní parametry. Ty se zadávají pomocí zástupných znaků otazníku jako formálních parametrů
  4. řádek – přiřazuje se první reálný parametr pomocí jeho typu (String), pořadí v seznamu formálních parametrů a hodnoty (v tomto případě proměnná, které je formálním parametrem celé funkce)
  5. řádek – podobně se přiřazuje druhý reálný parametr – název sloupce
  6. řádek – třetí parametr má jiný typ – Integer
  7. řádek – příkaz se provede
  8. řádek – vzhledem k tomu, že PG funkce nemá žádný výstupní parametr, může se příkaz okamžitě po provedení ukončit

Při práci na nové funkci si můžeme všimnout, že nám IJI hlásí chybu. Ta je způsobena tím, že voláme proměnnou calStat, která ale není nikde deklarovaná. Proto musíme na začátek procedury přidat řádek s její deklarací a pak bude vše v pořádku:

CS24_early

CallableStatement calStat = null;

Výkonnou proceduru máme tedy připravenou a zkusíme ji tedy zavolat. To si ale necháme až na příště.

V dnešním dílu jsme se zaměřili na mazání tabulkových záznamů pomocí klasických SQL příkazů. Do příštího dílu jsme si připravili výkonnou funkci, která zajistí mazání záznamů pomocí deklarované PG funkce. Také se v příštím dílu začneme věnovat aktualizaci a změnám v tabulkových záznamech.

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