č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

IrminsulCriteria

Anotace nahrazující SQL a OQL dotazy (důvody)

V minulém příspěvku jsem psal o tom, jakým způsobem nadefinovat základní DAO vrstvu. Zvolil jsem způsob, který se stal zavislý na Hibernate Session. Tento "vendor" nabízí i jednu z věcí, kterou jsem si pomalu oblíbil, a tím jsou Criteria API.

Psaní základních SQL dotazů jsem nahrazil OQL dotazy. OQL je v podstatě velice podobný SQL s tím rozdílem, že daný dotaz se provádí nad entitami reprezentující data v DB. Díky tomu odpadá nutnost psát ruzné JOIN konstrukce, specifikovat vrácené sloupce a hlavně získávám nezávislost na použitém databázovém systému. Převod z MySQL na Oracle či MSSQL je triviální záležitost spočívající v přepsání pár řádku v persistence.xml či v přepsání JDBC zdroje v aplikačním serveru.

Ale zpět. I když je OQL velice elegantní způsob psaní, stále obsahuje velké omezení, které spočívá v tvorbě dynamických dotazů. Pokud aplikace přistupuje k datům tak, že je získává na základě uživatelského filtru, musím dotaz poskládat z daného Stringu, což je dost otravující záležitost.
K tomuto účelu se velice hodí Criteria API. Nebudu zde popisovat funkci Criteria API, k tomuto účelu slouží referenční příručka. U Criteria API se mi velice zalíbila možnost filtrovat data přes danou entitu. K tomuto účelu zde existuje objekt "Example". Při použití tohoto přístupu mohu velice snadno získat data filtrovaná podle daných sloupců (atributů) v tabulce (entitě).

Jenže....

Pokud si například řeknu: "Chci vybrat všechny záznamy s podmínkou WHERE datum BETWEEN od AND do", dostanu se opět do slepé uličky a musím přistoupit zpět k čistému Criteria API nebo dokonce k OQL.

Toto byl první důvod, proč jsem přistoupil k implementaci vlastního způsobu, který je řízen pomocí anotací. Ještě než přejdu k samotnému způsobu, musím zmínit druhou pozitivní věc.

Pokud začnu tvořit DAO vrstvu, začne se mi každý DAO objekt hemžit metodami jako: findByDatumPrijeti, findByXXX.... Jistě není nic špatného na implementaci daných metod, až na to, že metody obsahují parametry, které již jasně říkají, jaká data mají být výsledkem (návratovou hodnotou).
Uvedu malý příklad (pro pozdější srovnání):

public List findForExample(String zakaznik, Date datumOd, Date datumDo) {

// implementace Criteria API, OQL ci suroveho SQL

}


Jak je z ukázky patrné, metoda vybíra zakázky, které spadají jistému zákazníkovi a jejich datum je v rozmezi datumOd-datumDo.

K odstranění psaní samotných dotazů použiji následující kroky:

  • vytvořím objekt pro filtraci

  • daný objekt anotuji příslušnými "metadaty"

  • zruším metodu findForExample a v DAO vytvořím obecnou metodu: findByCriteria(Object filtr)



Jediné, co mi zůstane je objekt pro filtrování. Sám se svými metadaty je schopen řici, co vlastně chci. Následující ukázka je ekvivalentní předchozímu způsobu:

@Criteria(entity = Zakazky.class)
public class ZakazkyForExampleFiltr {

@Criterion(property="zakaznik.id")
private String zakaznik;

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


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

// settry, gettry
}

// pouziti
ZakazkyForExampleFiltr filtr = new ZakazkyForExampleFiltr();
// naplneni filtru
List result = dao.findByCriteria(filtr);


Pro lidi nesnášející anotace, bude tento způsob jistě nepoužitelný, pro lidi snažící se o co největší usnadnění a zpřehlednění své práce, naopak přínosem. Věřím, že každý si již ten svůj zpusob nalezl a nehodlá ho měnit, na druhou stranu při pohledu na takovýto kod si pokládám jednu otázku: "Není přece jen efektivnější popsat kod metadaty a nechat zbytek na daném frameworku?".

Jelikož se mi článek rozrostl, rozhodl jsem se ho rozdělit do dvou částí. V první části jsem popsal důvody a uvedl malou ukázku. V druhé části uvedu možnosti a nabídnu danou funkčnost ke stažení. Tedy ten základní "motor", který lze jednoduše "přilepit" do své DAO vrstvy. Navíc uvedu další plány a položím otázky, na které si zatím nedokážu odpovědět.

čtvrtek 13. prosince 2007

DAO pattern + generika pro EJB3

Když jsem se dostal do fáze většího poznávání J2EE, došlo mi, že vrstvení logiky je nedílnou součástí úspěchu při návrhu.
Jistě by nebylo moudré nechat "dolování dat" rozházené po aplikační logice tak, jak ho zrovna potřebuji. Zde nastupuje DAO (Data Access Object), což je návrhový vzor sloužící ke komunikaci mezi persistentní vrstvou a zbytkem světa.

