Zobrazují se příspěvky se štítkemhibernate. Zobrazit všechny příspěvky
Zobrazují se příspěvky se štítkemhibernate. Zobrazit všechny příspěvky

středa 14. ledna 2009

Hibernate Collection Of Elements a Criteria API

Navykl jsem si používat Hibernatovský CollectionOfElements ve spojení s Java 5 enumy. Collection of elements je zjednodušený vztah one-to-many, na jehož druhém konci není entita (s příslušnou tabulkou), ale jednoduchý typ jako string nebo enum.
Například mám předmět, u kterého chci mít zaznamenané jeho barvy (může mít víc barev). Barvy jsou jasně a pevně dané, není potřeba na ně mít v databázi číselníkovou tabulku.

public enum Color {
   RED, GREEN, BLUE, ...
}

@Entity
public class Item {
  @CollectionOfElements(targetElement=Color.class)
  @Enumerated(EnumType.STRING)
  protected Set<Color> color = EnumSet.noneOf(Color.class);
  ...
}

Tím pádem  mám v databázi jen "vazební" tabulku item_color se sloupečky id (primární klíč a zároveň cizí klíč do item) a element, kam jsou jako stringy serializované barvy.
Potud ideální. Pokud laskavý čtenář nehodlá používat Criteria API, může odejít spokojen. V opačném případě může zakusit něco horkých chvil a pokud nečte FAQ, tak i dost beznaděje, protože pokus o vytvoření Criteria k vybrání předmětů dané barvy skončí totiž výjimkou org.hibernate.MappingException: collection was not an association.
Ve FAQ se totiž praví, že Criteria API a Collection of Elements nelze. Ne že by samotné přečtení tohoto smutného faktu situaci vylepšilo... Na tento problém existuje záznam v Jira a dobrotivý přispěvatel mě navedl na řešení.

Vrátíme se zpět k SQL:
session.createCriteria(Item.class).add(Restrictions
  .sqlRestriction("{alias}.id in (select item_id from item_color where {alias}.id=item_id and element=?)", 
     i.name(), Hibernate.STRING));

čtvrtek 31. července 2008

Hibernate a One-to-One (ne)lazy loading

Vztahy jedna ku jedné nepoužívám příliš často (aspoň co se databázových aplikací týče), takže mě zarazila následující věc: ve výchozím nastavení totiž Hibernate po vytažení jedné entity selectem ihned dalším selectem natáhne i druhou entitu ze vztahu. To by nebylo tak zlé, horší je, že po jednom dotazu na 1000 entit provede ihned dalších 1000 dotazů na vázané entity.
K pochopení proč to tak je a jak z toho ven je potřeba udělat úkrok stranou a uvědomit si, jak funguje lazy loading (líné načítání) ve vztazích Many-to-One. V něm je entita, obsahující kolekci (set, list,...) dalších entit. Při načtení "hlavní" entity nejsou prvky kolekce ihned načteny, ale na místo kolekce vloží Hibernate svůj speciální proxy objekt, který umí podřízené entity načíst z databáze teprve až při prvním přístupu k prvkům. Důležité je, že kolekce jako taková nikdy není null -- pokud vztah na straně Many má nulový počet prvků, má kolekce velikost nula.
U vztahu One-to-One ale takové řešení není možné proto, že nejde nasimulovat hodnotu null: Hibernate nemůže vložit "svůj speciální proxy objekt" a při prvním přístupu ho načíst, protože pokud by entita v databází chyběla, není způsob, jak proxy vyměnit za null. A právě z tohoto důvodu se musí jít do databáze zjistit, jestli objekt je nebo není. (Tady bych poněkud čekal, že bude aspoň možné objekty načítat nastavením BatchSize po větších kusech než po jednom, ale to se mi nepodařilo rozchodit).
V případě, že máme opravdový vztah One-to-One (tj. obě strany jsou vždy přítomné), nejjednoduším řešením je přidat k anotaci parametr optional=false (případně v mapovacím souboru uvést constrained="true") a lazy loading bude fungovat tak, jak člověk čeká.
Some explanations on lazy loading

čtvrtek 24. ledna 2008

CGLIB a metoda equals

