Dotazy na tabulky s dědičností

Dědičnost tabulek je zajímavá vlastnost Dynamics AX 2012, díky které je možné modelovat hierarchie typů v databázi velmi podobně jako v objektovém modelování. Dynamics AX nejen že umožňuje definovat dědičnost v metadatech, ale také ji automaticky zohledňuje v dotazech do databáze, na formulářích a podobně.

Problém je, že ji řada vývojářů používá bez pochopení, jak vlastně funguje, a její nesprávné použití může mít podstatný dopad na kvalitu datového modelu a na výkon. (A také komplikuje implementaci formulářů a podobně, ale tomu se dnes nebudu věnovat.)

Začněme tímto triviálním dotazem:

OMInternalOrganization org;
select org;

Bude nás zajímat, jaký dotaz je ve skutečnosti poslán do databáze – to snadno zjistíme pomocí jiné funkcionality přidané v AX2012.

OMInternalOrganization org;
select generateOnly org;
info(org.getSQLStatement());

Nepřehlédněte klíčové slovo generateOnly, bez nějž by metoda getSQLStatement() vrátila prázdný řetězec. Žádný výstup také nedostanete tehdy, když jsou data načtena z cache, protože se skutečně žádný dotaz do databáze neposílá.

Kdo čekal, že výsledkem bude něco jako SELECT * FROM OMInternalOrganization, bude asi dost překvapen. Výsledný dotaz vypadá v AX2012 takto (seznam sloupců jsem v zájmu čitelnosti nahradil hvězdičkou) :

SELECT * FROM DIRPARTYTABLE T1
    CROSS JOIN DIRORGANIZATIONBASE T2
    CROSS JOIN OMINTERNALORGANIZATION T3
    LEFT OUTER JOIN OMTEAM T4 ON (T3.RECID=T4.RECID)
    LEFT OUTER JOIN OMOPERATINGUNIT T5 ON (T3.RECID=T5.RECID)
    LEFT OUTER JOIN COMPANYINFO T6 ON (T3.RECID=T6.RECID)
    WHERE (T2.RECID=T1.RECID) AND (T3.RECID=T2.RECID)

V X++ vidíme dotaz na jedinou tabulku, ale do databáze jde dotaz na šest tabulek. Jak to?

Kdyby systém vrátil pouze pole z tabulky OMInternalOrganization, nebylo by to příliš užitečné (vlastně je tam jediné aktivní pole: OrganizationType). V první řadě chceme dostat také všechna zděděná pole, tudíž AX se musí dotázat také na pole všech předků, v tomto případě tabulek DirPartyTable a DirOrganizationBase. (Nenechte se zmást těmi CROSS JOINy, klauzule WHERE z nich efektivně dělá INNER JOINy.)

Záznam v tabulce OMInternalOrganization může ve skutečnosti reprezentovat jednoho z jejích potomků (OMInternalOrganization je navíc abstraktní, takže sama o sobě vůbec nemá smysl). Abychom dostali kompletní údaje ke všem záznamům, musíme nalinkovat i pole ze všech potomků, tedy OMTeam, OMOperatingUnit a CompanyInfo. K tomu je pochopitelně třeba použít OUTER JOIN.

Celý ten komplikovaný dotaz se zkrátka snaží získat všechna potřebná pole. Pokud ale požadujeme jen některá pole, AX může dotaz podstatně zjednodušit. Například:

OMInternalOrganization org;
select OrganizationType from org;

… vygeneruje tento dotaz:

SELECT T1.INSTANCERELATIONTYPE,T1.RECID,T2.ORGANIZATIONTYPE,T2.RECID
FROM DIRPARTYTABLE T1
CROSS JOIN OMINTERNALORGANIZATION T2
WHERE (T2.RECID=T1.RECID)

Abyste se vyhnuli spojování velkého počtu tabulek, neměli byste vytvářet příliš rozsáhlé hierarchie dědičnosti. Také byste se měli snažit omezit vracené sloupce, protože pak není třeba načítat všechny tabulky v daném podstromu.

Pokud chcete vědět více, zkuste třeba Developing with Table Inheritance.

AX2012 R2

Aby to nebylo tak jednoduché, pojďme se podívat, jak to funguje v AX 2012 R2 (CTP). Spustíme v zásadě identický dotaz (forceLiterals nám pomůže dostat podrobnosti, které budeme potřebovat později):

OMInternalOrganization org;
select generateOnly forceLiterals org;

A dostaneme tento T-SQL kód:

SELECT * FROM DIRPARTYTABLE T1
WHERE ((T1.PARTITION=5637144576) AND (T1.INSTANCERELATIONTYPE IN (2376,41,2377,5329) ))

Je zřejmé, že celý dotaz je mnohem jednodušší než v předchozí verzi, ale asi není příliš jasné, jak vlastně funguje.

Mělo by vás zarazit, že v dotazu nefiguruje jiná tabulka než DirPartyTable, dokonce ani OMInternalOrganization, na kterou jsme se dotazovali v X++. Pokud bych zde zobrazil všechna pole, mohli byste zjistit (po chvíli pátrání), že všechna pole definovaná v Dynamics AX v potomcích jsou v databázi přímo v tabulce DirPartyTable.

Ale pokud jsou všechna pole v DirPartyTable,  k čemu pak jsou rozšiřující tabulky jako OMInternalOrganization nebo CompanyInfo? Odpověď je: V databázi potřeba nejsou a vůbec v ní neexistují. Jako samostatné objekty existují až v aplikační vrstvě.

Přestože jsou data všech typů z jedné hierarchie uložena v jediné tabulce, informace o struktuře v ní samozřejmě existuje. V T-SQL můžete vidět, jak AX získá podstrom hierarchie, v které se nachází dotazovaná tabulka:

WHERE ((T1.PARTITION=5637144576) AND (T1.INSTANCERELATIONTYPE IN (2376,41,2377,5329) ))

Pole Partition souvisí s jinou novinkou v AX2012 R2, ale na dědičnost nemá vliv, tudíž ho zde můžeme ignorovat. To podstatné je InstanceRelationType – dříve toto pole určovalo, z jakých dalších tabulek je třeba načíst data, nyní se podle něj zkrátka vyfiltrují požadované typy záznamů. Hodnoty v InstanceRelationType představují ID tabulek:

2376 OMInternalOrganization
41 CompanyInfo
2377 OMOperatingUnit
5329 OMTeam

Veškerá pole, která nejsou definovaná v rodičovské tabulce, mají smysl jen pro určité typy záznamů. Pokud v určitém podtypu nějaké pole neexistuje, jeho hodnota v databázi je jednoduše NULL.

Přestože dědičnost tabulek vypadá v AX stále stejně, implementace v databázi se zcela změnila. AX2012 R2 se obejde bez spojování mnoha tabulek, ale zas vytváří jednu potenciálně velikou tabulku. Způsob návrhu a optimalizace se tedy může v AX2012 a AX2012 lišit, ale základní pravidla platí obecně: chápat co všechno se děje na pozadí, vyhnout se příliš komplikovaným hierarchiím, vyhnout se dědičnosti v oblastech s vysokými požadavky na výkon a výslednou implementaci (včetně výkonu) otestovat.