Asi nejvíce mě zaujala možnost, kde DAO vrstvu definuji pomocí generiky a abstraktní třídy. Více než tisíc slov je lepší přímá ukázka kódu.

První věcí je interface, který obsahuje všechny důležité metody, které je třeba pro DAO jednotlivých entit třeba vystavit:
public interface GenericDAO {
public T find(ID id);
public List findAll();
public T create(T entity);
public T save(T entity);
public void remove(T entity);
}

Jednotlivé metody jsou vcelku jasné. Jedná se o základní operace jako: získej data, vytvoř, ulož, smaž. Co se týče generických typů, jedná se o entitu, které přísluší dané DAO a o typ primárního klíče. Typem primárního klíče může být jak základní datový typ jako String, Integer, tak i složený primární klíč, např: ZakazkaPK (obsahuje číslo zakázky a datum vystavení).

Nyní mohu přejít k vlastní implementaci lokálního interface:
@Local
public interface ZamestnanecDAOLocal extends GenericDAO {
}

Samotná EJB beana může vypadat následovně:
@Stateless
public class ZamestnanecDAOBean implements ZamestnanecDAOLocal {
@PersistenceContext(unitName = "myDB")
private EntityManager em;
//... implementace metod
}


Velkou nevýhodou je ovšem fakt, že pokaždé musím implementovat metody, které jsou potřeba ke splnění požadavku z GenericDAO.

Zde nastupuje abstraktní třída, která za nás vyřeší onu potřebnou implementaci.
public abstract class AbstractDAO implements GenericDAO {
private Class entityBeanType;
private EntityManager em;
public AbstractDAO() {
// vytvari, ziskava typ entity pro dane DAO
this.entityBeanType = (Class) ((ParameterizedType)
getClass().getGenericSuperclass())
.getActualTypeArguments()[0];
}

public Class getEntityBeanType() {
return entityBeanType;
}

@PersistenceContext(unitName = "default")
public void setEntityManager(EntityManager em) {
this.em = em;
}

protected EntityManager getEntityManager() {
if (em == null) {
throw new IllegalStateException("...");
}
return em;
}

public T find(ID id) {
T entity;
entity = getEntityManager().find(getEntityBeanType(), id);
return entity;
}

// ... implementace zbylych metod
}


Nyní stačí danou abstraktní třídu nechat dědit samotnou implementací EJB beany:
@Stateless
public class ZamestnanecDAOBean extends AbstractDAO implements ZamestnanecDAOLocal {
}


Takto definovaná beana je schopna podědit abstraktní třídu a například anotovaný setter pro entitymanager převzít na svá bedra. Podle specifikace je totiž možné EJB beany nechat dědit mezi sebou, s vyjímkou, že dependency injections a další služby jsou poté managovány potomkem, v našem případě "ZamestnanecDAOBean".

Aby samotné DAO nebylo tak chudé, přidal jsem si několik dalších možností. Některé jsou již specifikovány na danou implementace ORM frameworku. Osobně jsem díky tomu přešel na Hibernate Session a Criteria API.
Tady jsou některé další metody:

/**
*

Vraci vsechny zaznamy z dane tabulky, ktera prislusi entite.
* Navic umoznuje definovat razeni podle Hibernate Criteria API: Order


*

Zpusob pouziti: {@code findAll(Order.asc("jmeno"), Order.desc("prijmeni"))}


*
* @param orderBy definice razeni zaznamu
* @return vsechny zaznamy, pokud zadne neexistuji, vraci null
*/
public List findAll(Order... orderBy);

/**
*

Vraci vsechny zaznamy z dane tabulky, ktera prislusi entite.
* Navic umoznuje definovat omezeny vyber dat, pouzivane napr. pro strankovani.


*

Zpusob pouziti: {@code findAll(0, 20); } Prvni radek je roven 0


*
* @param first prvni radek, od ktereho se vybiraji zaznamy
* @param offset maximalni pocet vybiranych zaznamu
* @return vsechny zaznamy dle definovanych omezenich
*/
public List findAll(int first, int offset);

/**
*

Vraci vsechny zaznamy z dane tabulky, ktera prislusi entite.
* Navic umoznuje definovat omezeny vyber dat, pouzivane napr. pro strankovani
* a definovat razeni podle Hibernate Criteria API: Order


*

Zpusob pouziti: {@code findAll(0, 20, Order.asc("jmeno")); } Prvni radek je roven 0


*
* @param first prvni radek, od ktereho se vybiraji zaznamy
* @param offset maximalni pocet vybiranych zaznamu
* @param orderBy definice razeni zaznamu
*
* @return vsechny zaznamy dle definovanych omezenich
*/
public List findAll(int first, int offset, Order... orderBy);

