JavaFX: H2 + JOOQ – připojení, konfigurace, zobrazení záznamů v tabulce

11. 2. 2016
Doba čtení: 11 minut

Sdílet

Minulý díl byl věnován ukázce vkládání záznamů pomocí Hibernate. Zahájili jsme další kapitolu našeho seriálu o H2 databázi. Dnes si ukážeme připojení H2 z aplikace JavaFX, konfiguraci JOOQ a zobrazení záznamů v tabulce.

minulého dílu máme vše kolem H2 Database připravené a můžeme se pustit do prvních pokusů jejího propojení s naší ukázkovou aplikací. Na začátek si samozřejmě musíme připravit další ukázkovou úlohu, a to bude představovat práci jak v JFXSB, tak v IJI. Otevřeme si JFXSB a poslední použitý FXML soubor ze zkušební úlohy č. 6. Soubor uložíme pod novým názvem samExam7.fxml. Pak už můžeme provést čtyři jednoduché změny a úpravy:

  • změnit název třídy kontroléru na jfxapp.samexam7
  • vymazat třetí záložku z widgetu TabPane
  • změnit název druhé (aktuálně poslední) záložky widgetu
  • nově nastavit názvy tlačítek

Vzhled nového formuláře v JFXSB je vidět na prvním obrázku v galerii. Po uložení změn si zobrazíme kostru kontroléru a uložíme ji do schránky ve verzi FULL. Pak už můžeme otevřít IJI a přidat novou zkušební úlohu známým způsobem: vytvořit novou třídu samexam7, zkopírovat do ní kostru kontroléru a upravit importy potřebných knihoven, do hlavní třídy přidat volání nové ukázkové úlohy včetně všech změn v nastavení a konečně vyzkoušet zobrazení nového formuláře. Výsledek je vidět na druhém obrázku galerie. Aplikace je tedy připravena a my můžeme pomalu zahájit plnění nové třídy kódem. Jak je asi patrné z názvů tlačítek, budeme v této zkušební úloze používat projekt JOOQ, K tomu se ale dostaneme trochu později, protože prvním krokem musí být samozřejmě připojení k nové databázi. K tomu využijeme jednak vzor z PDF dokumentace (strana 21) a také naše předchozí zkušenosti s připojením PG. Abychom si trochu ulehčili práci, přidáme si do globálních proměnných kromě již známé deklarace spojení také tři další textové proměnné (pro porovnání ponecháváme také proměnnou s napojením na PG):

private Connection CONN = null;
private String host = "jdbc:postgresql://127.0.0.1:5432/fxguidedb";

private final String h2file = "jdbc:h2:file:./Data/fxguidedb";
private final String jmenoh2 = "fxguide";
private final String hesloh2 = "fxguide";

V deklaraci proměnné h2file je třeba si více povšimnout odkazu na soubor s databází. Ten je umístěn v aplikačním adresáři Data a tedy třeba použít konstrukce odkazu ./Data/název_databázového_souboru. Funkci pro připojení databáze můžeme klidně zkopírovat z předchozích ukázkových úloh (jedná se o funkci connDB) a následně přejmenovat a upravit dle vzoru:

private Connection connH2 () {
        try { Class.forName("org.h2.Driver");
        } catch (ClassNotFoundException e) { e.printStackTrace(); }
        try { CONN = DriverManager.getConnection(h2file, jmenoh2, hesloh2);
            return CONN;
        } catch (SQLException e) { e.getMessage();
            return CONN; } }

Změn není tolik, takže si je jenom stručně popíšeme:

  • kromě názvu funkce zmizely i vstupní parametry
  • změnila se samozřejmě třída ovladače databáze
  • pro samotné připojení se využijí dříve deklarované globální textové proměnné

Vzhledem k tomu, že se chystáme použít JOOQ, nebudeme se v první fázi nijak moc snažit a úspěšné připojení k databázi vyzkoušíme jednoduše. V jednom z prvních dílů našeho seriálu jsme to již dělali, takže si odtud zkopírujeme příslušnou funkci:

private boolean logOK() {
        Connection c = connH2();
        if (c!=null) {
            try { c.createStatement().execute("SELECT 1"); return true;
            } catch (SQLException e) { e.getMessage(); return false; } }
        else return false; }

V původním kódu změníme pouze odkaz na nové připojení H2 a do procedury initialize přidáme kód pro vyvolání ověřovací funkce a výpis výsledků do konzole:

@FXML void initialize() {
        if(logOK())
            System.out.println("Spojení se podařilo");
        else
            System.out.println("Bohužel kužel..."); }

