Umíme to s Delphi: 80. díl – pod povrch formulářů Master/Detail

V minulém dílu jsme společně vytvořili databázovou aplikaci používající formulář Master/Detail a zobrazující tak data ze dvou databázových tabulek. Dnes se podíváme do nitra této aplikace, vysvětlíme si, jak funguje, proč tak funguje, a jak ji modifikovat a vylepšovat.
V minulém dílu jsme společně vytvořili databázovou aplikaci používající formulář Master/Detail a zobrazující tak data ze dvou databázových tabulek. Dnes se podíváme do nitra této aplikace, vysvětlíme si, jak funguje, proč tak funguje, a jak ji modifikovat a vylepšovat. Přitom samozřejmě objevíme řadu dalších důležitých databázových otázek a odpovědí, ujasníme si databázovou architekturu, použijeme parametrizovaný dotaz, poznáme aktuální záznam v datasetu, a tak dále...

Jak fungují formuláře Master/Detail?

Před týdnem jsme společně využili průvodce Delphi – Database Form Wizard a vytvořili databázovou aplikaci, jejímž hlavní (a momentálně jediný) význam spočívá v realizaci formuláře Master/Detail. Dnes si tuto aplikaci podrobně analyzujeme, proto doporučuji všem čtenářům, kteří případně nečetli předchozí článek, aby si jej nejprve prošli a seznámili se s vytvořenou aplikací.

Pojďme se podívat na podrobný způsob, jakým funguje vygenerovaný formulář Master/Detail. Poznáme totiž několik velmi důležitých a zajímavých databázových maličkostí.

Datový modul obsahuje dva dotazy Query a dva datové zdroje DataSource. Komponenty Query1 a DataSource1 se týkají tabulky Master (tedy tabulky KUCHARI). Získávají data na základě tohoto dotazu:

Select
  kuchari."Idkuchare",
  kuchari."Jmeno",
  kuchari."Plat"
From "kuchari.db"
As kuchari

Výsledkem tohoto dotazu jsou všechny tři atributy (idkuchare, jmeno, plat) ze všech záznamů uvedených v tabulce KUCHARI. Na formuláři jsou celkem tři komponenty uživatelského rozhraní (tři editační pole), které zobrazují údaje tabulky KUCHARI. Tato editační pole samozřejmě dokáží zobrazit v jednom časovém okamžiku pouze jeden údaj (jeden záznam), na rozdíl od mřížky, která zobrazí všechny výsledky dotazu zároveň. Otázka tedy zní, jakým způsobem je určeno, který záznam z výsledku dotazu je zobrazen editačními poli?

K zodpovězení tohoto dotazu je nutné říci si několik podrobností k databázovým komponentám uživatelského rozhraní.

Komponenta DBEdit

Komponenta DBEdit (stejně jako řada dalších analogických komponent, které poznáme v některém z příštích dílů seriálu) se nalézá na záložce DatControls palety komponent. Komponenta se na pohled nijak neliší od komponenty Edit, dokáže tedy zobrazit jednořádkový údaj a umožnit jeho editaci.

Komponenta vždy zobrazuje aktuální hodnotu tzv. datového pole, ke kterému je připojena. Předpokládejme například, že máme komponentu DataSource, která se nazývá CustomersSource, která je aktivní a která je připojena k otevřenému datasetu nazvanému CustomersTable. V takovém případě je možné použít komponentu DBEdit a nastavit její vlastnosti takto:

vlastnost DataSource: CustomerSource
vlastnost DataField: CustNo

Tato komponenta bude okamžitě zobrazovat hodnotu aktuální řádky sloupce CustNo datasetu CustomerTable, a to jak v době návrhu, tak i za běhu aplikace.

Z řečeného vyplývá, že komponenta DBEdit se používá podobně jako „obyčejný“ Edit, ovšem při nutnosti správně nastavit nezbytné vlastnosti specifikující databázi, tedy DataSource a DataField. Hodnotou vlastnosti DataSource je vždy vhodný datový zdroj (typicky komponenta DataSource), hodnotou vlastnosti DataField je pak typicky název sloupce v dané tabulce.

Zpět k průzkumu

Pokud se vrátíme k naší konkrétní situaci, zjistíme, že situace je nyní jasná: napříkald komponenta EditJmeno (jak ji pojmenoval průvodce) má zobrazovat jméno kuchaře, proto bude nastavena takto:

vlastnost DataSource: DataModule2.DataSource1 (protože datový zdroj DataSource1 je „napojen“ na datovou sadu Query1, která „doluje“ data z tabulky KUCHARI)

vlastnost DataField: Jmeno (protože editační pole má zobrazovat hodnotu ve sloupci Jmeno)

Všimněte si také skutečnosti, že kdybychom upravili databázový dotaz ve vlastnosti SQL komponenty Query1 takto:

Select
  kuchari."Idkuchare",
  kuchari."Plat"
From "kuchari.db"
As kuchari

(tedy bychom z dotazu vypustili jméno kuchaře), nebude aplikace fungovat: po jejím spuštění se nastaví vlastnost Active komponenty Query1 na True, nicméně systém zjistí, že požadovaný sloupec Jmeno, na který odkazuje vlastnost DataField editačního pole EditJmeno, se nenachází v datovém zdroji specifikovaném vlastností DataSource komponenty EditJmeno; dále zjistí, že datový zdroj DataSource1 získává data ze sady představované dotazem Query1, a tedy vyhodí chybové hlášení. Aplikaci sice spustí, ale nedojde k nastavení vlastnosti Active komponenty Query1 na True a přístup k datům bude tedy odříznut. Vypadá to velmi komplikovaně, ale v důsledku je to logické.

Možná někoho napadá ještě další otázka: jak komponenta EditJmeno (a další editační pole DBEdit) „poznají“, kterého kuchaře mají vypisovat? Jinak řečeno – jak se pozná, který záznam získaný z tabulky KUCHARI je právě aktivní?

Odpověď zní tak, že komponenta DBEdit to sama nepozná, nicméně „dozví“ se to od datové sady. Každá komponenta datové sady (v našem případě tedy komponenta Query1) udržuje v každém okamžiku informaci o tom, který záznam z výsledku je v danou chvíli aktuální. Podrobnější popis by vyžadoval mnoho řádků a přitom asi není úplně účelný. Představte si, že v okamžiku, kdy komponenta Query získá z databáze výsledky dotazu, vznikne jakýsi kurzor, který začne ukazovat na první záznam výsledku (proto také po spuštění aplikace vidíme v komponentách DBEdit prvního kuchaře). Komponenta dále disponuje celou řadou metod pro přesouvání takového kurzoru, např. metoda Next přesune kurzor na další záznam, metoda Last na poslední záznam apod. Můžete si to vyzkoušet; přidáme do aplikace tlačítko a ošetříme jeho událost OnClick:

procedure TForm3.Button1Click(Sender: TObject);
begin
  DataModule2.Query1.Last;
end;

Po klepnutí na toto tlačítko se v aplikaci zobrazí poslední kuchař. Komponenta DBNavigator, která je v aplikaci také použita, v podstatě interně volá tyto metody komponenty Query, takže po klepnutí na tlačítko s šipkou doprava zavolá interně metodu DataModule.Query1.Next, čímž se kurzor posune na další záznam a komponenty DBEdit zobrazující pole aktuálního záznamu automaticky ukáží nové údaje.

Množství dostupných metod se pro jednotlivé datasety liší v závislosti na tom, jak komponenta (dataset) funguje a jak získává informace z databáze. Některé datasety jsou třeba tzv. jednosměrné, takže u nich neprobíhá cachování získaných výsledků dotazu a navigace je u nich možná pouze jedním směrem (třeba pomocí metod Next a Last). S těmito datovými sadami se setkáme později.

Proč se v mřížce zobrazují vždy údaje o aktuálním kuchaři?

Další dotaz týkající se naší Master/Detail aplikace zní, jak je zajištěno, aby se v mřížce zobrazovaly vždy údaje o konkrétním (aktuálním) kuchaři? O mřížku (a vůbec o „detail“ část aplikace a tedy o tabulku UMIVARIT) se starají druhé dvě komponenty datového modulu: dataset Query2 a datový zdroj DataSource2.

Po mřížce vyžadujeme, aby v každé situaci zobrazovala všechny záznamy týkající se toho kuchaře, který je zobrazen v komponentách DBEdit, a tedy který je aktuální v datové sadě Query1. Zbývá tedy zajistit, aby se mřížka vždy dozvěděla, který z kuchařů má takové štěstí a je aktuální v Query1.