/**
*

Dohledava zaznamy na zaklade Hibernate Example.


*

Spousti: {@code findByExample(exampleInstance, false, false, false, excludeProperty)}


*

Zpusob pouziti: 15.6. Example queries
*
* @param exampleInstance s naplnenymi vlastnostmi pro vyber
* @param excludeProperty pradavajici vlastnosti pro vyber pres example
* @return dohledane zaznamy
*/
public List findByExample(T exampleInstance, String... excludeProperty);

/**
*

Ziskava pocet vsech radku z tabulky prislusici dane entite


*
* @return pocet vsech radku
*/
public int countAll();

pondělí 3. prosince 2007

Seam & Hibernate Validator

Když jsem přemýšlel nad tím jak validovat (kontrolovat) data od uzivatele, napadlo mě spoustu možností jak to řešit. Jeden je ovšem velice sexy, který stojí za to zmínit.

Mám klasickou webovou aplikaci postavenou pomocí MVC (model-view-controller). Do modelu můžu nacpat entity, do kontroleru jednotlivé akce nad modelem a komunikace mezi view a business logikou. Do view klasické webové stránky.
Jelikož je Java striktně typový jazyk, nutí mě již na základě modelu definovat typ tak, aby odpovídal skutečnosti. Při vystavení modelu do persistentní vrstvy, ode mně navíc očekává popis jednotlivých atributů, podle kterého komunikuje s databází.

Ale zpět k view.
Začnu tvořit klasický formulář, který například obsahuje vlastnosti uživatele. Musím ošetřit, že login nesmí být prázdný, že musí splňovat podmínku, která říká, že použitelné znaky jsou pouze daného výčtu. Podobné vlastnosti budu kontrolovat například u hesla či jména a příjmení. Dané kritéria, které musí uživatel, při vyplňování formuláře, splnit, jsou identická s kritériemi, které musí splnit model (entita), aby byla schopna se persistovat (uložit) do databáze.
Abych se vyhnul duplicitní validaci, existuje možnost tyto věci propojit. Zde právě nastupuje Seam framework a Hibernate Validator. Nejlépe bude, pokud vše vysvětlím na malé ukázce:

Mám entitu uživatele, která může vypadat následovně:
@Entity
@Name("uzivatel")
public class Uzivatel implements Serializable {
@Id
@NotNull
@Length(min=5, max=30)
@Pattern("^\w*$", message="#{message.errorValidateLogin}")
private String login;
@NotNull
@Length(min=6)
private String heslo;
private String jmeno;
private String prijmeni;
// ... settry, gettry
}


Danými anotacemi jsem dal najevo, že model musí splnit tyto požadavky. Pokud se tak nestane, nepodaří se mi mi danou hodnotu uložit do databáze.

Tak a teď k view.
Mám v podstatě 2 možnosti, buď budu znovu psát pravidla pro validaci, nebo nechám vše na frameworku.







Při pokusu odeslání formuláře, dostanu hlášení, že jsem nesplnil daná kritéria. K tomu ale dojde ještě dříve, než se model dostane k persistentní vrstvě. To znamená, že validace se provádí hned při spuštění akce "registrace".
Jednotlivé hlášky, které posílá Hibernate Validator jsou přeložitelné, takže není problém si udělat vlastní definici, která mi jasně řekne: "Nene, máš chybu, chyby jsou... zde a zde...".

Je to jedna z vlastností Seamu, která mě pomalu nutí více používat Hibernate :)

Příjemných vlastností v Seam frameworku mohu nalézt více. Ve zkratce uvedu, pro mne, ty nejzásadnější:

  • Hot deploy - mohu provádět změny nad aplikací, bez nutnosti restartovat server či provádět redeploy aplikace

  • @In, @Out - Jednotlivé komponenty mohu zasazovat do logiky či je dále vystavovat; v ukázce je vidět, že uživatele jako komponentu přímo použiji v xhtml stránce, aniž bych musel používat nějaký controller nad modelem

  • @Scope - výčet životností komponent je mnohem bohatší, než je tomu v JSF

  • @Logger - přímá podpora logování pomocí log4j

  • pages.xml - pageflow, které umožňuje mnohem větší možnosti než je tomu u navigation-rules v JSF

  • No POST - díky Seamu, který umožňuje používat konverzaci, nemusí být view posílán přes POST

  • security - Seam obsahuje docela pěknou práci s rolemi, uživateli a vůbec s restrikcemi; navíc umožňuje ručně definovat práva a jejich chování


  • components.xml, exceptions.xml - konfigurovat se dá snad vše, ať už jsou to komponenty či zachytávání výjimek



Výčet by byl jistě mnohem bohatší. I když s daným frameworkem nemám tolik zkušenností, ihned jsem si ho oblíbil. Důvodem je možná i samotná referenční příručka, která mi odpoví téměř na všechny dotazy.