Celou konstrukci předešlých příkazů můžeme také udělat trochu jinak a ve druhé variantě využít toho, že H2 databáze podporuje a poskytuje implementaci třídy javax.sql.DataSource. V této variantě bude deklarace proměnných vypadat následovně:

private final String dataSourceUrl = "jdbc:h2:file:./Data/fxguidedb";
    private final String dataSourceUser = "fxguide";
    private final String dataSourcePassword = "fxguide";
    private DataSource dataSource;

Pro inicializaci příslušného objektu pak musíme přidat extra funkci:

private void initializeDataSource() {
        JdbcDataSource dataSource = new JdbcDataSource();
        dataSource.setURL(dataSourceUrl);
        dataSource.setUser(dataSourceUser);
        dataSource.setPassword(dataSourcePassword);

        this.dataSource = dataSource; }

Hlavní inicializační procedura pak může vypadat např. takto:

@FXML void initialize() {
        initializeDataSource();

        try(Connection connection = dataSource.getConnection()) {
        } catch (Exception e) {
            throw new IllegalStateException("Připojení k databázi selhalo: " + e.getMessage(), e); } }

Aplikaci můžeme spustit a otevřít aktuální zkušební úlohu. Třetí obrázek v galerii nám jasně ukazuje, že se připojení k databází zdařilo. Proto můžeme klidně přikročit k nastavení JOOQ. Jak si mnozí jistě vzpomenou z dřívějších dílů, prvním krokem je vždy vygenerování potřebných aplikačních tříd. K tomu opět s výhodou využijeme dříve vytvořenou třídu a pouze upravíme její formát:

private void code_Gener() {
        Configuration configuration = new Configuration()
                .withJdbc(new Jdbc()
                        .withDriver("org.h2.Driver")                //1
                        .withUrl(h2file)                    //2
                        .withUser(jmenoh2)                  //3
                        .withPassword(hesloh2))                 //4
                .withGenerator(new Generator()
                        .withDatabase(new Database()
                                .withName("org.jooq.util.h2.H2Database")    //5
                                .withIncludes(".*")             //6
                                .withExcludes("") )             //7
                        .withTarget(new Target()
                                .withPackageName("jfxapp")
                                .withDirectory("./src")));          //8
        try {
            GenerationTool.generate(configuration);
        } catch (Exception e) { e.printStackTrace();  } }

Změn sice není zdánlivě mnoho, ale jsou docela důležité, takže připojíme podrobnější komentář:

  1. řádek – zde je změna jasná a nutná, ovladač bude samozřejmě jiný
  2. řádek – deklaraci umístění databázového souboru přebíráme z globální proměnné
  3. řádek – přihlašovací jméno přebíráme z globální proměnné
  4. řádek – uživatelské heslo přebíráme z globální proměnné
  5. řádek – deklaraci typu databáze raději opět ověříme pomocí nápovědy IJI, abychom použiji správný formát
  6. řádek – zde necháme vygenerovat kompletní obsah databáze
  7. řádek – zde byl původně odkaz na databázové schéma, které ale v H2 nepoužíváme, takže se celý řádek vynechává
  8. řádek – zde nebylo původně nic (konkrétně prázdný řetězec). Z důvodů, které objasníme později, sem zadáváme cestu do zdrojového adresáře aplikace

Novou proceduru připojíme k akci příslušného tlačítka a následně spustíme. Pokud jsme někde neudělali nějakou zákeřnou chybu, tak by vše mělo proběhnout bez problémů a chybových hlášení. Výsledek generovacího procesu nám ukazuje čtvrtý obrázek galerie. Jak je z něj zřejmé, tak se v balíčku objevily dva nové adresáře (to je způsobeno nastavením na řádku č. 8 v proceduře): information_schema a public_. Nás bude zajímat hlavně ten druhý, a tak se na něj podíváme podrobněji:

  • tento adresář má velmi podobnou strukturu, jako měl ten z minulých dílů, kde jsme generovali třídy pouze pro jednu tabulku
  • přímo v adresáři jsou tři známé třídy – Keys, Public, Tables
  • nově je přítomna třída Sequences. To souvisí s tím, že v tabulce prihlaseni máme klíčovou položku definovanou pomoc typu IDENTITY
  • v podadresářích tables a records jsou pak vygenerované obě tabulky a jeden pohled, které jsme dříve vytvořili v H2 administrátoru

