Jak mě potrápil jeden detached objekt
Známe to všichni – přijdete ráno do práce a máte za úkol naimplementovat nějaký webový formulář v aplikaci, která používá Spring Framework a Hibernate.
Už v sample aplikaci Petclinic, která je součástí distribuce Spring Frameworku je vidět jak na to:
public class EditOwnerForm extends AbstractClinicForm {
...
/** Method forms a copy of an existing Owner for editing */
protected Object formBackingObject(HttpServletRequest request) throws ServletException {
// get the Owner referred to by id in the request
return getClinic().loadOwner(RequestUtils.getRequiredIntParameter(request, "ownerId"));
}
/** Method updates an existing Owner. */
protected ModelAndView onSubmit(Object command) throws ServletException {
Owner owner = (Owner) command;
// delegate the update to the Business layer
getClinic().storeOwner(owner);
return new ModelAndView(getSuccessView(), "ownerId", owner.getId());
}
}
Zjednodušeně slovy:
Nechť je
formBacking
objekt naloadován z fasády pomocí metodyloadOwner
(a tady vzniká detached objekt). Po odeslání (a případně validaci) nechť je editovaný (stále mluvíme o našem detached objektu) uložen do databáze pomocí metodystoreOwner
.
Vše funguje skvěle až do chvíle, kdy nastane tento scénář:
- V session A naloadujete objekt A s ID např 1 (vznikne tak vzpomínaný detached objekt)
- V session B (vzniká při odeslání formuláře) dojde k inicializaci objektu A s ID 1 (tedy toho samého objektu jež právě editujete).
Hibernate vyhodí nepěknou NonUniqueObjectException
.
A dobře dělá – který z dvou objektů typu A, oba s ID
1 je ten pravý.
K tomuto scénáři může dojít zcela jednoduše a hlavně na první
pohled z nevysvětlitelných důvodů. Například díky řetězci
několika many-to-one asociací. V mém případě k tomu došlo při
zavolání metody setAsText(String text)
v jednom z property
editorů. A k tomu dochází ještě před validací a uložením
editovaném objektu.
Nejdřívě mě napadlo vyřešit to lazy loadingem many-to-one asociací. Při bližším prostudování dokumentace Hibernatu zjistíme, že asociace many-to-one nejsou defaultně lazy. Ani pomocí atributu outer-join to nezměníte. Pouze řeknete, zda se má použít join (outer-join=„true“), samostatný sql dotaz (outer-join=„false“) a nebo zda se objekt fetchne v rámci joinu nebo lazy (outer-join=„auto“ – defaultní hodnota). K lazy loadu může dojít pouze když má objekt definovanou proxy. Ovšem tohle řešení je hodně ugly…krom toho to řeše problém pouze u vazech many-to-one.
Použité řešení bylo nakonec jiné – při ukládání objektu do databáze udělat něco takového:
protected ModelAndView onSubmit(Object command) throws ServletException {
Owner owner = (Owner) command;
Owner detachedOwner = getClinic().loadOwner(owner.getId().intValue());
BeanUtils.copyProperties(detachedOwner, owner);
getClinic().storeOwner(owner);
return new ModelAndView(getSuccessView(), "ownerId", owner.getId());
}
Provedli jsme nové naloadování editovaného objektu. Pokud již Hibernate v této session má tento objekt fetchnutý zafunguje first level cache a dojde pouze k vrácení odkazu na daný objekt bez jeho druhého loadování z db. Poté zkopírujeme vlastnosti ze změněného objektu do čertvě fetchnutého. Nakonec jej uložíme.
Závěr
Článkem jsem chtěl poukázat na to, že i když nám Hibernate život hodně zjednodušuje, je pořád dost věcí, na které je potřeba dávat pozor, a které dokážou pěkně potrápit (v tomto případě 2 hodiny).
Celý článek 1 komentář 30. April 2007