čtvrtek 27. prosince 2007

Anotace nahrazující SQL a OQL dotazy (popis)

Bez dalších řečí se rovnou pustím do popisu.
Jak už jsem předeslal, základem dané filtrace nad daty, jsou anotace. Uvedu jejich výčet a popis.

@Criteria (class)
Jedná se o základní anotaci, která říká, že se jedná o "filtrační" objekt, který je schopen nabídnout své atributy k porovnání. Jediným parametrem je entita, která říká o jakou entitu se jedná.

@Criterion (atribut)
Popisuje atribut, který je automaticky brán jako jeden z prvků filtrace. Samotná anotace obsahuje moznosti, které označí, jak se k danému atributu bude přistupovat.
Zde je výčet některých z nich:

  • property - název atributu podle dané entity, může se jednat i o vnořenou hodnotu (zakaznik.id)

  • operator - definuje typ porovnávání mezi hodnotou atributu a názvem atributu, typ porovnávání je výčtový typ, který lze rozšířit

  • excludeEmptyString - určuje, zda se bude brát v potaz prázdný String či se bude ignorovat


V budoucnu předpokládám rozšíření těchto vlastností. Ale držím se pravidla: Méně je někdy více. Přeci jen nechci tvořit kód, který bude obsahovat spoustu deprekovaných způsobů.

@Between (atribut)
Složení minimální a maximální hodnoty, která je prováděna na základě definovaného idf a property, což je výčtový typ, který může být buď MIN či MAX. Výhodou tohoto použití je také v tom, že pokud jedna z hodnot neexistuje, automaticky převádí BETWEEN na porovnání jedné hodnoty. Pokud například hodnota pro MAX bude empty a MIN bude existovat, bude generována následující podmínka: WHERE datum >= 'hodnota'.

@Conjunction (atribut)
Konjunkce funguje podobně jako v Criteria API. Na základě definovaného idf se spojí ty hodnoty, které k sobě patří. Zde ovšem existuje jedno omezení (stejně jako u disjunkce): Vnořené podmínky. Je to další krok, který budu muset vyřešit.

@Disjunction (atribut)
Opět podobný způsob jako v případě konjunkce. Zde se tedy generuje něco jako: WHERE (property [operator] value OR ....). Spojení daných atributu se opět provádí pomocí stejného názvu v idf.

@Alias (class)
Jelikož se často stává, že se potřebuji dotazovat na sloupec, který je v nějaké tabulce, která je v relaci s danou entitou, potřebuji aliasovat danou entitu, pro kterou bude platit nasledující alias.
Zde rovnou uvedu příklad:
Mám zaměstnance, který spadá do daného střediska, z kterého chci znát název, podle kterého filtruji. V OQL by mohl dotaz vypadat následovně: FROM Zamestnanec z WHERE z.stredisko.nazev = 'Vyroba'. V Criteria API se k danému sloupci dostanu přes alias, takže by to mohlo vypadat následovně: @Alias(associationPath = "stredisko", alias = "s").
Aby bylo možné definovat aliasů více, existuje anotace s nazvem Aliases, která může obsahovat pole daných aliasů.

@OrderBy (class)
Poslední popisovaná anotace slouží k definování řazení dat. Jelikož požadavek na řazení dat je častým jevem, existuje možnost nadefinovat filtračnímu objektu tuto vlastnost. Daná anotace obsahuje pole anotací s názvem @OrderValue, která má dvě vlastností a tím je název atributu pro řazení a typ řazení, který je výčtvový typ ASC nebo DESC.

Samotné použití je postaveno na DetachedCriteria. Ti, co znají Hibernate vědí, že se jedná o criteria, které neobsahují Session. Jinými slovy, takováto criteria si můžete vytvořit ještě v době, kdy o Hibernate Session nemá Váš kod ani ponětí. Jistě je to dobrý způsob jak zajistit určité odělení od zbytku vlastní implementace.