Mřížka DBGrid to samozřejmě nebude zjišťovat, ani se jí to netýká, neboť jejím úkolem je pouze zobrazovat data, která získá ze svého datového zdroje. Tím je DataSource2, jehož datovou sadou je Query2. Zodpovědnost za určení aktuálního kuchaře tedy leží na bedrech komponenty Query2.

Abychom pochopili odpověď, bude nejlepší vyjít opět z dotazu, který je uveden ve vlastnosti SQL komponenty Query2:

Select
  umivarit."Idkuchare",
  umivarit."Idjidla"
From "umivarit.db"
As umivarit
Where
  "umivarit"."Idkuchare" =:"Idkuchare"

Všimněte si, že je použito tzv. parametrického dotazu (těmito dotazy jsme se zabývali v díle 78). Parametr je použit v klauzuli WHERE, jejíž pomocí redukujeme výsledky dotazu na záznamy, které splňují danou podmínku. Podmínka stojí tak, že nás zajímají pouze takové záznamy z tabulky UMIVARIT, jejichž atribut IDKUCHARE má hodnotu :IDKUCHARE. Protože aplikace funguje, je zřejmé, že v tom je celé kouzlo: díky výrazu :IDKUCHARE komponenta Query2 vždy vrátí správná data.

Avšak nejen díky němu. Nyní přichází klíčový moment celých manévrů: využijeme totiž vlastnost DataSource komponenty Query2.

Zmatení? DataSet? DataSource?

Věřím, že i úplní databázoví začátečníci z předchozích dílů seriálu dokonale pochopili, co je myšleno pojmy „datový zdroj“ (DataSource) a „datová sada“ (DataSet). Bohužel nyní je zcela pochopitelné, že se někteří čtenáři cítí zmateni, neboť skutečnost, že komponenta vystupující jako DataSet má vlastnost DataSource, je zdánlivě zcela nesmyslná.

Dosud jsme si databázovou architekturu představovali tak, že kdesi existuje nějaký databázový stroj, který je připraven poskytovat data. Dále máme komponentu, s jejíž pomocí se k tomuto stroji fyzicky připojujeme (při práci s BDE ji nemusíme využívat). Další vrstva spočívá v komponentě datové sady, která získá data z tohoto připojení k databázi (příp. v případě BDE přímo z databázového souboru), spravuje aktuální záznam a je připravena poskytnout data dalším zájemcům. Další vrstvou je datový zdroj (DataSource), který převezme data z datasetu a poskytuje je v „přenositelné“ formě (nezávislé na databázové platformě) komponentám uživatelského rozhraní, které tvoří poslední vrstvu.

Z tohoto chápání databázové architektury vyplývá, že dataset se vůbec nezajímá o datový zdroj. Datasetu je zcela lhostejné, jaký datový zdroj si od něho vezme data a komu tato data tedy poskytne. Proto jsme při práci s komponentou datasetu (např. s komponentou Query) až dosud ani jednou nepronesli termín DataSource. Závislost je zcela opačná: teprve datový zdroj datasource se z pochopitelných důvodů „zajímá“ o to, s jakým datasetem má komunikovat. Je to zřejmé, musí přece vědět, od koho získávat data.

Rád bych uklidnil všechny, kteří se teď začínají obávat věty typu: „ode dneška však budeme nahlížet na databázovou architekturu zcela jinak a budeme v ní hledat i opačné závislosti“. K ničemu takovému nedojde, musíme si pouze lehce rozšířit povědomí o tom, co všechno dokáží datasety.

Vlastnost DataSource komponenty Query

Komponenta Query, jakožto zástupce vrstvy datové sady (DataSetu), disponuje vlastností DataSource. Neznamená to však, že bychom jejím nastavením jednoznačně a direktivně stanovili, komu (jakému datovému zdroji) smí příslušná komponenta Query (příslušný dataset) poskytovat data.

Vlastnost DataSource se používá výhradně v případech, v nichž potřebujeme pracovat s parametrickými dotazy. Tato vlastnost totiž specifikuje datový zdroj (komponentu DataSource), z něhož se mají získávat hodnoty aktuálních polí pro přiřazení hodnot parametrům v parametrických SQL dotazech.

Předvedeme si to velmi názorně.

Máme komponentu DataSource1, který z tabulky KUCHARI získává položky Idkuchare, Jmeno a Plat. Nyní je lhostejné, od koho DataSource1 získává data, důležitý je pouze fakt, že tato data má.

