Univerzita Karlova v Praze Matematicko-fyzikální fakulta
BAKALÁŘSKÁ PRÁCE
Jan Drozen Generátor dat pro Microsoft SQL Server Katedra softwarového inženýrství Vedoucí bakalářské práce: RNDr. David Hoksza, Ph.D. Studijní program: Informatika Studijní obor: Informatika - obecná informatika Praha 2013
Na tomto místě bych chtěl poděkovat mému vedoucímu, panu RNDr. Davidu Hokszovi, Ph.D., za odborné vedení bakalářské práce a poskytnuté konzultace.
Prohlašuji, že jsem tuto bakalářskou práci vypracoval samostatně a výhradně s použitím citovaných pramenů, literatury a dalších odborných zdrojů. Beru na vědomí, že se na moji práci vztahují práva a povinnosti vyplývající ze zákona č. 121/2000 Sb., autorského zákona v platném znění, zejména skutečnost, že Univerzita Karlova v Praze má právo na uzavření licenční smlouvy o užití této práce jako školního díla podle §60 odst. 1 autorského zákona.
V Praze dne 21. května 2013
Jan Drozen
Název práce: Generátor dat pro Microsoft SQL Server Autor: Jan Drozen Katedra: Katedra softwarového inženýrství Vedoucí bakalářské práce: RNDr. David Hoksza, Ph.D., Katedra softwarového inženýrství Abstrakt: Testování naprogramovaných komponent je nedílnou součástí vývoje software. Generátor dat pro Microsoft SQL Server má sloužit jako nástroj pro jednoduché naplnění tabulek databáze testovacími daty, která umožňí programátorům, administrátorům a dalším vývojářům databází efektivně testovat databázové komponenty. Generovaná data je možné strukturovat pomocí parametrů, které se nastavují v jednoduchém uživatelském prostředí. Vygenerovaná data odpovídají schématu databáze a podléhají integritním omezením definovaným na sloupcích. V následujícím textu společně s potřebnými teoretickými poznatky popíšeme architekturu a postup implementace aplikace. Klíčová slova: náhodná data, Microsoft SQL Server, generátor, testování, optimalizace
Title: Data Generator for Microsoft SQL Server Author: Jan Drozen Department: Department of Software Engineering Supervisor: RNDr. David Hoksza, Ph.D.,Department of Software Engineering Abstract: Testing of newly programmed components is an integral part of software development. Data Generator for Microsoft SQL Server is a tool for simple populating database tables with testing data which allows efficient database component testing for programmers, administrators and other database developers. Generated data can be structured using the parameter which are set in a simple user interface. Generated data correspond with database schema and constraints defined on columns. In following text we are going to describe the architecture and implementation details together with the necessary theoretical knowledge. Keywords: random data, Microsoft SQL Server, generator, testing, optimization
Potřeba data ukládat a zpětně dohledávat je velice důležitá již od nejstarších civilizací. Moderní databázové systémy jsou vysoce výkonné a umožňují zpracování velkého množství informací. Návrh struktury pro ukládání dat v databázovém systému - databáze - je velmi komplexní proces a skládá se z mnoha kroků, ke kterým by mělo patřit i testování. Za účelem testování libovolné databáze jsme vytvořili nástroj, který programátorům a dalším vývojářům databází umožní jednoduše otestovat některé vlastnosti jejich systému.
1.2
Databáze
Databázi můžeme definovat jako (uspořádanou) množinu dat (informací) uloženou na nějakém médiu. Tuto definici lze ještě rozšířit, takže databází nebudeme rozumět pouze samotná data, ale i prostředky pro manipulaci s nimi. Tím dostáváme určitý systém, na který můžeme nahlížet jako na celek a také ho tak ovládat. Tento systém nazveme Systémem řízení báze dat (SŘBD nebo z angličtiny DBMS[6]). Typický SŘBD umožňuje data ukládat (INSERT), vyhledávat (SELECT) nebo měnit (UPDATE). Takto obecná definice dává velký prostor pro nejrůznější specifikace podle toho, k čemu má být daný systém používán. Jistě existují jiné požadavky na např. informace uložené v sociálních sítích a jiné v bankovních systémech. Pro příklad jmenujme grafové databáze, sloupcové databáze nebo dokumentové databáze. My se však dále budeme zabývat jiným, často používaným typem. A to relační databází. Abychom tento typ odlišili, budeme dále používat standardní zkratku Relational database management system (RDBMS) (Systém řízení relační báze dat (SŘRBD) zní poněkud těžkopádně).
5
1.3
Relační databáze
Relační databáze, jak je patrné z názvu, využívá určitých vztahů. Objekty reálného světa, o kterých chceme zaznamenat nějaká data, modelujeme pomocí entit. Entity pak spojujeme logicky pomocí vztahů - relací. Takovouto soustavu je možné, v praxi běžně využívané, zaznamenat pomocí grafu, tzv. ER-diagramu[7] či schématu databáze.
Obrázek 1.1: ER-model, příklad 1
Entity představují vrcholy a vztahy pak hrany příslušného grafu. Hrany mohou i nemusí být orientované, záleží na konkrétní situaci a pohledu. Tohoto přístupu k relační databázi můžeme využít a aplikovat na databázi celou řadu známých 1 ZDROJ: http://www.ksi.mff.cuni.cz/˜holubova/A7B36DBS/podklady/A7B36DBS_02_ ER2R.pdf
6
grafových algoritmů. V dalším textu se ke grafům vrátíme při konkrétním využití tohoto faktu. Ačkoli to není hlavním cílem této práce, zmíníme krátce pojem normalizace databáze. Stručně řečeno, normalizace nebo též převod databáze do nějaké tzv. normální formy[8] znamená dodržení určitých zásad a pravidel při návrhu schématu databáze. V praxi se normalizace projevuje typicky zvýšením počtu tabulek a s tím souvisejícím počtem relací mezi nimi. Jedním z důvodů, proč jsme se o této věci, která je čtenáři jistě známá, zmínili, je poměrně úzká souvislost s naším tématem. Je zřejmé, že vyhledání informace v databázi (dotaz) závisí na schématu databáze. V extrémním případě může být databáze navrhnuta tak špatně, že nebude schopna vydat výsledky na potřebné dotazy. Pokud se odkloníme od tohoto okrajového případu, dostaneme se do reálnější situace. A to, že schéma databáze neovlivňuje schopnost odpovědět na dotaz, ale ovlivňuje čas potřebný pro jeho zpracování. Když navíc obohatíme pojem schématu o další implementační specifika, získáme pohled na problém, se kterým se denně setkává velké množství databázových programátorů a administrátorů, totiž návrh schématu a dotazů. Proces návrhu dotazů pro naše účely nazveme optimalizací dotazu programátorem.
1.4
Microsoft SQL Server
Poprvé se nyní zmíníme o konkrétní implementaci RDBMS, se kterou budeme dále pracovat. Tou je Microsoft® SQL Server® (MSSQL). Ten patří mezi nejpoužívanější implementace, spolu s např. SQL serverem od Oracle 3
2
nebo MySQL
. Proces optimalizace dotazů programově serverem je důležitou součástí každé
jedné implementace. Nicméně špatně navržené schéma a dotaz se nemusí podařit optimalizovat ani tak sofistikované aplikaci, jakou bezesporu SQL server je. V naší práci budeme Microsoft SQL Serverem, SQL Serverem nebo jen zkratkou MSSQL uvažovat Microsoft® SQL Server® 2008R2. 2 3
Oracle, URL http://www.oracle.com/cz/index.html [citováno 22. května 2013] MySQL, URL http://www.mysql.com/ [citováno 22. května 2013]
7
1.5
Optimalizace databázových dotazů
Není třeba do detailu vysvětlovat proces optimalizace dotazů serverem (optimalizace dotazu je zde poněkud přetížený pojem, nicméně čtenář jistě cítí rozdíl mezi optimalizací dotazu uživatelem jakožto jeho návrhem a optimalizací položeného dotazu programově při vykonávání na serveru). Při jeho přijmutí se převede do strojově čitelné podoby, do relační algebry a najde se dostatečně dobrý způsob, jak dotaz provést. 4 . Konkrétnímu způsobu, jak dotaz vykonat uvnitř SQL Serveru se říká execution plan 5 . Není třeba zdůrazňovat, že se jinak bude vykonávat dotaz, který najde potřebná data v jedné tabulce, narozdíl od jiného, který sémanticky stejnou informaci dohledá spojením několika tabulek - což je přímá souvislost se schématem databáze, resp. normalizací. SQL Server při vytváření execution planu rozhoduje, jak požadovaná data zpřístupnit - přečíst z disku. Tímto se dostáváme k velmi podstatné části optimalizace dotazu programátorem. Dobu potřebnou pro získání informací můžeme velmi netriviálně zkrátit použitím pomocných struktur - indexů 6 . Je vždy velkou otázkou a uměním programátora definovat správné indexy. V SQL Serveru je index implementovaný pomocí B+stromu[9]. Respektive oba indexy, neboť rozlišujeme klastrovaný (clustered), někdy též označovaný jako primární index a neklastrovaný (nonclustered) index. Rozdíl mezi nimi je především ten, že listová úroveň primárního indexu obsahuje přímo uložená data, zatímco neklastrovaný index odkazy do primárního indexu. Pokud nemá daná tabulka definován primární index, neklastrovaný pak obsahuje číslo řádku v neindexované tabulce, které se říká heap (český překlad halda je zavádějící kvůli shodě se setřízenou stromovou strukturou[10]). Existence a definice indexů zřejmě ovlivňuje efektivitu dotazu naprosto zásadním způsobem. Je rozdíl, jestli pro získání jediného záznamu musíme projít všechna uložená data, nebo cíleně vyzvednout z disku pár stránek s požadovanými daty. Tyto operace mají své specifické názvy: 4
dostatečně dobrý způsob nemusí být vždy ideální, např. RDBMS Oracle hledá optimální plán 5 Execution plan, URL http://msdn.microsoft.com/en-us/library/ms178071%28v=sql. 105%29.aspx [citováno 22. května 2013] 6 Indexy v SQL Serveru, URL http://msdn.microsoft.com/en-us/library/ms175049% 28v=sql.105%29.aspx [citováno 22. května 2013]
8
• Table scan - provádí se na heapu a je to v podstatě známé hledání jehly v kupce sena. S lineární složitostí musíme v nejhorším případě prohledat celý heap. • Index scan - sekvenčně se prochází celá listová úroveň daného indexu. • Index seek - cíleně se projde stromem od kořene do listu pro konkrétní záznam • Key lookup - k získaným informacím z indexu se ještě dohledávají další pomocí jiného indexu
Obrázek 1.2: Execution plan - grafické zobrazení Za nejlepší se dá považovat index seek, ale opět záleží na konkrétní situaci. SQL Server se rozhoduje podle statistik, které si automaticky vytváří. Ty mu pak napoví, zda-li se vyplatí vyhledávat záznamy pomocí průchodů stromem nebo použít index scan. A právě v tento okamžik se dostáváme do bodu, kdy přirozeně, avšak velmi těsně, přicházíme k hlavnímu motivu naší práce. Optimalizovat požadovaný dotaz samotný nebo související struktury můžeme efektivně tehdy, když máme k dispozici nějaká data pro testování. Lze tak otestovat např. i objemy dat přenesených po síti a další ůdaje, které sice lze odhadnout, nicméně při složitější struktuře mohou být i odhady značně netriviální. Dotaz na tabulku, která má pouze pár málo ručně vložených řádek proběhne vždy rychle.
9
Proto jsme se rozhodli zpracovat aplikaci, která umožní jednoduše naplnit databázi testovacími daty, na kterých bude možné ladit dotazy a provádět další úkony. Naše aplikace navíc umožňuje data základním způsobem strukturovat - tím myslíme přiblížit pseudonáhodná data reálným hodnotám, které by v reálném použití v databázi byly.
10
2. Existující programy Než přejdeme k rozboru naší aplikace, představíme krátce již existující programy, které se zabývají touto tématikou. V krátké rešerši jsme vybrali tyto programy: • EMS Data Generator for SQL Server
7
• Datanamic Data Generator for MS SQL 2011 • Red Gate SQL Data Generator
2.1
8
9
EMS Data Generator for SQL Server
Jedná se o komerční produkt, nicméně existuje levnější nekomerční verze, která stojí 47 USD. Plná verze pak 95 USD10 . Celé workflow je realizované formou průvodce, kdy konfigurace probíhá v jednotlivých krocích. První formulář umožňuje připojení k instanci serveru - lokální nebo vzdálené. Možnosti autentizace jsou standardní. Kontaktování serveru probíhá zřejmě synchronně a tak dochází k “zamrzání” rozhraní. Po přihlášení se zobrazí druhé okno, kde si uživatel zvolí databázi a tabulky, se kterými bude chtít dál pracovat. Potvrzením se přejde k dalšímu kroku, kde se konfiguruje generování pro jednotlivé sloupce. K dispozici jsou generátory náhodných dat, odpovídajících regexpu, inkrementální data nebo z SQL dotazu. Vygenerovaná data je možné vložit přímo do databáze nebo uložit do skriptu. O průběhu je uživatel informován logem. 7
EMS Data Generator, URL http://www.sqlmanager.net/en/products/mssql/ datagenerator, [citováno 22. května 2013] 8 Datanamic Data Generator, URL http://www.datanamic.com/ datagenerator-for-mssql/index.html, [citováno 22. května 2013] 9 Red Gate SQL Data Generator, URL http://www.red-gate.com/products/ sql-development/sql-data-generator/, [citováno 22. května 2013] 10 platné k 21.5.2013
11
Obrázek 2.1: EMS Data Generator
2.2
Datanamic Data Generator for MSSQL 2011
Další komerční nástroj, dražší než v případě EMS - s cenou 20011 USD, běží jako nativní aplikace. Po spuštění se zobrazí interface podobný dříve uvedenému. Adresu instance nelze vybrat z připraveného seznamu, je potřeba vše zadat ručně, nebo využít dříve uložené spojení. V prvním kroce uživatel vybírá i databázi, se kterou chce dále pracovat. Navíc je zde tlačítko pro otestování připojení. V dalším kroce se zobrazí seznam tabulek zvolené databáze, ze kterého si uživatel opět vybere ty, pro které bude chtít generovat data. Možnosti generování jsou širší. K dispozici je náhodný text, regex, uživatelský soubor, uživatelský seznam, sql dotaz a pohled. Dále je možnost vybírat z předdefinovaných sad, jako jsou například adresy, zaměstnání, atd. Je možné definovat skripty, které se spustí před a po generování, stejně jako lze vypnout nebo zapnout triggery. 11
tato cena platila v době vzniku textu, v době publikace již byla (pro nás celkem nepochopitelně) 499 USD [citováno 22. května 2013]
12
Obrázek 2.2: Datanamic Data Generator
2.3
Red Gate SQL Data Generator
Z našeho seznamu poslední nástroj, který běží na platformě .NET, je s cenou 295 USD druhý nejdražší. Avšak vychází ze srovnání jako nejlepší. Principem ovládání se spíše odklání od konceptu průvodce a nabízí méně oken s bohatšími možnostmi. Úvodní formulář poskytuje obvyklé možnosti připojení. Při přihlašování znovu dochází k problému s odezvou UI, které je jinak přehledné a velice dobře zpracované. Nejvíce patrné to je v hlavním okně, kde se nastavuje generování pro jednotlivé tabulky, resp. sloupce. U nich je možné se rozhodovat, pro které se má generovat a pro které ne. Z generátorů dat jsou k dispozici generování podle regexpu, ze seznamu slov, podle definované váhy, z SQL dotazu, z CSV, ze souboru a také z uživatelského Python scriptu. Také předdefinovaných
13
sad je k dispozici dostatek. Užitečná je možnost vymazat obsah tabulky před generováním.
Obrázek 2.3: Red Gate SQL Data Generator Ačkoli bychom rádi provedli i nějaké konkrétnější srovnání uvedených programů, nemůžeme tak učinit kvůli jejich prodejní politice. Až na generátor od RedGatu není k dispozici žádná verze vhodná k otestování. První dva zmiňované mají modifikaci trial, která je omezená malým počtem záznamů k vygenerování a to maximálně 100 řádků. Na takto malém množství není možné výkonnostní rozdíly otestovat. Oproti tomu RedGate dává k dispozici daleko použitelnější, která je na funkčnosti omezena pouze dvoutýdenní zkušební dobou.
14
3. Generátor dat pro Microsoft SQL Server Naši aplikaci jsme se rozhodli nazvat anglicky Random data generator for Microsoft SQL Server. Relativně dlouhý název budeme v textu nahrazovat zkratkou RDG. Naším cílem bylo vytvořit program jednoduchý na použití, ale přitom obsahující všechny funkce potřebné pro jeho plnou použitelnost. Uživateli dává zdarma k dispozici nástroj pro snadné naplnění databáze strukturovanými daty nejdůležitějších datových typů. Vývoj jsme optimalizovali podle předpokládaného typického využití. Programátor nebo administrátor bude chtít otestovat novou databázovou komponentu v rozsahu maximálně desítek tabulek a objemem dat v řádu desítek tisíc řádků. RDG lze samozřejmě použít i na rozsáhlé databáze a naplnit je miliony řádků dat. Nicméně i naše zkušenosti z praxe odpovídají dříve uvedenému scénáři testování jednotlivých menších částí. Cílem je poskytnout data pro testování dotazů, proto neuvažujeme použití pro testování propustnosti transakčních systémů, které by vyžadovalo obrovské objemy dat. RDG si také klade za cíl použitelnost na běžném počítači a některé náročnější operace přesunout na server.
3.1
Funkce a vlastnosti
• možnost výběru tabulek a sloupců pro generování • informace o struktuře zvolené databáze • možnosti nastavení jednotlivých generátorů - již zmíněné základní strukturování dat • dodržování integritních omezení (je-li to možné) • možnost uložit a znovu načíst konfiguraci • intuitivní a přehledné uživatelské rozhraní.
15
3.2
Použité technologie
RDG je napsán v jazyce C# 5.0 a běží na platformě .NET4.5. Pro spuštění je vyžadován operační systém Microsoft Windows s podporou .NET Frameworku 4.5, tj. Windows Vista, Windows 7 a Windows 8, resp. Windows Server 2008 (R2). Předpokládá se také použití pro server Microsoft SQL Server 2008R2. Minimální hardwarové konfigurace odpovídají specifikacím u uvedeného softwaru.
3.3
Platforma .NET 4.5/C# 5.0
Tuto platformu, resp. jazyk jsme se rozhodli použít, protože přináší některé velmi zajímavé funkce. Platforma .NET má k dispozici několik způsobů práce s SQL Serverem, které nazýváme takto: ADO.NET Connected[1], ADO.NET Disconnected [1] a EntityFramework 12 . Zjednodušeně by se dalo říci, že jedna technologie je základem druhé a v pořadí, v jakém jsme je nyní uvedli, roste i úroveň abstrakce. EntityFramework (EF) je nástroj pro tzv. OR-Mapping 13 . Tím rozumíme technologii převádění entit relační databáze na objekty imperativního programovacího jazyka. EF se běžně využívá v produčním prostředí a je považován za plně použitelný. Avšak my jsme se ho rozhodli nevyužít. A to z důvodu, že předem neznáme schéma databáze. Právě pro případy, kdy pro již navrženou databázi píšeme klientskou aplikaci, je EF vhodný. Využívá různé pomocné struktury, které je třeba generovat pro schéma konkrétní DB. Jako druhou jsme zmínili ADO.NET Disconnected. Pod tímto pojmem si představujeme techniku dostupnosti dat z databáze u klienta za použití objektů, kterým se říká např. adaptér nebo dataset. Pomocí těchto nástrojů chceme u klienta vzbudit dojem, že pracuje s lokálními daty, která jsou ve skutečnosti na serveru. Adaptér se připojí k serveru a potřebnou podmnožinou dat naplní dataset. Uživatel pracuje namísto s celou databází jenom s datasetem. Případné úpravy se na server dostanou zpátky přes adaptér, aniž by se uživatel musel starat 12
ADO.NET Entity framework, URL http://msdn.microsoft.com/en-us/data/ef.aspx [citováno 22. května 2013] 13 OR-mapping, URL http://en.wikipedia.org/wiki/Object-relational_mapping [citováno 22. května 2013]
16
o databázové dotazy nebo připojení. V průběhu vývoje se ukázalo, že použití této technologie je pro RDG poněkud těžkopádné a mnohdy i pomalé. Navzdory tomu, že v některých případech používáme obdobný přístup, nejdůležitější je pro nás technika ADO.NET Connected. Ta je naprosto základní a umožňuje přímý přístup k SQL Serveru na úrovni připojení a jednotlivých dotazů. Máme tak přímou kontrolu nad tím, co SQL Server zpracovává a dává nám to možnost vlastního OR mapování. Jako další nástroj pro práci s databází využíváme knihovny komunikující s SQL Server Compact Edition 14 databází. Tato verze SQL Serveru je tzv. embedded databáze - tedy databáze uložená v jediném souboru, se kterou se manipuluje pomocí příslušného API. Narozdíl od plnohodnotného SQL Serveru nejde o systémovou službu. Při výběru technologie pro tvorbu grafického uživatelského rozhraní jsme vybrali technologii Windows Presentaion Foundation(WPF). Rozhraní je možné vytvářet dvěma rozdílnými způsoby - imperativně pomocí kódu v C#, jako je tomu např. u staršího typu WinForms, nebo neprocedurálně použitím speciálního značkovacího jazyka Extensible Application Markup Language (XAML), který vychází z XML. Tento jazyk umožňuje jednoduše deklarovat různé komponenty rozhraní a navíc i jejich chování. Více o WPF je možné nalézt ve vynikající knize od Andrewa Troelsena [1].
3.3.1
async-await
V předchozím textu jsme zmínili nové funkce platformy .NET. V tomto bodě bychom se rádi zmínili o jedné velmi užitečné funkcionalitě, kterou jsme také v aplikaci využívali. Jde o koncept takzvaných awaitables, resp. novou syntaxi async-await. Jedná se o další využití již dříve existující technologie Task Parallel Library (TPL), která je .NET implementací návrhového vzoru Thread Pool 15 . Díky ní nemusí programátor explicitně vytvářet nová vlákna, když potřebuje nějaký blok kódu vykonat asynchronním způsobem. Vytvoření nového vlákna 14
SQL Server Compact, URL http://www.microsoft.com/en-us/sqlserver/editions/ 2012-editions/compact.aspx [citováno 22. května 2013] 15 Thread pool, URL http://social.technet.microsoft.com/wiki/contents/articles/ 13245.thread-pool-design-pattern.aspx [citováno 22. května 2013]
17
je náročná operace, takže motivací je zde úspora systémových zdrojů. Syntaxe async-await zjednodušuje možnosti asynchronního vykonávání kódu bez použití zpětných volání - callbacků. Uveďme jednoduchý příklad pro ilustraci: Nechť máme jednoduchý formulář s tlačítkem a oblastí zobrazující text. Po stisknutí tlačítka se chceme připojit k nějakému serveru, stáhnout z něj požadovaný text a ten zobrazit. Připojování k serveru, odesílání požadavku, jeho zpracování a přijetí výsledků může trvat z uživatelského pohledu dlouhou dobu, v jejímž průběhu je aplikace zablokovaná. Systém Windows ji navíc může označit textem “Neodpovídá” a nabídnout uživateli její okamžité ukončení. Proto chceme implementovat podobné vylepšení: Po stisknutí tlačítka toto již nebude dostupné pro kliknutí a jeho popisek se změní tak, aby měl uživatel informaci o tom, že probíhá dlouhotrvající operace. Dále se ve vedlejším vlákně stáhne požadovaný text ze serveru. Po dokončení této operace se tlačítko opět zpřístupní a text vypíše do příslušné oblasti. Při použití async-await a TPL je celé řešení velice jednoduché a přímočaré (použijeme částečně pseudokód): 1 p r i v a t e async v o i d b u t t o n C l i c k ( o b j e c t sender , RoutedEventArgs e ) 2
{
3
button . I s E n a b l e d = f a l s e ;
4
button . Content = ” Working ” ;
5
s t r i n g r e s u l t = await getTextFromServer ( ) ;
6
t e x t . Text = r e s u l t ;
7
button . Content = ” Download ” ;
8
button . I s E n a b l e d = t r u e ;
9
}
10 11 p r i v a t e Task<s t r i n g > getTextFromServer ( ) 12
{
13
r e t u r n Task<s t r i n g >. Factory . StartNew (
14
( ) =>
15
{ 18
16
return contactServer () ;
17
}
18
); }
19
První podstatnou věcí je klíčové slovo async v deklaraci metody buttonClick. Async v podstatě značí pouze to, že v těle metody se může objevit klíčové slovo await. Nyní přeskočíme na řádek 11. Zde deklarujeme metodu, která stáhne text ze serveru. Nevrací však přímo řetězcovou hodnotu, ale objekt typu Task<string>. Jde o příslib, že se vykonává úkol, který vrátí požadovaný řetězec. To je vidět i v těle této funkce, kde se TPL předá požadavek na vykonání úlohy stažení textu ze serveru. Vraťme se zpět ke zpracování samotného stisku tlačítka metodou buttonClick. Kroky na 3. a 4. řádku jsou zřejmé - zakážeme klikání na tlačítko a změníme jeho popisek. Podstatný je pro nás řádek číslo 5. Pozorný čtenář si všimne, že do objektu typu string přiřazujeme instanci typu Task<string>. Jediným důvodem, proč tento zápis neskončí chybou při kompilaci je klíčové slovo await. Await informuje odběratele dat právě o dříve zmíněném přislibu vykonávání úkolu. Při běhu programu je v nyní metoda na 5. řádku opuštěna a aplikace běží dál. Avšak ve chvíli, kdy skončí vykonávání metody contactServer, která vrací výsledek, je úloha - Task<string> vyřešena a vrací řetězec objektu result. V tuto chvíli se běh programu vrací zpět do místa, kde přestal, tedy na 5. řádku a pokračuje. Dále už jen vypíše text a obnoví tlačítko. Jak jsme nyní ukázali, tyto nástroje poskytují skutečně silné možnosti, jak psát jednoduše asynchronní kód a využít tak nejen možností vícejádrových procesorů, ale i vytvářet pro uživatele přátelštější uživatelské prostředí.
3.4
Workflow
Nejdříve ve stručnosti představíme průběh ovládání aplikace, tzv. workflow. Následovat bude podrobnější rozbor jednotlivých částí s přihlédnutím k implementačním detailům. RDG je desktopová aplikace pro systém Windows. Desktopová aplikace zdůrazňujeme záměrně, neboť v dnešní době existují pro systém Windows 8 i aplikace 19
s rozhraním ModernUI (dříve Metro). Zvážili jsme, že nové prostředí není vhodné pro aplikaci totoho typu, proto je z uživatelského pohledu tvořena standardními okny. Po spuštění se zobrazí tzv. splash screen 16 . Ten zobrazuje logo a název aplikace, zatímco na pozadí probíhají potřebné operace. Následuje formulář pro připojení k instanci SQL Serveru a výběr databáze, se kterou chce uživatel dále pracovat. Stiskem příslušného tlačítka pokračuje průběh zobrazením hlavního okna. V něm se vybírají tabulky a sloupce pro generování, upravují parametry jednotlivých generátorů a pomocí menu je možné vyvolat další nabídky a příkazy. Dalším krokem je generování, o jehož průběhu je uživatel informován v samostatném dialogovém okně. Po jeho dokončení se opět vrací k hlavnímu oknu, kde je možné pokračovat dalším generováním.
16
Splash screen, URL http://en.wikipedia.org/wiki/Splash_screen [citováno 22. května 2013]
20
Část II Struktura aplikace
21
4. Splash screen Splash screen, jak jsme nazvali úvodní okno, slouží ke zobrazení informací uživateli v době, kdy se načítají údaje potřebné pro start aplikace. Neslouží k interakci, ale jeho použití je uživatelsky příjemnější. Jinak by se potřebné operace prováděly ještě před zobrazením jakékoli grafiky na monitoru. To má za následek subjektivně dlouhý start programu, případně i domněnku uživatele, že se aplikace vůbec nespustila. Zobrazen je motiv a název aplikace. V dolní části se nachází komponenta ProgressBar. Tento ProgressBar je nedeterministický. To znamená, že nezobrazuje, v jakém stavu se zrovna v danou chvíli nachází. Nicméně dává uživateli informaci o tom, že právě probíhá nějaká operace. Dále se tak budeme zmiňovat pouze o deterministickém a nedeterministickém ProgressBaru. Pro použití nedeterministického jsme se rozhodli právě z toho důvodu, že u procedur, které se během doby zobrazení vykonávají, není možné určit jejich stav.
Obrázek 4.1: Splash screen Na pozadí probíhají tyto operace: • Načtení konfiguračního souboru s nastaveními aplikace • Vymazání tabulek z pomocné databáze (bude dále vysvětleno) • Načtení lokálních instancí SQL Serveru • Načtení dříve použitých instancí SQL Serveru
22
Poslední dvě zmíněné položky usnadňují uživateli připojení k serveru. Pro zjištění instancí SQL Serveru v lokální síti jsme použili instanci třídy SqlDataSourceEnumerator, která se nachází ve jmenném prostoru System.Data.Sql. Ta prostřednictvím metody GetDataSources() zpřístupňuje informace o těchto serverech. Nám pro připojení postačuje adresa serveru a jméno instance. Dále se do získaného seznamu přidají uložená jména instancí, ke kterým se již uživatel dříve úspěšně připojil.
23
5. Přihlašovací formulář Přihlašovací formulář umožňuje uživateli připojení k instanci SQL Serveru a výběr databáze, se kterou bude chtít dále pracovat. Logicky je rozdělen právě do těchto dvou částí. Levá polovina obsahuje nabídku lokálních a dříve použitých instancí, kterou je možné editovat a zadat tak i libovolnou adresu. Pod ní se nachází prvky pro nastavení autentizačních údajů. Tzn. klasická pole pro uživatelské jméno a heslo doplněné možností použít přihlášení pomocí účtu uživatele systému Windows. Po stisknutí tlačítka se údaje zpracují a dojde k pokusu o připojení na SQL Server.
Obrázek 5.1: Přihlašovací formulář Pro komunikaci se, jako v celé aplikaci, používá provider pro SQL Server z knihovny ADO.NET. Ten pro navázání připojení potřebuje přístup k potřebným údajům, jako jsou právě adresa a uživatelské údaje. Tato data se provideru předávají ve formě speciálně formátovaného připojovacího řetězce, v literatuře známého pod anglickým názvem connection string. Provider pro SQL Server obsahuje jednu velice užitečnou třídu SqlConnectionStringBuilder. Její instanci stačí předat potřebné údaje o serveru, uživateli a případná další data a provider bude schopen připojení vytvořit i bez speciálního řetězce na základě údajů z instance této třídy. Připojování je prováděno asynchronním způsobem a v průběhu je zobrazen
24
preloader, který informuje o probíhající operaci (bude probráno později).Pokud je připojení úspěšné, získá se ze serveru seznam databází. Ten je zobrazen v pravé části okna, kde uživatel vybírá právě konkrétní databázi. Po jejím výběru je možné pokračovat stisknutím příslušného tlačítka v dolní části.
25
6. Hlavní okno Další komponentu RDG, kterou se nyní budeme zabývat, jsme pro účely tohoto textu pojmenovali hlavní okno. Přesnější název by musel být poněkud delší, avšak označení “hlavní ” je přesné, protože právě v tomto okně probíhá téměř všechna uživatelská interakce. V hlavním okně uživatel vybírá databázové struktury, tj. tabulky a sloupce, pro které chce generovat data, nastavuje jednotlivé generátory a ovládá další nastavení. Jedná se o zásadní a komplexní prvek aplikace, proto se zaměříme spíše na jeho jednotlivé součásti. Z globálního pohledu můžeme hlavní okno rozdělit na 3 důležité sekce: • nabídku databázových struktur • panel informací a nastavení • náhled na data
Obrázek 6.1: Hlavní okno
26
6.1
Nabídka databázových struktur
Jak již bylo zmíněno, tato nabídka se nachází v levé části okna a zobrazuje všechny tabulky a sloupce zvolené databáze.
6.1.1
Schéma databáze
Dříve v textu jsme uvedli termín schéma databáze, který nyní dále rozvineme. Schéma databáze si můžeme představit jako graf, kde tabulky tvoří vrcholy a cizí klíče, jakožto vztahy relační databáze, hrany grafu. Cizím klíčem rozumíme integritní omezení definované v dokumentaci jako FOREIGN KEY constraint. Cizí klíč zajišťuje datovou konzistenci mezi dvěma tabulkami a, BÚNO, mezi dvěma sloupci. Cizí klíč je definován vůči tabulce (tzv. rodičovská tabulka, parent table) a jejímu sloupci (rodičkovský sloupec, parent column) a odkazuje se na referenční tabulku (referenced table) a její sloupec (referenced column). Platí, že hodnota rodičovského sloupce, pokud je definována, musí být obsažena v množině hodnot řádků referenčního sloupce. Čili cizí klíče můžeme uvažovat spíše jako orientované hrany našeho virtuálního grafu. Proč je pro nás tato reprezentace tak důležitá, navzdory tomu, že naše práce není primárně zaměřena na teorii grafů? Odpověď nalezneme, když si odpovíme na jednu velice triviální, avšak klíčovou otázku: Nechť máme databázi s tabulkami A, B, C a D a pro každou chceme generovat data. V jakém pořadí musíme jednotlivé tabulky, resp. sloupce plnit daty, aby nebylo porušeno pravidlo o datové integritě v cizím klíči? Tato otázka se dá přirozeně přeformulovat na problém množiny akcí, které na sobě závisí a záleží na pořadí jejich vykonávání v čase. Standardním ešením tohoto problému je tzv. topologické uspořádání, což je i známý grafový pojem. Odbočka do teorie grafů Máme orientovaný graf G s N vrcholy a chceme očíslovat vrcholy čísly 1 až N tak, aby všechny hrany vedly z vrcholu s větším číslem do vrcholu s menším číslem, tedy, aby pro každou hranu e = (vi , vj ) bylo i < j. Představme si to jako srovnání vrcholů grafu na přímku tak, aby šipky vedly pouze zprava doleva[2]. Pokud se tabulka A odkazuje na tabulku B (existuje cizí klíč na tabulce A, který referencuje sloupec tabulky B), tabulka B musí být v topologickém uspořádání před tabulkou A. 27
Pozorování 1. Pro grafy obsahující orientovaný cyklus není možné definovat topologické uspořádání Důkaz. Nechť vrcholy u a v náleží do orientovaného cyklu grafu G a e = (u, v) ∈ E(G). Z definice uspořádání platí, že index(u) < index(v). Protože existuje orientovaná cesta z v do u, tak i index(u) > index(v), což je spor.
Obrázek 6.2: Příklad topologického uspořádání grafu Tento problém jsme důkladně zvážili a rozhodli jsme se ho řešit tak, že orientované cykly z grafu odebereme a nebudeme je dále uvažovat. Otázkou zůstává, nakolik je tato situace v produkčním prostředí reálná. Existují databáze, ve kterých se cyklické závislosti ve strukturách objevují, nicméně z pohledu datového modelu zastáváme ten názor, že orientovaným cyklům je lepší se vyhnout již při návrhu databáze. Speciálním případem jsou orientované cykly délky 1. V řeči databázových struktur se jedná o tabulku, která referencuje sama sebe. Tento případ i dále uvažujeme a nebrání nijak globálnímu topologickému uspořádání, o které nám jde. (Bylo by možné upravit i formální definici tak, že každá tabulka má unikátní index a nerovnost mezi sousedními indexy je neostrá). Takovéto struktury se v praxi využívají pro modelování vztahů rodič-potomek nebo jiné stromové struktury.
6.1.2
OR-mapping
Zmínili jsme také OR-mapping, tj. mapování objektů na databázové struktury. V této fázi popíšeme, jak jsme v tomto ohledu postupovali. Pro nás jsou pod28
statné tyto databázové struktury a prvky: tabulka, sloupec, cizí klíč, typ (obšírné informace o strukturách je možné najít např. v knize SQL Server Internals [5]). Pro každý z uvedených pojmů jsme implementovali speciální třídu, která nese potřebné informace. Tyto třídy se nacházejí v souboru SchemaClasses.cs a nyní ve stručnosti představíme jejich obsah (prvky, které jsou v tuto chvíli nepodstatné prozatím vynecháme).
Tabulka 6.1: Schéma OR mapování tabulky Pole
Použití objektová reprezentace databázové ta-
class Table
bulky název schématu, do kterého tabulka
string schema
patří string name
jméno tabulky
int id
id tabulky jako databázového objektu získané z pohledu sys.tables
Dictionaryhint,Columni
co- kolekce dvojic id,sloupec obsahující
lumns
všechny sloupce, které jsou příslušné tabulce definovány
ListhForeignKeyi foreignKeys
seznam cizích klíčů definovaných na tabulce
ListhForeignKeyi reverseKeys
seznam cizích klíčů, které nejsou definovány na této tabulce, ale odkazují se na ní. Jedná se de facto o opačně orientované hrany.
ListhForeignKeyi selfKeys
seznam cizích klíčů, které referencují zpátky tutéž tabulku
int refCount
čítač referencí na tabulku
bool visited
příznak, zda-li byla navštívena v průchodu grafem Pokračování na další stránce
29
Tabulka 6.1 – pokračování tabulky Pole
Použití pole používaná k detekci cyklů v grafu
int sccLink
schématu ListhColumni PrimaryKey
seznam sloupců tvořících primární klíč tabulky
string PKname
databázový název primárního klíče
Listhstringi triggers
seznam názvů triggerů definovaných na tabulce příznak, jestli je povoleno generování
bool enabled
pro tuto tabulku bool generateForThisTable
označení, jestli se má generovat pro tuto tabulku
int topoIndex
index v topologickém uspořádání
bool wipeData
příznak, mají-li se vymazat řádky tabulky před generováním
int rowsToGenerate
počet řádků, které se mají vygenerovat
TableItem displayItem
vazba na objekt, který reprezentuje tabulku v UI
Tabulka 6.2: Schéma OR mapování sloupce Pole
Použití
class Column
třída reprezentující sloupec
string name
jméno sloupce
int id
id sloupce v kontextu tabulky
Type t
datový typ sloupce
int length
maximální přípustná délka obsahu sloupce Pokračování na další stránce
30
Tabulka 6.2 – pokračování tabulky Pole bool isnull
Použití příznak, může-li obsahovat řádky hodnoty NULL
bool isrowguicol
příznak,
je-li
sloupec
typu
ROWGUICOL 17 bool isidentity
příznak, jestli se používají automatické zvyžující se hodnoty
bool iscomputed
příznak, jsou-li data v řádcích automaticky počítána
bool isPrimaryKey
příznak, je-li součástí primárního klíče
bool isunique
příznak, může-li obsahovat pouze unikátní hodnoty
int precision
matematická přesnost sloupce
int scale
počet cifer, které mohou být za desetinnou čárkou
ForeignKey foreignKey
odkaz na cizí klíč, který je definován na tomto sloupci
Table table
odkaz na tabulku, pro kterou je tento sloupec definován
bool generateForThisColumn
příznak, má-li se generovat pro tento sloupec
System.Type generationType
typ platformy .NET, který odpovídá typu sloupce v databázi
iDataGenerator generator
generátor, který bude generovat požadovaná data
ColumnControl displayItem
reference na prvek UI
17
Identifier columns, URL http://technet.microsoft.com/en-us/library/ms191131% 28v=SQL.100%29.aspx [citováno 22. května 2013]
31
Tabulka 6.3: Schéma OR mapování cizího klíče Pole
Použití
class ForeignKey
třída reprezentující cizí klíč
Column parent
rodičovský sloupec cizího klíče
Column target
referenční sloupec cizího klíče
string name
jméno cizího klíče definované v databázi
Obrázek 6.3: Struktura tříd pro reprezentaci datových typů
32
Tabulka 6.4: Schéma OR mapování datového typu Pole
Použití
class Type
třída reprezentující databázový typ
string name
jméno typu z databáze
int id
dentifikátor typu z databáze
int maxLenght
maximální délka datového typu podle specifikace
Třída Type je abstraktní a pouze zastřešuje další specifické typy. Ty jsme se rozhodli sdružit do několika kategorií podle podobných požadavků na generování. Uvádíme také seznam databázových typů, které RDG podporuje:
Tabulka 6.5: Podporované datové typy Typ bigint
bit
date
datetime
datetime2
datetimeoffset
decimal
float
char
int
money
nchar
numeric
nvarchar
real
smalldatetime
smallint
smallmoney
time
varchar
tinyint
uniqueidentifier
Tyto typy jsou nejdůležitější a také nejčastěji používané. Ostatní typy se také využívají, nicméně jejich význam pro optimalizaci dotazování jsme nevyhodnotili jako zásadní.
6.1.3
Sestavení schématu databáze
Sestavení schématu databáze probíhá v několika krocích: 33
1. ze systémového pohledu sys.tables a sys.columns získáme seznam tabulek a jejich sloupců 2. z pohledů sys.foreign keys a sys.foreign key columns vybereme seznam cizích klíčů, pomocí kterých propojíme sloupce, zároveň také detekujeme cizí klíče referencující rodičovskou tabulku 3. dále se dotážeme na další údaje (např. primární klíče a triggery) 4. v získaném grafu najdeme orientované cykly - silně souvislé komponenty a odstraníme je (oddělíme do nového grafu) 5. nově vzniklý acyklický graf topologicky uspořádáme
6.1.4
Grafické prvky
Grafová struktura slouží pouze pro reprezentaci v paměti. Nad tímto grafem je postavena odpovídající vrstva uživatelského rozhraní, která umožňuje zobrazení v okně a uživatelskou interakci. Jedná se o třídy ColumnControl, TableItem a MyTreeView. Všechny tyto třídy jsou potomkem třídy UserControl, čímž získávají některé schopnosti potřebné pro vykreslení v grafickém rozhraní. Ikony U některých komponent jsme pro názornost použili jednoduché ikony. Jejich zdrojem je server ICONFINDER18 . Konkrétními autory použitých ikon jsou designéři z Momentum19 a Fatcow20 . Ikony jsou dostupné s licencí Creative Commons 21 . ColumnControl Tato třída je ovladačem pro jednu instanci třídy Column reprezentující sloupec tabulky. Kromě názorné ikonky obsahuje hlavně název sloupce a informace o příslušném datovém typu. Dalším oznamovacím prvkem je pak sada ikon, které se mohou zobrazovat v pravé části. Jejich účelem je upozornit uživatele na další 18
Iconfinder.com, URL http://www.iconfinder.com/ [citováno 22. května 2013] Momentum design lab, URL http://momentumdesignlab.com/ [citováno 22. května 2013] 20 Fatcow, URL http://www.fatcow.com/ [citováno 22. května 2013] 21 creative commons, URL http://creativecommons.org/licenses/by/3.0/us/ [citováno 22. května 2013] 19
34
informace. Např. že na sloupci je definován primární či cizí klíč. Důležitým prvkem je z pohledu uživatele zaškrtávací políčko vedle ikonky sloupce. Tím uživatel určuje, má-li se pro tento konkrétní sloupec generovat. V některých případech toto není možné uživatelsky měnit, neboť se jedná o stav vynucený kontextem databáze. Taková situace nastane na sloupcích definovaných jako IDENTITY (pro ty nelze generovat) nebo NOT NULL. (generování je vynuceno). TableItem Instance této třídy odpovídají jednotlivým tabulkám zvolené databáze. Jedná se o poněkud komplexnější objekt, než je tomu v případě sloupce. Ze zobrazovacích prvků je nejdůležitější textové pole s názvem tabulky. Prvkem této grafické komponenty je dále seznam zobrazení sloupců prostřednictvím objektů třídy ColumnControl. Pozadí může měnit barvy, což využíváme jako jednoduchý indikační prvek. Vedle názvu tabulky se nachází přepínací tlačítko pro zobrazení/skrytí sloupců příslušné tabulky. To umožňuje přehledné minimální zobrazení, kdy jsou viditelné pouze tabulky. Mezi tímto tlačítkem a ikonkou, která symbolizuje tabulku, je zaškrtávací políčko, které plní stejnou funkci, jako u sloupců - totiž umožňuje uživateli výběr tabulek pro generování. MyTreeView Všechny tabulky jsou pak sdruženy v komponentě MyTreeView. Tento prvek jsme se rozhodli implementovat proto, jelikož nám nevyhovoval žádný z dostupných ovládacích prvků. Původně jsme zvažovali použití standardního TreeView, avšak kvůli problémům s přizpůsobením našemu použití, kde by bylo nutno upravit celou velmi netriviální šablonu, jsme se rozhodli pro vlastní řešení. To je principiálně založeno na kontejneru typu StackPanel. Ten umožňuje jednoduché sekvenční přidávání prvků v daném směru, což se ukázalo jako dostačující, když jsme část logiky přesunuli do prvku TableItem. Použití StackPanelu s sebou nese určitá výkonnostní omezení, zvláště pokud bude obsahovat velké množství položek. V praxi však nepředpokládáme použití s natolik rozsáhlou databází, aby tím byla limitována použitelnost uživatelského prostředí. Při vývoji jsme tuto komponentu testovali s pomocí rozsáhlé produkční databáze a naše předpoklady se tak
35
experimentálně potvrdily. Objekt třídy MyTreeView také definuje jakýsi globální kontext pro ovladače tabulek a sloupců, které obsahuje. Na spodním okraji této části okna jsou pak dva přepínače, kterými lze ovládat pořadí tabulek v seznamu (objektu MyTreeView). Jedná se o zobrazení podle topologického uspořádání, které je výchozí a zobrazení v abecedním pořadí podle jmen tabulek. Pro samotné nastavování však doporučujeme topologické uspořádání, které přímo určuje i pořadí generování. Akce na stisk tlačítka myši Zámerně jsme při popisu jednotlivých ovládacích prvků tabulek a sloupců nezmínili možnosti té nejpřirozenější uživatelské interakce, totiž kliknutí kurzorem, které se budeme věnovat v tuto chvíli. Činíme tak proto, jelikož se tím dotkneme i další části hlavního okna, které se budeme věnovat vzápětí. Začneme opět zobrazením sloupců. Kliknutím na objekt ColumnControl může dojít de facto ke dvěma akcím: pokud není sloupec povolený zaškrtávacím políčkem definovaným dříve, kliknutí se propaguje dále k nadřazenému objektu - tabulce. V opačném případě je sloupec považován za označený, což se primárně projeví změnou zobrazeného pozadí. Další provedenou akcí je přizpůsobení nastavovací oblasti a náhledu dat hlavního okna podle definice sloupce. Při kliknutí kurzorem na objekt TableItem nastávají obdobné situace. Pokud není tabulka povolena pomocí příslušného políčka, tak se kliknutí nijak neprojeví. Akce se provede pouze v opačném případě. V tom je nyní tabulka také považována za označenou a zvýrazněna odlišným pozadím. Zároveň může dojít k odlišnému zvýraznění i u jiných tabulek. V krátkosti vysvětlíme, proč jsme se rozhodli tuto funkcionalitu implementovat. Zvýrazněné tabulky jsou právě ty, které jsou potenciálně potřebné kvůli datové integritě vynucené pomocí cizích klíčů. Uživatel tak dostává nápovědu, že i pro tyto tabulky by měl povolit generování. Další možností kontroly je, jak už jsme zmínili, že zvýrazněné tabulky se nacházejí v topologickém uspořádání před tabulkou, kterou právě označil. To však nutně neznamená, že jsou potřeba všechny z těchto tabulek. Nahlédnout tak můžeme opět jednoduchou úvahou z teorie grafů.
36
Odbočka do teorie grafů Topologicky uspořádatelný graf jsme definovali jako acyklický. Souvislé grafy bez cyklů se označují jako stromy, nesouvislý graf bez kružnic pak jako les. Aby nedošlo k nedorozumění, není tím myšlen zakořeněný strom. Jelikož pracujeme s orientovaným grafem, definujeme následující pojmy: Definice 1. List je vrchol, který nemá žádné vstupní hrany. Definice 2. Kořen je vrchol, který nemá žádné výstupní hrany. Předpokládejme, že graf schématu databáze je strom. Jednotlivé vrcholy můžeme sdružovat do skupin, kterým říkejme třídy. Definice 3. Dva vrcholy náleží do stejné třídy, tj class(a) = class(b) tehdy a jen tehdy, pokud mají stejnou maximální délku cesty z listu. Pozorování 2. 1. Mezi dvěma vrcholy téže třídy nevede hrana. 2. Všechny listy patří do jedné třídy. 3. Hrany vedou pouze z tříd s nižším číslem do tříd s vyšším. Důkaz. 1. Z definice platí, že maximální délka cesty je stejná. Pokud by vedla hrana e(u, v), pak je nutně class(u) < class(v), tedy spor s definicí. 2. Plyne přímo z definice - mají stejnou délku cesty 0. 3. Pokud by vedla hrana z třídy s vyšším číslem V do třídy s nižším číslem N , byla by součástí delší cesty, než je délka nejdelší cesty do N . Protože ta je nejdelší, nemůže taková hrana existovat.
Tyto jedoduché důsledky nyní využijeme. Pokud označíme tabulku A, která referencuje tabulku B, tak třída B je nutně nižší než třída A. Nechť do třídy B patří i tabulka C. Pak v topoogickém uspořádání je C před A, ale z našeho pohledu není A datově závislá na C a tak není nezbytně nutné generovat data i pro tuto tabulku. 37
6.2
Panel nastavení
Další komponentou, kterou se v tuto chvíli budeme zabývat, je ta část okna, kterou jsme nazvali panel nastavení. Jeho účelem je zpřístupnit uživateli potřebné možnosti konfigurace objektu, se kterým uživatel právě pracuje. Prvky v této oblasti můžeme rozdělit do dvou kategorií: 1. informace a nastavení tabulky 2. nastavení generátoru sloupce
6.2.1
Informace a nastavení tabulky
Tato konfigurace je zobrazena, pokud uživatel označil ve výběru objekt tabulky. Protože je náš generátor orientovaný na sloupce, nedává velký smysl zobrazovat zde nastavení pro generátory, které odpovídají více sloupcům. Je zde prostor pro ta nastavení, která přísluší tabulce z její databázové definice nebo jsou společná pro všechny sloupce. Kromě možností konfigurace jsou zde i čistě informativní prvky. Tím je myšlena oblast, ve které se zobrazují některé další informace o vybrané tabulce. Konkrétně jde o: 1. cizí klíče 2. triggery Uvedená data slouží pro připomenutí uživateli, že na tabulce jsou definované určité objekty, které mohou mít zásadní vliv na generování dat. Ve spodní části panelu se nachází dvě zaškrtávací políčka. První z nich slouží pro volbu vymazání stávajících dat z tabulky před začátkem generování. Druhé pole dává možnost zobrazit referenční závislosti na zvolené tabulce. Jedná se o obdobnou funkci zvýraznění tabulek, jako jsme popsali u výběru tabulky, avšak místo tabulek, na kterých právě označená závisí jsou podbarveny ty, které na zvolené tabulce závisí. To úzce souvisí s předchozím polem. Pokud bychom chtěli vymazat všechna data z této tabulky a exsitovala by nějaká referenčně závislá data v jiné tabulce, mohlo by dojít k chybě a data by nebyla vymazána. Poslední, ale podstatnou položkou nastavení je počet řádků, který se má pro danou tabulku vygenerovat. 38
6.2.2
Implementace
Podíváme se nyní trochu podrobněji, jak je zobrazení v panelu nastavení implementováno. Stejný postup je použit jak u již prozkoumaného nastavení tabulek, tak i pro všechna nastavení sloupců. Při řešení jsme se rozhodli využít jeden ze základních návrhových vzorů - singleton 22 . Singleton zaručuje, že za běhu programu bude existovat nejvýše jedna instance třídy, která dodržuje tento vzor. Typicky se využívá v případech, kdy je potřeba zajistit exkluzivní přístup, jako je například hardware, otevřený soubor nebo synchronizační primitiva [3]. Takové, relativně náročné požadavky na kontrolu přístupů v našem případě nenastaly, nicméně architektura aplikace umožňuje tento jistě velice elegantní návrhový vzor využít. Architekturou nyní myslíme fakt, že panel nastavení existuje v hlavním okně právě jeden a je v něm zobrazeno nejvýše jedno nastavení pro aktuálně vybraný objekt, který je také nejvýše jeden. Použití singletonu umožňilo tvorbu jednoduššího a přehlednějšího kódu, který dodržuje určitá jednotná pravidla při manipulaci s panelem nastavení. Navíc je toto řešení i paměťově úspornější. Pro každý objekt, který je možné konfigurovat v panelu nastavení, existuje zvláštní třída nesoucí informace o uživatelském rozhraní. Jediná instance třídy má zpřístupněnou metodu init, v jejímž kódu dojde k doplnění potřebných dat do uživatelského rozhraní a uložení reference na instanci, pro kterou provádíme nastavení. Ta je potřebná pro uložení pozměněného nastavení.
6.2.3
Nastavení generátoru sloupce
Nastavení jednotlivých generátorů se liší a tak se jim budeme věnovat až později v samostatné kapitole. Jednou společnou vlastností je pro některé z generátorů možnost generovat místo dat NULL hodnoty. Slouží k tomu speciální prvek NullAmountControl. Možné je zadat hodnotu v procentech přímo do textového pole, nebo přičítat a odčítat pomocí zobrazených tlačítek. 22
Singleton, URL http://msdn.microsoft.com/en-us/library/ff650849.aspx [citováno 22. května 2013]
39
6.3
Náhled na data
Předposlední oblastí hlavního okna, které jsme se ještě nevěnovali, je dolní pravá část, kde se nachází tabulka, která zobrazuje náhled na podobná data, jaká budou při generování vkládána do databáze. Existuje zde také vazba na právě zvolený prvek a to opět sloupec nebo celou tabulku. V případě zvolení sloupce se zobrazují pouze řádky právě tohoto jednoho sloupce. Pokud je označena tabulka, pak se její struktura přenese i na náhled, kde je možné zkontrolovat řádky všech sloupců. Šířku jednotlivých sloupců v náhledu je možné do jisté míry měnit. To závisí na aktuální šířce okna a délce názvu sloupce. V případě velkého množství sloupců a dlouhých řetězcových hodnot je zde užitečná možnost nastavení minimální šířky sloupce. Kliknutím na záhlaví sloupce se zobrazí jednoduchý dialog, kde je předvyplněná aktuální šířka sloupce, kterou je možné změnit a nastavit tak minimální šířku, která se nezmění ani případným zmenšením okna aplikace.
6.3.1
Zdroj náhledu na data
V tuto chvíli se krátce zmíníme o použití SQL Serveru Compact Edition, které jsme avizovali v úvodu. Rozhodli jsme se pro tuto implementaci z důvodu snížení paměťové náročnosti aplikace. Každý sloupec má definovanou datovou tabulku, která, pokud došlo k označení sloupce, obsahuje řádky náhledu na data. Při náhledu na data celé tabulky se spojují právě data, která poskytnou jednotlivé sloupce. S rostoucím počtem zobrazených tabulek a sloupců, kdy se nagenerují a uloží náhledová data však rostou nároky na paměť. Zobrazuje se 100 řádků, tedy každá taková tabulka potřebuje uchovávat stonásobek počtu sloupců záznamů. Abychom se tohoto vyvarovali, implementovali jsme jednoduchý a elegantní mechamizmus, který vychází z předpokladu, že jsou potřeba pouze náhledová data pro tabulku, se kterou uživatel v daný okamžik pracuje. Ostatní mohou být uloženy v sekundární paměti a budou načteny až v případě, že je uživatel bude chtít zobrazit. Jako úložiště jsme se rozhodli pro použití SQL Server CE databáze (dříve námi označené jako sekundární databáze), protože umožňuje prakticky stejný přístup, jako k plnohodnotné databázi SQL Serveru.
40
Pro tento koncept jsou nutné další informace o aktuálním stavu dat náhledu, které se dají vyjádřit jednoduchým stavovým diagramem.
Obrázek 6.4: Diagram práce s pamětí u náhledových dat K ukládání náhledových dat do persistentního úložiště dochází jen tehdy, pokud je to nutné. Tím omezíme zbytečnou režii, která by nastala, kdybychom data v sekundární databázi aktualizovali pokaždé. Aby nedocházelo ke konfliktům nebo čtení zastaralých dat, při každém spuštění aplikace vymažeme všechny tabulky z této sekundární databáze. Kromě standardních dat náhledu existují ještě speciální hodnoty a to NULL tabulka, která obsahuje pouze řádky “NULL” a používá se pro sloupce, které nejsou vybrány pro generování. Dále IDENTITY tabulka obsahující řádky “identity”, která reprezentuje sloupce, které jsou typu IDENTITY a o jejich data se stará samotná databáze. Poslední speciální hodnotou je řádek “NO DATA”, jehož zobrazení značí, že referencovaná tabulka neobsahuje žádná data (a tedy není možné zobrazit je v náhledu).
41
6.4
Menu
V hlavním okně zbývá probrat pouze poslední část a to menu, které se nachází v pravé horní části. Struktura nabídky menu je následující: • Soubor – Otevřít projekt – Uložit projekt – Konec • Možnosti – Ukázat/skrýt tabulky – Zobrazit DELETE-skript – Preference
6.4.1
Soubor
V první položce menu se kromě tlačítka pro ukončení aplikace nacházejí možnosti pro uložení a načtení konfigurace. Uložení a načtení konfigurace Konfigurací myslíme přiřazení a nastavení některých generátorů k definovaným sloupcům a nastavení tabulek. Nastavení se obnovují pro odpovídající tabulky a sloupce. Pro tabulky je vyžadováno pouze stejné schéma a název. U sloupců navíc datový typ a integritní omezení. Mezi generátory, jejichž konfigurace se nedá načíst, patří generátor z cizího klíče (ten žádná nastavení nemá) a generátor z uživatelského CSV a to z bezpečnostních důvodů. Uloženou konfiguraci je možné načíst na jiném počítači, kde nebude cesta k danému souboru platná a předejdeme tak chybám vzniklým neexistení souboru a případným, ještě závažnějším chybám, načtení nesprávného souboru. Ukládání a načítání je realizováno pomocí serializace do formátu XML, které je strukturováno následujícím způsobem23 : 23
pro jednoduchost neuvádíme v DTD nebo XML Schema
42
1 2 3
4
5
6
7
8 9 Každý generátor implementuje metody, které umožňují získat jeho konfigurační data a pomocí nich zpět zrekonstruovat jeho nastavení. Pokud není nalezen odpovídající sloupec nebo tabulka, načítání pokračuje dále a nekončí chybou. To umožňuje načítat starší konfigurace na rozšířené databáze.
6.4.2
Možnosti
Ukázat/skrýt tabulky Protože může nastat situace, kdy uživatel bude chtít pracovat pouze s určitou databázovou komponentou, tedy pomnožinou tabulek, rozhodli jsme se implementovat funkci, která umožní zobrazovat nebo skrývat vybrané tabulky. Skrytí nepotřebných tabulek může zvláště v případě rozsáhlé databáze výrazně zjednodušit nastavení generování. Některé dříve uvedené programy také umožňují pracovat pouze s podmnožinou databáze, ale tuto je potřeba zvolit před samotným nastavením generování a není možné ji libovolně měnit. Jak je patrné ze vzhledu tlačítka v menu, jedná se o přepínač. Pomocí něho je možné aktivovat jakýsi editační mód, ve kterém se zobrazí nad ikonkou tabulky zaškrtávací políčka, která představují možnost zobrazení/skrytí tabulky. Přirozeně jsou v tomto módu zobrazeny všechny tabulky. Ty, které nejsou určeny pro zobrazení se deaktivací editačního módu skryjí. Pořád je nutno mít na paměti, že skrytím tabulek
43
přichází uživatel o možnost kontroly referenčních závislostí, o kterých jsme se zmínili dříve. Proto je nutné skrývat tabulky po pečlivém uvážení. Zobrazit DELETE-skript Po výběru této možnosti v menu se zobrazí okno obsahující TSQL skript pro vymazání všech dat všech tabulek z databáze. Tabulky jsou setřízeny v topologickém uspořádání tak, aby nedošlo ke konfliktům při kontrole referenční integrity. Po zobrazení se text skriptu vloží do systémové schránky. Možné je také využít nepříliš známou funkci systému Windows - pokud je aktivní okno se zprávou, tak stisknutím zkratky Ctrl+C se jeho textový obsah vloží do schránky. Preference Zobrazí okno Preference, které popíšeme později.
44
7. Generování Po provedení všech nezbytných nastavení se uživatel stisknutím příslušného tlačítka přesune do dalšího kroku workflow. Na pozadí probíhá generování dat společně s jejich odesíláním na server a o průběhu je uživatel informován právě v okně Generování. U každého kroku generování se vypíše čas začátku a čas dokončení. Pro znázornění stavu průběhu dané úlohy používáme komponenty ProgressBar. U mazání a standardního generování deterministický, při unikátním generování používáme nedeterministický. Pokud dojde v průběhu generování k chybě, vypíše se také tato informace.
Obrázek 7.1: Okno Generování Na dokončení celého generování upozorňuje vyskakovací okno se zprávou. Následně je možné okno Generování zavřít a vrátit se zpět k hlavnímu oknu.
45
8. Další 8.1
Preference
Jak jsme již dříve slíbili, vrátíme se nyní k oknu Preference. Jedná se o formulář, ve kterém lze nastavit údaje platné pro celou aplikaci. A to: • Timeout pro unikátní generování • Cesty k předdefinovaným datovým sadám Timeout pro unikátní generování přiblížíme v samostatné sekci věnované unikátnímu generování. Stejně tak cesty k předdefinovaným datovým sadám probereme v kapitole generátorů.
8.2
Preloader
V předchozích kapitolách jsme zmiňovali možnosti asynchronního zpracování ve spojitosti s přátelštějším rozhraním. Proto jsme implementovali preloader - tedy grafickou komponentu, která zobrazuje uživateli nějaké informace, zatímco důležité operace probíhají na pozadí. Z pohledu uživatele i operačního systému se jedná o užitečnou věc. Pokud bychom zpracovávali déle trvající operace v hlavním vlákně, tedy vlákně, které se stará i o uživatelské rozhraní (UI vlákno), operační systém by označil aplikaci jako “Neodpovídá”, což by mohlo vést k jejímu předčasnému ukončení ze strany uživatele. Aplikace neodpovídá Každá aplikace v systému Windows, která má grafické uživatelské rozhraní, disponuje frontou pro zaznamenávání uživatelských interakcí. Tedy kliknutí na tlačítko nebo označení textu se zařadí do fronty operací, odkud jsou někdy v budoucnu (ideálně ihned) vytaženy a zpracovány. Pokud ale nastane právě ta situace, kdy je vlákno zaneprázdněno a nemůže zpracovávat požadavky z této fronty, operační systém ho označí jako “Neodpovídá”.
46
8.2.1
WorkingPreloader
Tato třída, která reprezentuje samostatné okno (je potomkem třídy Window), je právě naší reprezentací preloaderu. Jedná se o nedeterministický ProgressBar, který uživatele informuje o probíhající operaci. Vedle této základní varianty je dostupná ještě rozšířená verze, která pomocí tlačítka umožňuje probíhající operaci přerušit. Než se pustíme do zkoumání pár zajímavých implementačních detailů, je potřeba upozornit na fakt ohledně rozhraní v platformě .NET s použitím technologie WPF. Jak jsme uvedli, o uživatelské prostředí se stará UI vlákno. Platí však, že se o něj stará právě UI vlákno. Při pokusu o změnu uživatelského rozhraní z jiného vlákna, než UI vlákna, které prvek instanciovalo, dojde k běhové chybě. The calling thread cannot access this object because a different thread owns it. Standardním řešením je použití speciálního objektu, kterému se říká Dispatcher. Při předání požadavku na vykonání nějaké metody objektu Dispatcher vlastně vkládáme požadavky do fronty, o které jsme psali v odstavci výše. Uvažme jedenoduchou modelovou situaci: Máme formulář se seznamem textových položek. Chceme na pozadí zpracovat velké množství souborů a průběžně jejich jména přidávat do seznamu v okně: 1 v o i d p r o c e s s F i l e s ( o b j e c t sender , RoutedEventArgs e ) 2
{
3
Task . Factory . StartNew (
4
( ) => doWork ( Owner . D i s p a t c h e r , f i l e s L i s t )
5
);
6
}
7 8
pr i va t e void doWork ( System . Windows . Threading . D i s p a t c h e r dispatcher , string [ ] Files )
9
{
10
foreach ( string f i l e in F i l e s )
11
{ 47
12
processFile ( f i l e ) ;
13
d i s p a t c h e r . Invoke (
14
( ) =>
15
{
16
f i l e s L i s t . Items . Add( f i l e ) ;
17
}
18
); }
19 20
} Zajímavá je pro nás metoda doWork, která je spuštěna asynchronně meto-
dou processFiles, která jí předává kromě seznamu souborů referenci na objekt Dispatcher okna, kterému náleží. V kódu procedury doWork se pak projdou všechny soubory a zpracují nějakou metodou processFile. Po každém kroce chceme přidat jméno souboru do kolekce nějakého zobrazovacího kontejneru. Využijeme přitom opět lambda-funkci a v jejím těle napíšeme stejný kód, jako bychom psali, kdybychom postupovali standardně v těle metody doWork. Avšak protože lambdu předáváme jako parametr metody Invoke objektu Dispatcher, kód se k vykonání pošle vláknu, které vlastní okno, kterému Dispatcher patří. Díky tomu nedojde k běhové chybě, ale až dojde k vyzvednutí tohoto úkolu z fronty okna, aktualizuje se uživatelské rozhraní přesně tak, jak jsme chtěli. Během vývoje jsme však narazili na problém, kvůli kterému jsme museli postupovat odlišně. Jedná se především o spuštění a inicializaci hlavního okna. Pokud by uživatel vybral rozsáhlou databázi se stovkami tabulek, její zpracování může trvat potenciálně dlouho. Asynchronní zpracování bychom měli použít naprosto přirozeně, avšak při zpracování zároveň generujeme uživatelské rozhraní, tedy komponenty zobrazující tabulky a sloupce. Z tohoto důvodu nemůžeme použít přímo koncept použitý výše. Zopakujme si, naším cílem bylo zobrazit na popředí plynule animovaný ProgressBar a na pozadí zpracovat schéma databáze společně s vygenerováním rozhraní. Pokud bychom se chtěli striktně držet zmíněného konceptu, docházelo by z pohledu uživatele k “zamrzání ”. Důvodem v tomto případě je, že ProgressBar i generování UI probíhá v jednom vlákně. Fronta událostí
48
Obrázek 8.1: Schéma komunikace při použití WorkingPreloaderu je pak zahlcena požadavky na generování UI, což negativně ovlivňuje plynulost zobrazení ProgressBaru. Řešením je paralelní běh inicializace hlavního okna i ProgressBaru, tedy spuštění druhého UI vlákna. Právě tak jsme postupovali i my. Protože je zobrazen vždy nejvýše jeden preloader, použili jsme opět návrhový vzor singleton. O zobrazení okna WorkingPreloader se stará instance třídy PreloaderViewer. Ta při inicializaci vytvoří vlákno, které označí jako STAThread, což je potřebné pro UI vlákno a následně jej spustí. V metodě Run, která se provádí při startu nového vlákna, je získána instance Dispatcheru a metodou System.Windows.Threading.Dispatcher.Run(); je spuštěna smyčka pro zpracování událostí. PreloaderViewer je možné ovládat pomocí metod Show a Close, které zobrazí nebo zavřou okno WorkingPreloaderu. Požadavek na vytvoření okna je zpracován v no49
vě spuštěném vlákně, které je pak i jeho vlastníkem. Proto je nutné i požadavek na zavření předložit definovanému vláknu.
50
Část III Generátory
51
9. Generátor V předchozích částech jsme několikrát zmínili pojem “generátor”, aniž bychom ho dříve definovali. Z kontextu textu je však až zřejmé, o co se jedná. Pokud bychom se pokusili generátor definovat, mohli bychom formulovat definici například takto: Definice 4. Generátor je komponenta aplikace, která je schopna na příkaz vydat pseudonáhodná data. Tato velice obecná definice specifikuje naprosto fundamentální požadavek na generátor. Z hlediska implementace definujeme generátor odlišně: Definice 5. Generátor je objekt typu, který je potomkem abstraktní třídy iDataGenerator. Třída iDataGenerator vystavuje parametry, které jsou společné pro všechny typy generátorů: • vrácení pseudonáhodných dat - jedné hodnoty nebo dávky • počáteční seed vnitřního pseudonáhodného generátoru • nastavení množství NULL hodnot mezi generovanými daty • vrácení jména daného generátoru • vrácení konfiguračních dat • možnost načtení konfiguračních dat Pro získání základních pseudonáhodných čísel používáme standardní generátor System.Random.Random. Ten generuje pseudonáhodná čísla podle zadaného rozsahu v rozdělení, které se blíží rovnoměrnému. Takovýto generátor je pro naše účely dostačující Generátory dále dělíme na generické a předdefinované. Generický generátor generuje data z množiny (domény), kterou definuje uživatel nastavením generátoru. Naopak u předdefinovaných generátorů je doména určena datovou sadou, ze které generátor čerpá. 52
10. Generické generátory Generické generátory můžeme rozdělit podle datového typu, který vrací: • řetězce – náhodný řetězec – řetězec odpovídající regulárnímu výrazu • celé číslo • desetinné číslo • datum a čas • GUID • generátor z cizího klíče Nyní stručně, kromě generátoru z regulárních výrazů, kterému se budeme věnovat více, představíme uvedené generátory.
10.1
Náhodný řetězec
Tento generátor je základní pro generování pseudonáhodných řetězců. Jedinou možností, jak ovlivnit řetězce, které generuje, je délka. Řetězce se skládají ze znaků, jejichž kód se nachází mezi 30 a 90. Tyto naprosto náhodné řetězce lze využít například k simulaci uživatelských hesel v databázi nebo jiných textových údajů, kde nezáleží na vnitřní struktuře nebo doméně.
10.2
Řetězec odpovídající regulárnímu výrazu
V předminulém odstavci jsme avizovali, že se tomuto generátoru budeme věnovat více, než ostatním. Jistě právem, neboť tento je nejkomplexnější ze všech dostupných generátorů a umožňuje způsob, jak jednoduše strukturovat data pokročilejším způsobem. Pro úplnost a vyjasnění pojmů též lehce nahlédneme do
53
teorie automatů a formálních jazyků. Nejdříve si vymezme pojem regulárního výrazu (použijeme definici Prof. Bartáka [4]): Definice 6. Regulární výraz je prvek množiny všech regulárních výrazů RV (X) nad konečnou neprázdnou abecedou X = {x1 , x2 , ..., xn }. RV (X) je nejmenší množina slov v abecedě {x1 , x2 , ..., xn , ∅, λ, +, ., ∗, (, )}, která: • obsahuje výraz ∅ a výraz λ • pro každé písmeno x ∈ X obsahuje výraz x • α, β ∈ RV (X) ⇒ (α + β) ∈ RV (X) • α, β ∈ RV (X) ⇒ (α.β) ∈ RV (X) • α ∈ RV (X) ⇒ α∗ ∈ RV (X) Příkladem regulárního výrazu je tedy třeba: ((a+((b.c)+d)*)+e). Dále definujme hodnotu regulárního výrazu, která je z našeho pohledu velice důležitá (opět využijeme formulaci z materiálů Prof. Bartáka [4]): Definice 7. Hodnotou regulárního výrazu α ∈ RV (X) je množina slov [α] definovaná následovně: • [∅] = ∅, [λ] = {λ} , [x] = {x} • [(α + β)] = [α] ∪ [β] • [(α.β)] = [α] . [β] • [α∗] = [α] ∗ Právě hodnota regulárního výrazu (odpovídající regulárnímu jazyku) definuje doménu, ze které budeme generovat data pro databázi. Definování správného regulárního výrazu s potřebnou hodnotou umožňuje uživateli generovat data potřebně strukturovaná. V definici jsme použili tři základní operátory, kterým říkejme regulární operace. Jde o: • sjednocení + - regulární výraz a + b má hodnotu {a, b} 54
• zřetězení . - regulární výraz a.b.c má hodnotu {abc} • iterace ∗ - regulární výraz a∗ má hodnotu {λ, a, aa, aaa, ...} Důležitým poznatkem z teorie jazyků je fakt, že regulární jazyky jsou uzavřené na regulární operace [4]. Pokud tedy spojíme dva regulární výrazy pomocí regulárních operací, získáme opět korektní regulární výraz, jehož hodnotou je regulární jazyk. V opačném směru je tento důsledek užitečný při dekompozici složitějšího výrazu na více výrazů jednodušších. Použití takovýchto formálních regulárních výrazů je jistě možné, avšak kdybychom chtěli zapsat výraz odpovídající složitějšímu regulárnímu jazyku, než jaký jsme uvedli v příkladech, vedlo by to k dlouhému a nepřehlednému zápisu. Proto existují různé možnosti zápisu, které při stejné výrazové síle zjednodušují syntaxi výrazu. Jedním ze standardů, pomocí kterého lze zapisovat regulární výrazy např. v utilitách systémů UNIX je norma POSIX [11]. Tímto standardem jsme se inspirovali a regulární výrazy, říkejme jim proto regexpy, abychom je odlišili od formálních regulárních výrazů, zapisujeme níže definovaným způsobem.
10.2.1
Definice syntaxe regexpu
Než definujeme samotnou syntaxi zápisu regexpu, upozorníme na jednu skutečnost, která nemusí být na první pohled zřejmá. Regulární výrazy se nejčastěji používají k rozpoznávání požadovaných slov v textu. Definujeme množinu slov (jazyk), kterou chceme vyhledávat a definujeme regulární výraz, jehož hodnotou je tento jazyk. V našem případě však nejde o rozpoznávání slov v cizím textu, ale generování jednotlivých slov daného jazyka. Tento fakt umožňuje zjednodušení syntaxe, resp. vypuštění některých speciálních znaků, které by byly pro generování zbytečné. Regexp se skládá z libovolných znaků. Může obsahovat speciální řídící znaky. Takovými znaky jsou:.? + ∗\()[]|. Syntaxe a sémantika speciálních znaků je definovaná v následující tabulce:
55
Tabulka 10.1: Syntaxe a sémantika speciálních znaků v regexpu Pole
Použití zastupuje jakýkoli alfanumerický znak
.
Př.: ab.d = {abad, abcd, ab8d, abqd, ...} kvantifikátor označující 0 nebo 1 výskyt
?
Př.: ab?d = {ad, abd} kvantifikátor označující 1 a více výskytů
+
Př.: ab + d = {abd, abbd, abbbd, ...} kvantifikátor označující 0 a více výskytů
*
Př.: ab ∗ d = {ad, abd, abbd, abbbd, ...} závorky určují explicitně prioritu operací
()
Př.: ab?d+ = {abd, add, abddd, ...} Př.: (ab)?d+ = {abd, d, ddd, abdddd, ...} označuje množinu hodnot, je možné používat rozsahy nebo předdefinované sady. Rozsahem rozumíme dva znaky oddělené pomlčkou, kdy první je menší než druhý (znaky lexikograficky). Př.: a[a − z]+ = {ab, at, az, ...} Př.: a[a − z0 − 9] = {aa, a5, a9, ax, ...}
[]
Př.: a[a − z0 − 9]+ = {a2gw, arf pb5f 8, ...} Př.: třída [: alnum :]=[A − Za − z0 − 9] Př.: třída [: digit :]=[0 − 9] Př.: třída [: alpha :]=[A − Za − z] Př.: třída [: lower :]=[a − z] Př.: třída [: upper :]=[A − Z] Př.: [abcd] = {a, b, c, d} Př.: [x − a] = {x, −, a} označuje
{}
počet
opakování
daného
prvku,
buď
jako
{minimální počet, maximální počet} nebo {počet} Př.: a[: digit :] {1, 3} = {a4, a935, a85, a2, ...} Pokračování na další stránce
56
Tabulka 10.1 – pokračování tabulky Syntaxe
Význam Př.: a. {2} = {atr, a9d, ar0, ...} mění znaky se speciálním významem na znaky
\
Př.: te\.ka = {te.ka} zvolí libovolně jeden ze dvou prvků
Priorita operátorů je definována v tomto pořadí (vzestupně): 1. výběr (selekce) 2. zřetězení (sekvence) 3. kvantifikátory, rozsahy, počty 4. závorky kde závorky mají nejvyšší prioritu. Jak je patrné z příkladů, operátor zřetězení není explicitně zapisován, ačkoli ve formální definici jsme jej uvedli. Tímto jsme chtěli opět významně zjednodušit zápis regexpu, protože jinak by bylo nutné mezi každé dva znaky psát i operátor zřetězení.
10.2.2
Implementace
První otázka, kterou jsme při implementaci tohoto generátoru museli zodpovědět, byla volba algoritmu pro převod textového řetězce zadaného výrazu do datové struktury - parser. Rozhodli jsme se pro použití modifikovaného algoritmu Shunting-yard[12], který se využívá především pro parsování matematických výrazů v infixové notaci. Modifikace spočívá hlavně ve zohlednění některých řídících znaků, které jsou v postfixové notaci. Princip algoritmu, tedy použití zásobníku a fronty však zůstal zachován. Algoritmus lze rozdělit do dvou kroků: 57
Obrázek 10.1: Struktura tříd pro reprezentaci regepxu 1. převod výrazu do postfixové notace 2. převod postfixové reprezentace na stromovou reprezentaci Datový model Než se dostaneme k popisu pseudokódu algoritmu, zmíníme se o použitých datových strukturách. Rozhodli jsme se pro plné využití dědičnosti objektově orientovaného jazyku C# . Datový model, který má logicky stromovou strukturu (C# neumožňuje vícenásobnou dědičnost), bude čitelnější z diagramu. Jednotlivým objektům reprezentujícím prvky regexpu budeme říkat tokeny. Uveďme zjednodušený pseudokód algoritmu Shunting-yard nazvaný jako Parser:
58
Algoritmus 1 Parser Require: regex R,výstupní fronta, zásobník operátorů pozice := 0 while pozice < délka(R) do AktuálníToken ← NačtiToken() if AktuálníToken je konstanta then přidej AktuálníToken do výstupní fronty end if if AktuálníToken je operátor then VypišZásobník() end if if AktuálníToken je levá závorka then vlož ji na zásobník end if if AktuálníToken je pravá závorka then while na vrcholu zásobníku není levá závorka do odeber token ze zásobníku a vlož jej do fronty end while end if end while
Algoritmus 2 Procedura NačtiToken Require: pozice,regex,výstupní fronta, zásobník operátorů AktuálníZnak = regex[pozice] vytvoř instanci tokenu typu podle AktuálníhoZnaku if AktuálníZnak ∈ (, ., [, \ then PřidejTokenSekvence end if return AktuálníToken V proceduře NačtiToken využíváme funkci PřidejTokenSekvence - její pseudokód pro jednoduchost neuvádíme, jednalo by se totiž o poněkud nepřehlednou strukturu více if-else konstrukcí. Na naší úrovni abstrakce nám stačí zmínit, že tato procedura umožňuje zkrácený zápis bez nutnosti použití znaků sekvence, neboť tyto znaky, potřebné pro korektní zpracování, generuje sama z daného vstupu. Algoritmus 3 Procedura VypišZásobník Require: AktuálníToken,výstupní fronta, zásobník operátorů while zásobník není prázdný ∧ na vrcholu zásobníku není závorka ∧ operátor na vrcholu zásobníku má vyšší prioritu než AktuálníToken do odeber token ze zásobníku a vlož jej do fronty end while
59
Pomocí tohoto algoritmu získáme regexp v postfixové notaci uložený ve výstupní frontě. Algoritmus převodu výrazu v postfixu na stromovou reprezentaci uvádět nebudeme, jde o jednoduchý průchod stromem do hloubky pomocí zásobníku. V mnoha případech bychom dále postupovali převodem na konečný automat, který bychom použili pro rozpoznávání nebo generování slov. Ukazuje se však, že stromová reprezentace plně dostačuje pro generování. Pozorování 3. Pro generování slov z regulárního jazyka dostačuje stromová reprezentace regexpu Důkaz. Nechť máme regexp ve stromové reprezentaci. Z předchozího textu již předpokládáme uzavřenost jazyka a formálních regulárních výrazů na regulární operace. Stejná pravidla platí i pro regexpy, jsou tedy s formálními regulárními výrazy sémanticky ekvivalentní. Každý token, který je operátorem, představuje operátor nějaké regulární operace. Každý z operátorů regexpu je pouze syntaktickou zkratkou za operátory regulárních operací formálního regulárního výrazu. Tabulka 10.2: Regulární operace a regexpy Operátor regexpu
Zkratka pro
a?
λ∪a
a+
a∗
a∗
λ ∪ a∗
a sekvence b
a.b
a|b
a∪b
a {1, 3}
a ∪ aa ∪ aaa
Připomeňme ještě fakt, že i jeden znak abecedy je korektní regulární výraz. Můžeme tak definovat regulární výraz i induktivně:
60
• Každý znak abecedy je regulární výraz • Regulární operace nad regulárním výrazem (regulárními výrazy) dává regulární výraz Když prohlásíme listy stromové reprezentace regexpu za znaky, vnitřní uzly za regulární operace o nichž víme, že jsou jim ekvivalentní, získáváme tak zpět korektní regulární výraz, jehož hodnotou je regulární jazyk. Nyní opustíme formální regulární výrazy a podíváme se, jak z praktického hlediska generování slov probíhá. Definice 8. Ohodnocením regexpu budeme rozumět jedno slovo z jazkya odpovídajícího hodnotě příslušného regexpu. Jazyk je tedy množina všech ohodnocení daného regexpu. Jednotlivá ohodnocení se od sebe liší různým ohodnocením tokenů ve stromě regexpu. Získáváme tak další přirozeně rekurzivní algoritmus, pomocí kterého probíhá již samotné generování. Společný předek všech tokenů definuje funkci, která vrací nějaké ohodnocení podstromu, jehož je daný token kořenem. Některé tokeny jsou deterministické (např. nulární token jednoho znaku), jiné zase přirozeně nedeterministické (např. token | rozhodnutí mezi dvěma možnostmi). Pokud zavoláme funkci na ohodnocení kořene celého stromu regexpu, dostaneme právě ohodnocení celého regexpu získané rekurzivními dotazy na ohodnocení potomků. Omezení Typický regulární jazyk je nekonečný. Tedy stačí v regexpu použít alespoň jednou znaky + nebo ∗ a dostáváme tak potenciálně nekonečné množství různých konečných i nekonečných slov. Tento teoretický fakt není pro nás nějakým přínosem, spíše nebezpečím. Datový typ, do kterého budeme vygenerovaný řetězec ukládat má pouze omezenou délku a nechtěli bychom jistě vkládat nekonečný řetězec do datového typu SQL Serveru VARCHAR(20). Z tohoto důvodu dáváme možnost uživateli definovat “sílu” těchto potenciálně nekonečných tokenů. Sílou zde rozumíme maximální počet opakování. Dáváme k dispozici i horní odhad této
61
hodnoty, nicméně jde opravdu pouze o hrubý odhad, který pro složitější výrazy nemusí být funkční a není v rozsahu naší aplikace a tohoto textu pokročilá analýza zadaného výrazu.
10.3
Celé číslo
Celočíselný generátor umožňuje generovat celá čísla ze zadaného intervalu. Tento jeden generátor je stejný pro všechny celočíselné datové typy, liší se pouze rozsahem přípustných hodnot.
Tabulka 10.3: Celočíselné datové typy Typ
minimální hodnota
maximální hodnota
tinyint
0
255
smallint
-32 768
32 767
int
-2 147 483 648
2 147 483 647
bigint
−263
263 − 1
Pro všechna čísla, tedy hlavně pro typ BIGINT, je generování omezeno velikostí 32b. Možné je též generovat sekvenční hodnoty, kdy se jako počáteční hodnota bere nastavené minimum a následující hodnota je o 1 větší než předchozí.
10.4
Desetinné číslo
Stejné možnosti nastavení jako celočíselný generátor má i generátor desetinných čísel. Dalšími atributy, které ovšem uživatel nenastavuje v aplikaci, jsou precision a scale (nebudeme překládat). Podívejme se, jak tyto pojmy definuje oficiální dokumentace. Definice 9. Precision - the maximum total number of decimal digits that can be stored, both to the left and to the right of the decimal point. 62
Definice 10. Scale - the maximum number of decimal digits that can be stored to the right of the decimal point. Scale must be a value from 0 through precision.24 Jednoduše řečeno, precision udává, kolik cifer může maximálně dané číslo mít. A abychom striktně dodrželi definici, scale je maximální počet cifer, které mohou být vpravo od desetinné čárky. Scale může nabývat hodnot od 0 do hodnoty precision. V tomto bodě upozorníme na nepřesnost v definici, která by vedla k programovým chybám, pokud bychom se jí drželi doslovně. Mějme například proměnnou typu DECIMAL(p=10,s=3). To tedy dle definice znamená, že může uchovávat číslo o deseti cifrách z nichž až tři můžou být vpravo za desetinnou čárkou. 1
>> A r i t h m e t i c o v e r f l o w e r r o r c o n v e r t i n g v a r c h a r t o data type numeric . Vidíme, že došlo k běhové chybě, ačkoli jsme dodrželi definici. Proto uvedeme
definici, kterou jsme formulovali na základě pozorování. Pozorování 4. Hodnota precision udává maximální počet cifer, který může číslo mít. Hodnota scale říká, kolik cifer z precision je rezervovaných pro použití vpravo za desetinnou čárkou. Vlevo může být maximálně precision-scale cifer. Této naší definice se držíme při generování desetinných čísel, aby nedocházelo právě k chybě aritmetického přetečení.
10.5
Datum a čas
Z pohledu implementace je tento generátor velice podobný generátoru čísel. Opět je možné nastavit minimální a maximální hodnotu, případně generovat hodnoty sekvenčně, kdy se hodnoty zvyšují o jeden den. Generátor pracuje ve dvou módech generování: 24 Precision, scale, URL http://msdn.microsoft.com/en-US/library/ms187746%28v=sql. 105%29.aspx [citováno 22. května 2013]
63
1. pro typy date,datetime,datetime2,datetimeoffset,smalldatetime 2. pro typ time Rozdílné chování je způsobeno odlišnostmi v přípustných hodnotách uvedených typů. Při inicializaci generátoru je z uvedeného parametru rozhodnuto, v jakém režimu bude generátor pracovat. Možnost měnit chování jsme implementovali použitím delegáta v metodě getData, což umožnilo jednoduchou a elegantní konstrukci. Při inicializaci se tak delegátu přiřadí potřebná metoda, která se pak již volá automaticky bez nutnosti pro každý požadavek na data vyhodnocovat hodnotu příznaku.
10.6
Generátor z cizího klíče
Pro udržení datové integrity je tento generátor velmi důležitý. Nemá žádné možnosti nastavení, neboť jeho doména je přesně určena integritním omezením. Generátor cizího klíče má vlastní vnitřní pole, ve kterém udržuje podmnožinu dat referenčního sloupce. Toto pole slouží jako buffer, aby nebylo nutné pro každý řádek odesílat dotaz na server. Metoda pro generování vrací postupně data načtená v bufferu. Až dojde na konec, načte z databáze novou sadu dat. Pokud nastane situace, kdy je referenční tabulka prázdná, vrací generátor speciální hodnotu “NO DATA”, která upozorňuje na nastalou situaci. Při získávání dat pro buffer z databáze jsme se zabývali možnostmi implementace dotazu, jehož výsledkem by byla pseudonáhodná podmnožina dat dané tabulky. Nicméně se jedná o operaci, na kterou není SQL Server konstruován a tak nejspíše neexistuje nativní způsob, jak tento dotaz formulovat. S určitými předpoklady by bylo možno využít indexovacích struktur, nicméně pro obecný případ jsme využili možnost náhodného setřízení pomocí operace ORDER BY NEWID().
64
11. Předdefinované generátory Předdefinované generátory tvoří druhou skupinu generátorů. Doména, ze které generují data je předem daná nějakou datovou sadou. Několik datových sad jsme předpřipravili, nicméně uživatel může zvolit vlastní. Každý předdefinovaný generátor je potomek třídy PredefinedGenerator (která je také potomkem třídy iDataGenerator), čili implementuje všechny funkce potřebné pro generátory. Společné pro všechny předdefinované generátory je vnitřní pole uložené v paměti, ve kterém se uchovává načtená datová sada. Předpokládáme, že datové sady nebudou extrémně velké a nebudou tak klást velké paměťové nároky. Datová sada se nastavuje vždy při inicializaci daného předdefinovaného generátoru. Implementujeme tyto předdefinované generátory: • jména • příjmení25 • ulice26 • města27 • emaily • domény28 • telefonní čísla • barvy29 • uživatelský CSV soubor 25
Zdroj dat pro jména a příjmení: URL http://names.mongabay.com/ [citováno 22. května 2013] 26 Zdroj: URL http://uk.answers.yahoo.com/question/index?qid= 20081111031516AArLWwP [citováno 22. května 2013] 27 Zdroj: URL http://en.wikipedia.org/wiki/List_of_United_States_cities_by_ population [citováno 22. května 2013] 28 Zdroj: URL http://www.domaining.com/ [citováno 22. května 2013] 29 Zdroj: URL http://www.w3schools.com/html/html_colornames.asp [citováno 22. května 2013]
65
Pro uživatelský CSV generátor i pro ostatní předdefinované sady je nutné dodržovat určitá pravidla při tvorbě souboru s daty. A to pravidlo, že na prvním řádku je vždy pouze řetězec, který se skládá ze znaků, jenž slouží jako oddělovače jednotlivých záznamů. Na dalších řádcích jsou samotná data. Pokud je tedy oddělovačem záznamů konec řádku, první řádek datového souboru zůstane prázdný a pak na každém řádku bude právě jeden záznam. Předdefinované datové sady mají svá data uložena v souborech, které jsou v adresáři data. Je možné tyto cesty změnit a to v okně Preference, které se zobrazí po kliknutí na příslušnou položku v menu. Naše datové sady jsou v angličtině, ale není problém takto změnit jejich nastavení a použít vlastní lokalizované soubory.
66
12. Průběh generování Nakonec se zaměříme na samotný průběh generování dat, který je z hlediska uživatelského rozhraní zastoupen oknem Generování. Již na začátku textu jsme zmiňovali různé technologie spojení klienta .NET s SQL Serverem. Při odesílání vygenerovaných dat do databáze využíváme mechanizmus, který bychom zahrnuli spíše do ADO.NET Disconnected. Konkrétně jde o třídu SqlBulkCopy. Tato třída umožňuje efektivní vkládání velkých objemů dat do databáze. Zamysleme se nyní nad možnostmi odesílání dat z aplikace do databáze.
12.1
Vkládání dat do databáze
Existuje několik přístupů k dané problematice. Ty se od sebe liší jak na straně naší aplikace(obecně an straně klienta), tak na straně serveru. Kromě implementačních rozdílů jsou zde rozdíly ve výkonnosti, které jsou právě motivací, proč se vůbec způsobem vkládání dat zabývat.
12.1.1
Naivní přístup
Tento postup je při použití ADO.NET Connected nejpřímočařejší možný. Každý jeden vygenerovaný záznam se odesílá do databáze. Algoritmus 4 Naivní přístup for i = 1 → PočetGenerovanýchŘádků do Array[]data ← V ygenerujData() new SqlCommand("INSERT INTO tabulka (s1,s2,...) VALUES (data[0], data[1], ..)",connection).ExecuteNonquery(); end for Tento algoritmus funguje, ale je velice pomalý. Samotné vložení jednoho řádku do databáze je typicky rychlé, nicméně celková režie kolem vykonání jednoho samostatného příkazu je neúnosně velká. Pro každý vygenerovaný řádek se instanciuje nový příkaz. Prostřednictvím spojení se kontaktuje SQL Server. Pomocí TDS [5, s. 11] se příkaz předá ke zpracování dále, kde je zaznamenána nová událost. Po přečtení a vyhodnocení příkazu se tento optimalizuje nebo se najde
67
dříve použitá optimalizace. Takto optimalizovaný plán vykonání příkazu ze pak odesílá dál k fyzickému zpracování. Z pohledu jednoho dotazu jde o zanedbatelný čas, který je potřebný k provedení všech uvedených kroků. Pokud budeme generovat větší množství dat, bude tato režie časově řádově náročnější, než samotné vykonání dotazu na straně SQL Serveru. Pokud bychom chtěli použít populární metaforu, jedná se o nošení vědra vody po lžičkách.
12.1.2
Zřetězené vkládání
Dalším možným přístupem je tzv. zřetězené vkládání. Jeho principem je více TSQL příkazů sjednotit v jednom .NET příkazu a ušetřit si tak režii při odesílání a zpracování serverem. Algoritmus 5 Zřetězené vkládání Require: velikostBuf f eru for i = 1 → PočetGenerovanýchŘádků do text = ”” for j = 1 → velikostBuf f eru ∨ PočetGenerovanýchŘádků do Array[]data ← V ygenerujData() text+ =”INSERT INTO tabulka (s1,s2,...) VALUES (data[0], data[1], ..);” end for new SqlCommand(text,connection).ExecuteNonquery(); end for Narozdíl od předchozího přístupu používáme buffer pro dočasné uchování vygenerovaných dat, což vede ke zvýšení paměťové náročnosti, které je spíše teoretické a můžeme jej s klidem zanedbat. Tento postup je efektivnější, nicméně v jedné dávce odesíláme více TSQL příkazů, které musí SQL Server postupně zpracovat.
12.1.3
Víceřádkové vkládání
Pokud bychom chtěli eliminovat nevýhodu předchozího postupu, zvolíme tento algoritmus. Ten využívá technologii, které se říká Table Value Constructor[5]. Zjednodušeně řečeno se jedná o schopnost specifikovat virtuální tabulku množinou řádků. Zapsáno v pseudokódu: 68
Algoritmus 6 Víceřádkové vkládání Require: velikostBuf f eru for i = 1 → PočetGenerovanýchŘádků do text = ”INSERT INTO tabulka (s1,s2,...) VALUES” for j = 1 → velikostBuf f eru ∨ PočetGenerovanýchŘádků do Array[]data ← V ygenerujData() text+ = ”(data[0], data[1], ..),” end for new SqlCommand(text,connection).ExecuteNonquery(); end for Existuje jedno omezení Table Value Constructoru a to, že lze v jednom příkazu definovat nejvýše 1000 řádků. Tedy velikost bufferu v paměti může být nejvíc 1000. Pokud bychom využili další optimalizace pro vkládání - například table-hint TABLOCK[5], tedy uzamčení celé tabulky pro vkládání, omezili bychom tím množství zápisů do databáze i transakčního logu a získali tak nejvýkonnější způsob, kterého lze dosáhnout tímto principem.
12.1.4
ADO.NET Disconnected
Úplně odlišným konceptem je použití technologie ADO.NET Disconnected. Princip fungování je takový, jak jsme jej popsali na začátku celého textu. Algoritmus 7 ADO.NET Disconnected Require: velikostBuf f eru nechť máme adaptér DataAdapter pri spojení s databází a DataT able odpovídající tabulce, pro kterou chceme generovat for i = 1 → PočetGenerovanýchŘádků do for j = 1 → velikostBuf f eru ∨ PočetGenerovanýchŘádků do DataT able ← V ygenerujData() end for vlož řádky z DataT able prostřednictvím DataAdapteru do databáze end for V tomto případě se nemusíme zabývat žádnými příkazy nebo sestavováním TSQL dotazů. Ukazuje se však, že tento způsob je stejně neefektivní, jako prvně zmiňovaný. Pokud bychom chtěli nahlédnout na průběh odesílání dat do databáze, můžeme využít velmi užitečný nástroj Microsoft SQL Server Profiler 30 , který 30
SQL Server Profiler, URL http://msdn.microsoft.com/en-us/library/ms181091%28v= sql.105%29.aspx
69
umožňuje sledování příkazů, které SQL Server zpracovává. Budeme-li zaznamenávat všechny dotazy a události, zjistíme, že vkládání dat tímto způsobem je identické, jako v prvním, naivním případě.
12.1.5
SqlBulkCopy
Jako poslední uvádíme způsob, který jsme se rozhodli použít i my v naší aplikaci. Jedná se o použití stejnojmenné třídy SqlBulkCopy z knihovny ADO.NET, který bychom principielně zařadili do ADO.NET Disconnected. Je zde několik podstatných rozdílů oproti posledně zmiňovanému neefektivnímu způsobu. Například je nutné explicitně definovat připojení k databázi bez použití datového adaptéru. Nejdříve definujeme datovou tabulku, která bude sloužit jako buffer. Této tabulce je nutné určit sloupce a jejich datové typy. Buffer se pak odešle do databáze právě pomocí objektu SqlBulkCopy. Ten je jakýmsi ADO.NET ekvivalentem populární utility bcp31 , která slouží pro nahrávání dat ze souborů do databáze. Odesílání je optimalizované pro vtětší objemy dat. Jedná se tak společně s přístupem víceřádkového vkládání o nejefektivnější metody, navíc zde není omezení na velikost bufferu a také odpadá nutnost vytváření TSQL dotazů v kódu.
12.2
Metody vkládání
RDG při generování využívá dvě metody vkládání dat do databáze. Metoda je určena podle sloupců, které jsou na dané dabulce definovány a jsou vybrány pro generování. Metody jsme rozdělili na: 1. Obyčejné generování 2. Unikátní generování
12.2.1
Obyčejné generování
Obyčejné generování jsme vlastně probrali v předchozím odstavci. Funguje jednoduše a intuitivně - jednotlivé sady řádek se vkládají do datové tabulky (bufferu) 31
bcp, URL http://msdn.microsoft.com/en-us/library/ms162802%28v=sql.105%29. aspx [citováno 22. května 2013]
70
a pak prostřednictvím SqlBulkCopy do databáze. Je tak navíc možné kontrolovat stav generování a uživatele informovat prostřednictvím deterministického ProgressBaru.
12.2.2
Unikátní generování
Narozdíl od obyčejného je unikátní generování složitější. Využívá se v případě, že je mezi sloupci takový, který je definovaný jako unikátní explicitně nebo je součástí primárního klíče. Způsobů, jak vygenerovat unikátní data je jistě celá řada. Nyní popíšeme způsob, jakým jsme se rozhodli postupovat. Unikátní generování využívá dočasné tabulky založené v databázi, pro kterou se generují data. Přesunujeme tak část zátěže z klienta na server, což odpovídá konceptu aplikace, který jsme představili na začátku textu. Průběh unikátního generování lze dále rozdělit do fází. 1. inicializace 2. generování unikátních sloupců 3. obyčejné generování ostatních sloupců 4. přesun dat do cílové tabulky 5. ukončení Inicializace V prvním kroce dojde k rozdělení sloupců do dvou seznamů - unikátních sloupců a ostatních sloupců. Pro každý unikátní sloupec je vytvořena speciální tabulka v databázi, která má dva sloupce. Jeden definovaný jako IDENTITY(1,1) a druhý odpovídající sloupci původní tabulky. Dále je definována ještě jedna tabulka, která obsahuje právě sloupce ze seznamu ostatních sloupců. Generování unikátních sloupců Tento krok plně využívá možnosti paralelního zpracování a Connection Poolingu. Connection Pooling je technologie, kdy je uchováváno více připojení k SQL Serveru. Při požadavku na vytvoření nového připojení se pak fyzicky, je-li to možné, 71
nevytváří nové připojení ale vrací se dříve vytvořené připojení z Connection Poolu. Connection Pooling slouží k urychlení připojení k SQL Serveru, neboť fyzické vytvoření nového připojení je relativně náročná a pomalá operace. Pro každou tabulku definovanou z unikátního sloupce se generuje obyčejným způsobem 1.3 násobek počtu řádek, kolik uživatel nastavil. Jedná se o koeficient, který jsme získali pozorováním. Po vygenerování tohoto počtu záznamů je na každé tabulce vytvořen klastrovaný index na sloupci, který odpovídá sloupci původní tabulky. Pomocí tohoto indexu budeme schopni efektivněji detekovat duplicity, což právě teď provedeme - zjistíme počet unikátních záznamů z právě vygenerovaných dat. Pokud jich máme alespoň tolik, kolik uživatel nastavil, pokračujeme dalším krokem. V opačném případě pokračujeme v generování a po každé vygenerované dávce zjistíme počet unikátních hodnot. Tímto způsobem pokračujeme tak dlouho, dokud nemáme požadovaný počet záznamů nebo nevyprší definovaný časový limit. Mohl by totiž nastat případ, kdy uživatel bude požadovat více unikátních záznamů, než je velikost domény, ze které se data generují. Obyčejné generování ostatních sloupců Jelikož zde není omezení na unikátnost dat, stačí nám pro zbylé sloupce vygenerovat data obyčejným způsobem, jak jsme již popsali dříve. Data vložíme do speciálně vytvořené tabulky. Přesun dat do cílové tabulky V předposledním kroce jde o přesun dat z nově vytvořených dočasných tabulek do původní cílové tabulky. Jak se ukázalo, jedná se o relativně netriviální problém, zvláště pokud bychom chtěli zachovat pseudonáhodou distribuci dat. Jednoduše řečeno chceme z každé tabulky definované pro unikátní sloupec získat unikátní záznamy a ty spojit dohromady spolu s tabulkou ostatních sloupců. Řádky tabulek chceme spojovat bez ohledu na jejich obsah, což je operace, na kterou není SQL Server konstruován a proto je nutné si pomoci malým trikem. Z tohoto důvodu jsme do každé nově definované tabulky přidali IDENTITY sloupec, který slouží jako pomocná struktura pro spojování. Nemůžeme ho použít přímo, kvůli
72
filtrování pouze unikátních hodnot. Chybějící řádky nahradíme použitím funkce ROW NUMBER(), kdy použijeme setřízení právě přes definovaný IDENTITY sloupec. Přes tyto hodnoty již můžeme spojit všechny tabulky do jedné a získaná data vložit původní cílové tabulky. Ukončení V posledním kroce dojde ke zrušení nově definovaných tabulek. Dále se zpět vrátí nastavení generátorů z cizího klíče, pokud odkazovalo na některý z unikátních sloupců. Následně je vyplněn příslušný ProgressBar v uživatelském rozhraní a úloha je dokončena.
73
13. Experimenty V textu jsme zmiňovali různé optimalizace, které jsme implementovali pro zefektivnění procesu generování. Pro konkrétnější představu o časech potřebných pro generování dat uvádíme výsledky experimentálních měření. Testovací prostředí Tabulka 13.1: Parametry testovacího prostředí Parametr CPU
Hodnota Intel® Core™ i5 3210 (IvyBridge) 2.5 GHz
RAM
8 GB DDR3 1600MHz
HDD
5400RPM SATAIII
OS
Windows 8 64-bit
SQL Server
Microsoft SQL Server 2008R2 Enterprise Edition (64-bit)
.NET
Microsoft .NET Framework 4.5
Pro testování použijeme prázdné tabulky s jedním sloupcem bez definovaných indexů (heap). Výsledky pro jednotlivé experimenty jsou průměrem deseti náhodných generování se stejnými parametry. Instance SQL Serveru běží na stejném počítači jako RDG Náhodné řetězce a celá čísla Pro řetězce definujeme sloupec typu VARCHAR(8000), pro celá čísla standardní typ INT. Náhodné řetězce budeme generovat v délce 128, 256 a v extrémním případě 8000 znaků. Osa Y má logaritmické měřítko, abychom byli schopni odečíst i nízké hodnoty. Také je vidět potvrzení intuitivního předpokladu, že doba generování je lineárně závislá na počtu řádků. U náhodných řetězců je doba potřebná pro generování lineární vůči délce řetězce. 74
Obrázek 13.1: Graf - náhodné řetězce a čísla Generátor podle regulárního výrazu Z generátorů jsme se nejvíce věnovali právě tomu, který vrací pseudonáhodné řetězce odpovídající zadanému regexpu, proto i zde uvedeme několik příkladů, které ilustrují časovou náročnost na použití tohoto generátoru. Jako první jsme zvolili alternativu ke generátoru náhodných řetězců, tedy regulární výraz, jehož ohodnocením jsou řetězce znaků s omezenou maximální délkou. Budeme generovat vždy přibližně 1 000 000 záznamů a sledovat závislost délky trvání na maximální délce řetězce. Opět můžeme pozorovat lineární závislost. Oproti předchozímu generátoru nedosahujeme tak dobrých časů, což je dáno použitím silnějšího a obecnějšího mechanizmu. Další příklad zobrazuje závislost délky regexpu a času potřebného pro vygenerování 100 000 záznamů. Jako regexp použijeme strukturu podobnou chemickému vzorci fiktivní organické sloučeniny. Ta se skládá z několika skupin. Osa X grafu níže zobrazuje počet skupin v regexpu. Operátor iterace + má nastaven maximální počet opakování na 100. Poslední dva regexpy jsou tvořeny zřetězením regexpů s osmi skupinami.
regexp IPv6 adresy ([0 − 9a − f ]{4} :){7}[0 − 9a − f ]{4} doba trvání: 54 s
testovací tabulka s deseti sloupci: • INT IDENTITY primární klastrovaný klíč • 3 INT sloupce • regexp struktury chemické sloučeniny • regexp IPv4 • regexp IPv6 77
• 3 sloupce DATETIME doba trvání: 132 s
Závěr Z uvedených údajů můžeme vyvodit závěr, že pro standardní použití, které jsme nasimulovali v testovacím prostředí, je aplikace dostatečně výkonná. Doby generování v jednotkách minut pro statisíce záznamů jsou přiměřené. Jak jsme již zmínili dříve, neuvažujeme generování dat pro celé rozsáhlé databáze jako typický příklad použití. Nicméně, jak se potvrdilo, závislost objemu dat a času potřebného pro vygenerování je lineární, takže je možné lehce odhadnout přibližnou dobu trvání pro větší objem dat na základě pozorování rychlosti s menším počtem generovaných záznamů.
78
Část IV Ukázková databáze
79
14. Ukázková databáze Jako poslední část textu jsme zařadili ukázkovou databázi, na které si uživatel může jednoduše vyzkoušet práci s RDG. Dle našeho názoru je takový tutorial pro začínajícího uživatele přínosnější, než listování ve formální dokumentaci. Ukázková databáze je fiktivní evidencí kontrol řidičů. V systému evidujeme osoby - řidiče a policisty, automobily a jejich kontroly společně se zjištěnými přestupky. Jednoduchý model databáze znázorňuje obrázek.
Obrázek 14.1: Schéma ukázkové databáze
1. Nejdříve je třeba vytvořit databázi a to spuštěním skriptu 01 CREATE DB.sql z adresáře tutorial. 2. Spusťte RDG, připojte se k instanci, na které jste vytvořili novou databázi a vyberte DB EvidenceKontrol. 80
3. Po načtení hlavního okna uvidíte databázové struktury.
Obrázek 14.2: Struktury DB EvidenceKontrol
4. Nyní začneme postupně nastavovat jednotlivé generátory. První bude tabulka Adresa. Na jedné adrese může bydlet více osob, tedy osoba má cizí klíč na adresu a adresa je v topologickém uspořádání před osobou. Řádků ke generování: 10000. Můžeme využít předdefinované generátory, pro jednotlivé sloupce nastavíme: • mesto - Cities • ulice - Streets • cp - Random numbers ponecháme, ale snížíme maximální hodnotu na 1000. 5. Pro tabulku Osoba definujeme generátory: • jmeno - First names 81
• prijmeni - Last names • RC - ponecháme Random numbers, ale nastavíme rozsah od 1000000000 do 10000000000. Řádků ke generování: 100000. 6. V tabulce Ridic vybereme všechny sloupce a nastavíme: • cisloRidicaku - Regex matching strings, pattern [: upper :]{2}[: digit :]{6} • opravneni - ponecháme původní hodnoty • pocetBodu - nastavíme maximální hodnotu na 16 Řádků ke generování: 50000. 7. Pro tabulku Policista: • SC - ponecháme • hodnost - User defined CSV, soubor hodnost.csv z adresáře tutorial Řádků ke generování: 5000. 8. Pro tabulku Automobil: • SPZ - Regex matching string, pattern [1−9][ABCEHJKLM P ST U Z][: digit :]{5} • znacka - User defined CSV, oubor znacky.csv z adresáře tutorial • barva - Colors Řádků ke generování: 10000. 9. V tabulce TK pouze zaškrtneme sloupec fk automobil. Řádků ke generování: 20000. 10. V tabulkách Kontrola(řádků ke generování: 500000) a JeZjisten (řádků ke generování: 1500000) opět pouze zaškrtneme všechny sloupce.
82
Obrázek 14.3: Nastavení generátorů 11. Nyní stačí pouze stisknout tlačítko GENERATE a generování bude spuštěno. O průběhu informuje příslušné okno, ve kterém se zobrazuje stav generování a časové údaje spuštění a dokončení.
83
Obrázek 14.4: Průběh generování
84
Závěr Cílem naší bakalářské práce bylo vytvoření jednoduché, ale přitom dostatečně výkonné, aplikace, která by umožnila naplnění databázových struktur testovacími daty. Takové aplikace již existují, ale jsou zpoplatněny částkami v řádech stovek dolarů a navíc je ovládání některých z nich poněkud těžkopádné. Naše aplikace si však neklade za cíl konkurovat komerčním produktům, ačkoli jsme u experimentů mohli nahlédnout, že doba potřebná pro vygenerování uváženého počtu záznamů je akceptovatelná a nebrání použití aplikace při testování. Použitelnost není omezena ani datovými typy, jelikož všechny nejdůležitější jsou podporovány. S tím souvisí možnosti strukturování dat pro jednotlivé typy. Nejsilnějším nástrojem je generátor podle regulárního výrazu, který umožňuje uživateli strukturovat generované záznamy pokročilým způsobem. V textu jsme upozornili i na zajímavé detaily z pohledu implementace s využitím nejmodernějších technologií a poznatky teoretické, na jejichž základech jsme celou aplikaci navrhli. Všechny tyto funkce jsou skryty za jednoduchým grafickým uživatelským rozhraním, které se drží moderního trendu vzhledu desktopových aplikací v systému Windows. Domníváme se, že v rámci příslušného rozsahu se nám podařilo stanovené cíle splnit a máme tak k dispozici nástroj, který jsme specifikovali. Nicméně bychom i nadále chtěli aplikaci vyvíjet, např. na základě poznatků z jejího reálného nasazení. Dalšími směry budoucího vývoje mohou být nové generátory, možnost určení pravděpodobnostního rozdělení nebo další paralelizace celého procesu generování.
85
Seznam použité literatury [1] Troelsen, Andrew. Pro C# 2010 and the .NET 4 Platform. 5. vydání. New York City: Apress, 2010. 1752 s. ISBN13: 978-1-4302-2549-2 [2] Mareš, Martin, David Matoušek a Petr Škoda. Recepty z programátorské kuchařky: Grafy. [online]. [citováno 22. května 2013] . Dostupné z URL: http://ksp.mff.cuni.cz/tasks/18/cook2.htm [3] Zavoral,Filip, PhD. Návrhové vzory: Singleton. [online]. [citováno 22. května 2013] . Dostupné z URL: http://ulita.ms.mff.cuni.cz/pub/predn/ NPRG024/05-Singleton.ppt [4] Barták,Roman. Automaty a gramatiky: on-line, 2001. [online]. [citováno 22. května 2013] . Dostupné z URL: http://ktiml.mff.cuni.cz/˜bartak/ automaty/index.html [5] Delaney, Kalen, Paul S. Randal, Kimberly L. Tripp, Conor Cunningham, Adam Machanic a Ben Nevarez. Microsoft® SQL Server® 2008 Internals. Microsoft Press, 2009. 786 s. ISBN: 978-0-7356-2624-9 [6] Ramakrishnan, Raghu, Johannes Gehrke. Database Management Systems. 3. vydání. McGraw-Hill Education, 2003. 1065 s. ISBN 978-0-07246563-1 [7] Chen, Peter Pin-shan. The Entity-Relationship Model: Toward a Unified View of Data. ACM Transactions on Database Systems, 1976, 36 s. [8] Codd, Edgar F. Further Normalization of the Data Base Relational Model. San Jose, California: IBM Research Report, 1971 [9] Comer, Douglas. Ubiquitous B-Tree. ACM Computing Surveys New York: ACM, 1979, 121-137 s. [10] Kučera, Luděk. Algovize aneb procházka krajinou algoritmů. Blatenská tiskárna s.r.o, 2009, 307 s. ISBN 978-80-902938-5-4
86
[11] Information technology — Portable Operating System Interface (POSIX®) Base Specifications. IEEE std. 9945, 2009 [12] Dijkstra, Edsger W. ALGOL-60 Translation. Stichting mathematisch centrum, Amsterdam: ALGOL Bulletin, 1961, 11 s.
Seznam použitých zkratek SŘBD Systém řízení báze dat DBMS Database management system RDBMS Relational database management system SŘRBD Systém řízení relační báze dat ER Entitně-Relační MSSQL Microsoft® SQL Server® RDG Random data generator for Microsoft SQL Server EF EntityFramework OR Objektově-relační WPF Windows Presentation Foundation XAML Extensible Application Markup Language TPL Task Parallel Library TDS Tabular data stream
Přílohy Obsah přiloženého CD • application - obsahuje zkompilovanou aplikaci • source - obsahuje zdrojové kódy aplikace • tutorial - obsahuje podklady k ukázkové databázi • doc - obsahuje tento text ve formátu PDF a obrázky použité v textu