Z 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ář:
- řádek – zde je změna jasná a nutná, ovladač bude samozřejmě jiný
- řádek – deklaraci umístění databázového souboru přebíráme z globální proměnné
- řádek – přihlašovací jméno přebíráme z globální proměnné
- řádek – uživatelské heslo přebíráme z globální proměnné
- řádek – deklaraci typu databáze raději opět ověříme pomocí nápovědy IJI, abychom použiji správný formát
- řádek – zde necháme vygenerovat kompletní obsah databáze
- řá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á
- řá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.
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.