Dále máme komponentu Query2, která získává data z tabulky UMIVARIT, ale kterou zajímají pouze ty záznamy z tabulky UMIVARIT, jež se týkají pouze aktivního kuchaře z množiny kuchařů existujících v DataSource1.

Řešení: v Query2 použijeme parametrický SQL dotaz a jako parametr uvedeme atribut, jehož hodnota se má získat z datového zdroje DataSource1. Pokud nás zajímají kuchaři, jejichž ID (Idkuchare) je rovno aktuálnímu kuchaři z množiny v DataSource1, použijeme parametr Idkuchare (z minula víme, že tento parametr se zapíše pomocí úvodní dvojtečky, tedy :idkuchare).

A na závěr to nejdůležitější: aby Query2 věděla, který kuchař je aktivní v množině všech kuchařů v DataSource1, uvedeme komponentu DataSource1 do vlastnosti DataSource komponenty Query2. Tím zajistíme, že Query2 bude hlídat DataSource1 a průběžně nastavovat hodnotu parametru :idkuchare podle toho, který kuchař je zrovna aktivní. Pokud je jméno parametru stejné jako název sloupce, jsou parametry vyhodnocovány automaticky. V opačném případě je nutné zajistit vše programově.

Je asi zřejmé, že vlastnost DataSource musí být nastavena na komponentu DataSource jiného datasetu, nelze se z A odkazovat na datový zdroj, který naopak získává data z A.

Vlastnost DataSource se nejčastěji používá právě v případech formulářů Master/Detail.

Jména jídel namísto jejich ID

Tím jsme si zodpověděli na všechny otázky přicházející v úvahu při práci s formuláři Master/Detail. Vzhledem k tomu, co jsme se všechno dozvěděli, nebude určitě nyní problém zařídit, aby se v mřížce nezobrazovaly ID jídel, které umí vařit aktuální kuchař, ale přímo jejich názvy. Postačí malá změna dotazu ve vlastnosti SQL komponenty Query2:

Select
  umivarit."Idkuchare",
  umivarit."Idjidla",
  jidla."Jidlo"
From "umivarit.db" As umivarit,
"jidla.db" As jidla
Where
  "umivarit"."Idkuchare" =:"Idkuchare"
And
    "jidla"."idjidla" = "umivarit"."idjidla"

Aby všechno fungovalo, je nutná ještě maličkost: přidat do datového modulu tzv. datové pole, což je komponenta, která umožňuje ve spolupráci s datovými sadami mít kontrolu nad zobrazením a editací jednotlivých polí z výsledku dotazu. Klepněte tedy pravým tlačítkem na komponentu Query2, z popup menu zvolte Fields Editor, uvnitř tohoto editoru klepněte opět pravým tlačítkem, zvolte Add Fields a vyberte pole Jidlo. Aplikace je nyní funkční.

Poznámka k polím: ty do aplikace „zanesl“ průvodce. Pokud bychom aplikaci vytvářeli sami, vůbec bychom je nepoužili a vůbec by nám to ani nevadilo. Kdybychom vymazali obě současné pole komponenty Query2, tedy idjidla a idkuchare, bude efekt stejný, jako když přidáme nové pole jidlo (totiž DBGrid zobrazí všechna pole).

Na závěr

Věřím, že jste dnešní díl shledali užitečným. Ne ani tak kvůli novým poznatkům, které jsme načerpali o formulářích Master/Detail, i když samozřejmě tyto formuláře umožňují realizovat velmi častý požadavek zobrazení dat ze dvou tabulek ve formě 1:N. Nejdůležitější pro dnešek však je, že jsme poznali několik dalších databázových „specialit“, které nás v Delphi mohou překvapit. Především zmatení pojmů Dataset a Datasource by mohlo přinést problémy, proto doufám, že uvedený popis rozptýlí všechny vaše pochybnosti.

Diskuze (8) Další článek: Nová verze bezinstalačního Linuxu

Témata článku: Software, Programování, Uživatelské jméno, Otevřená data, Master, Klíčový moment, Select, From, Opačný případ, Zdroje, Povrch, Last, Plat, Důležitý sloupec, Detail, Pole, Malá změna, Celý manévr, Sloupec, Formulář, Díl, Důležitý údaj, Komponenta

Určitě si přečtěte


Aktuální číslo časopisu Computer

Test 6 odolných telefonů a 22 powerbank

Srovnání technologií QLED a OLED

Měřte své sportovní výkony

Sady pro chytrou domácnost