Zájemci se mohou blíže seznámit s prvně jmenovaným adresářem, kde je hlavně v podadresáři tables opravdu značné množství položek. Obecně se dá říct, že jsou celkem zajímavé poměrně velké rozdíly v počtu a struktuře generovaných aplikačních tříd pro obě použité databáze. Není to ale vlastně nijak kritické, protože pro uživatele ani jedna varianta nepředstavuje zásadní rozdíl v pracnosti dosažení cíle. Všechny potřebné aplikační třídy máme v tuto chvíli vygenerované a můžeme přistoupit ke zkušebnímu zobrazení údajů z tabulky. Než ale začneme upravovat již známou proceduru, musíme provést tři statické importy:

import static javafx.collections.FXCollections.observableArrayList;
import static jfxapp.Tables.UDAJE;
import static jfxapp.public_.Tables.H2POHLED;
import static jfxapp.public_.Tables.ZKUSEBNI;

První z nich nám poslouží ke zkrácení příkazů, které jsme dříve vkládali v kompletní formě. Import PG tabulky necháváme jako ukázku toho, že je možné importovat tabulky z více různých zdrojů/databází. Při importu z H2 databáze musíme zohlednit umístění tříd ve struktuře aplikace a importujeme si jak tabulku, tak vytvořený pohled. Nyní už nám nic nebrání vytvořit první dotaz, který nám vypíše do konzole data za použití tabulky zkusebni:

private void jooq_Query1() {
        DSLContext create = DSL.using(connH2(), SQLDialect.H2);
        Result<Record> result = create.select().from(ZKUSEBNI).fetch();

        for (Record r : result) {
            Integer id = r.getValue(ZKUSEBNI.HID);
            Integer cis = r.getValue(ZKUSEBNI.HCELE);
            BigDecimal des = r.getValue(ZKUSEBNI.HDES);
            BigDecimal mad = r.getValue(ZKUSEBNI.HMALE);
            String str = r.getValue(ZKUSEBNI.HRETEZ);
            Date dtm = r.getValue(ZKUSEBNI.HDATUM);

            System.out.println("ID= " + id.toString() + " Cele= " + cis.toString() + " Desetinne= " + des.toString()
                    + " Male des.= " + mad.toString() + " String: " + str + " Datum= " + dtm); } }

Při použití druhé varianty kódu stačí změnit pouze jeden řádek:

DSLContext create = DSL.using(this.dataSource, SQLDialect.H2);

Kód nemá smysl komentovat, protože je až na jiný databázový dialekt, jiný typ klíčové položky a odkazovanou tabulku úplně stejný, jako byl v minulých ukázkách JOOQ. Vytvořenou proceduru připojíme k akci příslušného tlačítka, ale zatím nebudeme spouštět. Pokud bychom to udělali, dostali bychom chybové hlášení, které souvisí s deklarací widgetů tabulky a jejích sloupců. Pro porovnání uvedeme formát, který jsme při použití JOOQ měli v minulých dílech:

@FXML private TableView<Record> table1;
@FXML private TableColumn<Record, String> col1;
@FXML private TableColumn<Record, String> col2;
@FXML private TableColumn<Record, String> col3;
@FXML private TableColumn<Record, String> col4;
@FXML private TableColumn<Record, String> col5;
@FXML private TableColumn<Record, String> col6;

V aktuální třídě si provedeme zjednodušení, které bude funkční a mnohem přehlednější. Vystačíme si pouze ze dvěma řádky:

@FXML private TableView<Record> table1;
@FXML private TableColumn<?, ?> col1, col2, col3, col4, col5, col6;

Deklarace tabulky se nezměnila, ale u sloupců došlo ke dvěma zásadním změnám – nespecifikovali jsme žádný konkrétní typ hodnot a deklarovali jsme všechny sloupce na jednom řádku. První změnu objasníme později a ta druhá souvisí s tím, že zde deklarujeme odkazy na widgety podobným způsobem, jako deklarujeme proměnné. A pokud mají proměnné (naše widgety) stejný typ, je možné jejich deklarace sdružit. Mohlo by se to samozřejmě udělat i pro ostatní deklarované widgety, ale to nemá smysl demonstrovat. Tímto krokem už máme vše připravené a můžeme novou verzi aplikace vyzkoušet. Její funkčnost dokazuje pátý obrázek v galerii. Abychom si ukázali i funkčnost vytvořeného pohledu, vytvoříme si kopii této procedury, přejmenujeme ji a nahradíme název tabulky názvem pohledu (místo ZKUSEBNI bude H2POHLED). Změníme přiřazení akci tlačítka a znovu vyzkoušíme zobrazení údajů, tentokrát pomocí pohledu. Výsledek je vidět na šestém obrázku galerie. Z obou zkoušek je zřejmé, že umíme data z tabulky přečíst a zobrazit je v konzole. Pro nás je ale důležité, abychom zvládli hlavně zobrazení dat v tabulce. To si ukážeme následně. Kdybychom se vrátili do minulých dílů o JOOQ tak zjistíme, že k tomuto účelu potřebuje tři procedury (inicializace sloupců tabulky, zarovnání hodnot v jednotlivých sloupcích a „volací“ procedura) a jednu výkonnou funkci. Nejjednodušší situace bude u procedury pro zarovnání hodnot ve sloupcích, kde stačí prostě z minulých dílů zkopírovat proceduru alignTableColumn.