K přístupu slouží třída s názvem: "CriteriaDAOFactory". Jak už název napovídá, jedná se o factory, která vrací interface CriteriaDAO. Daný interface již obsahuje metodu vracející DetachedCriteria.
Dané použití může vypadat následovně:
MyFiltr filtr = new MyFiltr();
// naplneni filtru
CriteriaDAO d = CriteriaDAOFactory.getCriteria(filtr);
DetachedCriteria dc = d.getDetachedCriteria();


Nyní uvedu několik příkladů definování daného filtru a jeho ekvivalent v podobě SQL dotazu. U všech ukázek vynechám settry a gettry, které jsou nezbytnou součástí. Pokud někdo chce tvořit neměnný objekt, tak samozřejme settry může vynechat. Takže jedem:

@Criteria(entity = Zamestnanec.class)
public class ZamestnanecFiltr {

@Criterion(property="cislo")
private String id;

@Criterion(operator=RestrictionDAO.LIKE)
private String prijmeni;
}

SELECT * FROM zamestnanec
WHERE cislo = 'value' AND prijmeni LIKE '%value%'


@Criteria(entity = Zamestnanec.class)
@OrderBy(values={@OrderValue(name = "prijmeni")})
@Alias(associationPath = "stredisko", alias = "s")
public class ZamestnanecFiltr {

@Criterion(property="s.nazev")
private String nazev;

@Criterion(property="datumPrijeti")
@Between(idf = "dp", property = BetweenDAO.MIN)
private Date datumOd;

@Criterion(property="datumPrijeti")
@Between(idf = "dp", property = BetweenDAO.MAX)
private Date datumDo;
}

SELECT * FROM zamestnanec z
INNER JOIN stredisko s ON z.stredisko = s.cislo
WHERE s.nazev = 'value' AND z.datumPrijeti BETWEEN 'min' AND 'max'
ORDER BY z.prijmeni


Tím bych asi ukončil tento popis. Samozřejmě, lze dané věci dobře kombinovat. Je jasné, že se naleznou další potřebné funkce, které nechám spíše vyplynout postupem času. Pokud by někdo měl zájem, mohu se pokusit danou funčnost implementovat.

Poslední věcí, kterou chci zmínit je pár otázek, které bych rád nějak vyřešil. Jelikož vše má svá úskalí... Pokud bude mít někdo zájem, budu vítat jakoukoli pomoc či nakopnutí.

  • řazení dat je definováno staticky, jak implementovat dynamický způsob

  • specifikování získaných dat, jak omezení na určité sloupce, tak vlastní DTO či inicializace LAZY

  • vnořené konjunkce a disjunkce, které závisí na předchozích podmínkách, a zda má vůbec cenu něco takového implementovat

  • dynamické rozhodování zvoleného operátoru, uživatel například může mít k dispozici volbu mezi (LIKE, <, >, =)



Samotný projekt je psán v NetBeans, proto ho také jako NetBeans projekt vystavuji. Předem se omlouvám za absenci JUnit testů. Upřímně, zatím jsem neměl dostatek času vše otestovat a navíc testy mám komponovány přímo na danou specifikaci, ke které je třeba existence daného modelu a zdroje. Samotná implementace je závislá na Hibernate API a na log4j. Knihovna log4j je v adresáři lib, knihovny pro Hibernate neuvádím záměrně. Pro práci s Hibernate existuje mnohem větší závislost :)

Projekt ke stažení: IrminsulCriteria