Další kapitola do tolik oblíbené knihy "Ze života s Hibernate".
Hibernate zhusta používá knihovnu CGLIB, která slouží ke generování tříd za běhu programu. Místo vašich vlastních objektů tak Hibernate občas vrátí tento za běhu vygenerovaný proxy objekt.
Problém může nastat, pokud v metodě equals() příslušného objektu použijeme porovnání tříd jako například v metodě, kterou vytváří Eclipse:
if (getClass() == obj.getClass()) ...
V případě proxy objektu, který původní objekt rozšiřuje, tato podmínka neplatí a metoda equals() pak nefunguje.
Staré dobré if (obj instanceof Třída) ... funguje samozřejmě bez problémů.

úterý 16. ledna 2007

Jak si nenaběhnout s Hibernate

Hibernate je skvělý nástroj, ale přes veškerou abstrakci, DAO a podobné fičury, je potřeba nepřestat myslet. Já jsem si naběhl takto: zadání je metoda, přijímající seznam identifikátorů, která má vracet seznam řekněme produktů, které mají v databázi tyto identifikátory. Nic těžkého, že?
public Produkt[] getAll(Serializable id[]) {
List<Produkt> list = new ArrayList<Produkt>(id.length);
for (Serializable i: id) {
Produkt p = (Produkt)session.get(Produkt.class, i);
if (p != null)
list.add(p);
}
return list.toArray(new Produkt[]{});
}
Při zběžném otestování funguje báječně, nicméně v produkčním nasazení se objeví problém. Pokud je identifikátorů pár stovek, ne-li tisíc, provedení se může protáhnout na pár minut. Každé get() je totiž jeden SQL select...
Správné řešení obnáši použití Query:
public Produkt[] getAll(Serializable id[]) {
List<Produkt> list = session
.createQuery("from Produkt where id in (:id)")
.setParameterList("id", Arrays.asList(id)).list();
return list.toArray(new Produkt[]{});
}

neděle 17. září 2006

Ze života Hibernate

(Původně vyšlo 17. září 2006, aktualizováno 13. února 2008)
Tenhle spot bych chtěl pojmout jako takovou symbolickou omluvu Marovi, kterému jsem tvrdil, že v Hibernate (což je řekněme nástroj pro ulehčení databázového programování v Javě) se dá stejných výsledků jako pomocí konfigurace XML soubory dosáhnout také anotacemi podle JSR-220 (EJB 3.0 persistence). Není to pravda.

Mějme příklad objednávka-produkty (položky objednávky), tedy vztah many-to-many. Z hlediska Javy ho budou tvořit dva objekty (Objednavka a Produkt), které budou mít (obousměrný) vztah many-to-many , v databázi vyjádřený tabulkami objednavka a produkt a vazební tabulkou objednavka_produkt, obsahující cizí klíče (objednavka_id a produkt_id). Potud žádný problém.
Ovšem v reálném světě je často potřeba mít ve vazební tabulce další popis vztahu, třeba počet objednaných kusů. A tady už problém je, protože takový vztah popsat Hibrenate JSR-220 anotací (@ManyToMany) neumí. Podle vývojáře Hibernate Annotations Emmanuela Bernarda tohle mělo fungovat ve verzi 3.2.1, ale nefunguje a podle jeho posledního vyjádření ani hned tak nebude.

Co s tím? Dá se to obejít samozřejmě tak, že vztah many-to-many dekomponujeme na dva vztahy many-to-one. Z vazební tabulky vyrobíme entitu (ObjednavkaPol), která bude mít složený primární klíč z cizích klíčů do tabulek objednavka a produkt (se vztahem many-to-one). Způsobů jak toho dosáhnout je víc, ale jen jediný se mi povedlo spolehlivě rozhýbat v Hibernate 3.2.5 (ve verzi 3.2.0 tomu bylo ovšem jinak :)

K vytvoření složeného primárního klíče vyrobíme novou třídu (ObjednavkaPolId), necháme ji implementovat rozhraní Serializable (s překrytím metod hashCode() a equals()) , označíme ji anotací @Embeddable a přidáme do ní dva členy typu Objednavka a Produkt, které označíme anotacemi @ManyToOne a @JoinColumn(name="xxx_id", nullable=false).

Třídu ObjednavkaPol označíme anotací @IdClass(ObjednavkaPolId.class), přidáme do ní všechny property z ObjednavkaPolId, které tvoří primární klíč, oanotujeme je jako @Id a opět jako @ManyToOne a @JoinColumn(name="xxx_id", nullable=false).

To, že máme položky klíče ve dvou třídách se na první pohled může například proti řešení s anotací @EmbeddedId zdát neohrabané, ale není to tak hrozné. Části klíče jsou rovnou součástí entity a pokud potřebujeme entitu vyhledat prostřednictvím primárního klíče, využijeme ObjednavkaPolId.