Ostatní položky už tak jednoduché nebudou. Už třeba jenom proto, že si ukážeme jinou, mnohem jednodušší, možnost inicializace sloupců tabulky. V dřívějších dílech jsme používali proceduru, která byla jednak celkem složitá a navíc neumožňovala použít v deklaracích widgetů nedefinovaný typ hodnot:

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));
                } }); } }

Nová varianta bude mnohem jednodušší a přehlednější:

private void initCol(final TableColumn[] column, final String[] colName) {
        for (int i=0;i<column.length;i++)
            column[i].setCellValueFactory( new PropertyValueFactory<>(colName[i])); }

Procedura má dva parametry – seznam sloupců (z deklarace widgetů) a jejich názvů (z původní tabulky). Pro samotnou inicializaci se používá velmi jednoduchá konstrukce, která nevyžaduje explicitní definici typů hodnot pro sloupce. K této proceduře se vrátíme ještě za chvíli a v tuto chvíli si vytvoříme výkonnou funkci. Už máme vyzkoušeno, že použití pohledu místo tabulky funguje bez problémů. Díky tomu a globálnímu importu příslušné funkce můžeme zjednodušit také příkaz pro čtení dat:

private ObservableList<Record> dataView() {
        DSLContext create = DSL.using(connH2(), SQLDialect.H2);
        ObservableList<Record> data = observableArrayList(create.select().from(ZKUSEBNI).fetch());
        System.out.println(data.toString());
        return data; }

Pro duhou variantu kódu opět stačí jedna jediná změna:

DSLContext create = DSL.using(this.dataSource, SQLDialect.H2);

Ve funkci jsme zachovali původní typ. Změnil se samozřejmě odkaz na připojení databáze a databázový dialekt. Díky opatřením popsaným výše se mohl zkrátit a zjednodušit příkaz pro čtení dat, který obsahuje pouze tři příkazy a vejde se v pohodě na jeden řádek. Jako poslední vytvoříme volací proceduru:

private void viewTable() {
        ObservableList<Record> data = dataView();
        final TableColumn[] tc = new TableColumn[]{col1, col2, col3, col4, col5, col6};
        final String[] cn = new String[]{"hid","hcele","hdes","hmale","hretez","hdatum"};
        //final String[] cn = new String[]{"id","celecis","descis","maledes","retezec","datum"};
        initCol(tc,cn);
        alignTableColumn(tc, new String[]{"CA", "RA", "RA", "RA", "LA", "CA"});
        table1.setItems(data); }

První dva řádky jsou shodné s minulou verzí procedury. Třetí řádek je nový a definuje pole s názvy jednotlivých sloupců. Pro srovnání uvádíme i variantu pro PG tabulku. V této souvislosti je třeba uvést jednu důležitou informaci: názvy položek v tabulce jsou ve všech generovaných aplikačních třídách pro H2 uváděné pomocí velkých písmen (na rozdíl od tříd pro PG!). V definici pole názvů je ale nutné použít malá písmena. Pokud bychom nechali velká, žádná chyba se neobjeví, ale data v tabulce také ne!!! Další řádek pomocí dvou definovaných polí zajistí inicializaci sloupců tabulky a poslední dva příkazy pro zarovnání hodnot a vložení sady údajů do tabulky jsou opět stejné. Spuštění volací procedury vložíme do procedury initialize a při spuštění ukázkové úlohy se data z pohledu skutečně objeví v tabulce – viz předposlední obrázek v galerii. Pouze pro věření vyměníme ve výkonné proceduře pohled za tabulku a provedeme spuštění znovu. Výsledek je pak viditelný na posledním obrázku galerie. Z obou obrázků je patrné, že data nejsou nijak formátovaná a jsou zobrazena přesně tak, jak jsou uložená v tabulce.

ict ve školství 24

Na závěr dnešního dílu dáváme do přílohy aktuální kód příslušné procedury: samexam7.java.

V dnešním dílu jsme se věnovali připojení H2 databáze do ukázkové aplikace, konfiguraci JOOQ a zobrazení dat z tabulky a pohledu do konzole i příslušného widgetu. V příštím dílu si ukážeme možnosti H2 při manipulaci s CSV soubory.