7 komentářů:

  1. Ahoj,

    přiznám se, že jsem ne zcela pochopil, v čem tkví výhoda toho, že mám v DAO pouze jednu find metodu a pak haldu Filter objektů. Ale to je věc vkusu.

    Co se mi zdá jako lepší přístup, pak bych pro každý typ atributu, podle kterého se může vyhledávat, (tedy např. ID, jméno, číslo) vytvořit skupinu znovupoužitelných objektů, které podle něj filtrují (např. pro jméno: je sthodné, jako) a pak do find metody předat skupinu takových objektů, které umožní pomocí Criteria API vytvořit dotaz. Pokud by to byl OpenSource projekt, asi bych do něj šel ...

    Co se řazení týče, pak je to de facto vlastnost entyty, podle kterých atributů je možné řadit, takže buď to chce výčtový typ, který se předá a nebo poslouží starý dobrý string, jenž bude obsahovat názvy atributů, podle kterých řadit ...

    S pozdravem

    OdpovědětVymazat
  2. Predem, vyuziti navrhoveho vzoru command pro zapouzdreni filtru namisto rozsirovani rozhrani DAO objektu +1.

    Z jakeho duvodu byly zvoleny anotace resp. jaky by byl rozdil pouzit misto anotaci primo uvnitr objekt DetachedCriteria? Anotace stejne ve vysledku slouzi k tomu, aby se podle nich zkonstruoval DetachedCriteria objekt. Navic popis pomoci anotaci je staticky viz. problem s dynamickym rozhodovanim. To primo svadi drzet menitelne polozky jako soucast stavu filtr objektu.

    OdpovědětVymazat
  3. to Jira: Vyhodou takovych filter objektu muze byt fakt, ze ji mohu vystavit jako komponentu. Ted mam na mysli napr. v Seamu, kde automaticky do view vstriknu danou komponentu bez nejakych dalsich controlleru. Co se tyce razeni, take jsem premyslel, ze bych danou vlastnost nechal na dalsim parametru metody. Pomoci anotaci je to blbost a cpat to do filter objektu take (preci jen mohu radit i podle sloupce, podle ktereho nefiltruji). S tim predanim do find metody prilis nechapu, jak to myslis.

    to Dagi: DetachedCriteria se mi do filtr objektu nechtelo cpat z nekolika duvodu.
    1. Jsem prilis svazan s Criteria API, preci jen anotace jsou pouze popis, ktery lze vyuzit i s jinym mechanizmem (treba az budou v JPA 2.0 dana Criteria :))
    2. myslim, ze filtr objekt by mel nest jen to, k cemu slouzi
    3. kontrola hodnot, kdyz vezmu v potaz, ze prazdny String chci ignorovat, null chci ignorovat, nebo 0 chci ignorovat, musim pred kazdou vlozenou hodnotou provest podminku, coz zde odpada

    S cim souhlasim, tak je ten staticky problem u anotaci. Jedine co me napada, mit abstraktni tridu nad filtr objektem, ktera by byla schopna prepsat anotace. Tzn., ze bych byl schopen se dynamicky rozhodovat o zpusobu filtrovani.

    OdpovědětVymazat
  4. ad 1.) souhlas, vystaveni implementacniho detailu by nebylo dobre
    ad 2.) musi nest popis a hodnoty filtru
    ad 3.) to bude zrejme zaviset filtr od filtru a nebo je mozne to takto generalizovat ve vsech pripadech?
    ad dynamicka zmena.) jak jsem psal, jedina moznost to je mit jako stav objektu, kteremu bude rozumet builder toho criteria.

    OdpovědětVymazat
  5. ad 3.) Lze toto spravovat pomoci atributu "excludeEmptyString" a "excludeZero", oba jsou v anotaci @Criterion a oba nabyvaji boolean hodnoty. Prvni z nich rika, zda bude ignorovan string ktery je empty ci null (vychozi hodnota je true, tedy, ze bude ignorovat) a druhy je pro ciselny typ, kde rika, zda bude ignorovana '0' (vychozi hodnota je false, tedy, ze nebude ignorovat). Z duvodu nejcastejsiho pouziti, tedy ze se jedna o uzivatelskou filtraci, mi to prislo jako nejlepsi vychozi nastaveni, ktere lze ale u daneho filtru zmenit.

    S tou dynamickou zmenou je to stale na povazenou. K tomuto ucelu by klasicke settry, gettry byly docela malo, musela by ke kazdemu existovat dalsi vlastnost. Otazkou je jak to vyresit co nejelegantneji. Zejmena pro bezne pouziti. Nerad bych se dostal do faze, kdy sestaveni takoveho filtr objektu bude mnohem pracnejsi a slozitejsi, nez klasicky pristup pres Criteria API.
    Pokud te napada nejaky dobry zpusob, ktery nebude uplne prehlusovat jednoduchost filtru, sem s nim ;)

    OdpovědětVymazat
  6. to a.dostal&gt; No přijde mi, že se skutečně jedná pouze o přenesení kódu z DAO objektu do Filteru.

    Co se týče mé představy. Kdysi jsem postupoval podobným způsobem, tj. vytvořil jsem si 2 objekty, jedne pro selekci (filter) a druhý pro seskupení. Ty jsem předával do DAO objektu a vznial mi Report objekt, jenž jsem zobrazoval v JTable. Ovšem ukázalo se, že se stáloe příšerně opakuje kód.

    Co se mi zdá lepší, je vytvořit jakési Filter objekty, ale ne na celé entity, ale na jejich atributy. Tj. Mít Filter objektu na Date, String, Number atd. Mít různé varianty, tj. např. u Stringu na porovnávaní (větší, menší, rovno, vetší nebo rovno, menší nebo rovno), případně na vyhledávání pomocí LIKE operátoru.

    Následně by se do DAO objektu metodě find předávala množina dvojic (atribut, filtr). Metoda find by pak dokázala celkem jednoduše pomocí Filtr objektů aplikovaných na daný atribut vytvořit Criteria API tak aby správně načetla objekty z DB.

    OdpovědětVymazat
  7. to Jira: Ano, svym zpusobem mas pravdu v tom, ze se prenasi kod. Jenze, vyhodou je samotny fakt, ze filtr je lepe znovupouzitelny a neni potreba psat dotazy na ziskani dat. Dost casto uz samotna signatura metody urcuje, co bude vysledkem. Popripade samotny "cisty" filtr objekt rika co bude vysledkem. Proto jsem odstranil jednu cast a nahradil ji anotacemi.

    ad 2) Slozeni atribut-filtr je sice hezka zalezitost, ale videl bych hlavni problem v tom, ze bych stale musel sestavovat filtr objekt, coz uz mi prijde lepsi pouzit primo Criteria API. Navic bych nejak musel vyresit veci jako: OrderBy, disjunkce, vraceny typ, atd. coz uz presahuje ramec atribut-filtr. Ja jsem nektere veci nahradil tak, ze jsem vytvoril anotace nad celym filtr objektem, ktery prave presahuje ramec danych atributu.

    Mozna to nekomu prijde jako zbytecne prenaseni kodu, ale jednu vec jsem zapomel dodat. Ja dany filtr objekt pouzivam jako Seam komponentu, cimz padem jsem uplne odstinen od nejakeho sestavovani. Vse necham na frameworku. Pouze danou komponentu, vystavim a jeji atributy vstriknu do view (respektive do fitracniho formulare).

    Spis ted premyslim nad tim, jak do toho zakomponovat dynamickou zmenu. Prepsani anotaci pro dany typ filtrace. Tim bych zvysil znovupouzitelnost danych filtru a ziskal moznost dynamicke zmeny, kterou by "volil" uzivatel na zaklade danych "voleb".
    Asi jedine co me prijde zatim pouzitelne je abstraktni trida, ktera bude obsahovat neco jako: setPropetry(atribut, vlastnosti); ... pokud nekoho napada neco lepsiho, tak budu rad, jinak zkusim zakomponovat toto a pokusit se o dalsi "verzi" :)

    OdpovědětVymazat

Když programátor založí a řídí firmu

Jako malý jsem chtěl být popelářem. Ani ne tak proto, že bych měl nějaký zvláštní vztah k odpadkům, ale hrozně se mi líbilo, jak...