2012. január 15., vasárnap

Hibernate, Eclipselink JPA vs stored procedures vs stored functions

Szóval az van (szóval nem kezdünk mondatot! hehe :P), hogy próbára tettem a tárgyban jelölt két JPA implementációt, mit alkotnak, ha tárolt eljárásokkal és/vagy függvényekkel kerülnek szembe. Próbáltam védeni a becsületüket, de nem sikerült teljes mértékben.




(jelenlegi tudásom alapján ebben a pillanatban egyik implementáció sem képes maradéktalanul / mindkét dolgot lekezelni)**

**Ez a bejegyzés írása közben megváltozott, EclipseLink 2.2.1-es verziójával megoldható, a 2.3.1-essel viszont nem (jelen pillanatban)

Függvényhívások

Első ízben az EclipseLink-et izzasztottam meg, vagy inkább Ő engem.
Függvényhívással kezdtem volna a mókát. Találtam is egy annotációt, ami ígéretesnek tűnt, de úgy döntöttem a nativeQuery-t teszem előbb próbára.

Alapvetően 3 entitás vett részt a tesztelésben, 2 függvény és 1 tárolt eljárás.

ZipCode(ZipCode, PlaceName, Shire)
OfferType(ID, OfferTypName...)
UserStatusType(ID, StatusName)

AddUpdateOfferType // SP OUT: @NewId
GetStatusTypes // UDF returns table
GetOfferTypeList // UDF returns table
A GetStatusTypes függvény natív hívása félig-meddig sikeresnek mondható, de mégsem:


A fenti kódban a createNativeQuery második paramétere egy ResultSetMapping -re utal, amit az entitásra húztam rá:

@SqlResultSetMapping(name="statusResult", entities=@EntityResult(entityClass=UserStatusType.class))

...és valójában teljesen felesleges, gyakorlatilag ugyanazt mondja, mintha a második paraméter a UserStatusType.class lenne. Az SqlResultSetMapping összetett eredménytípus esetén hasznos, mikor több entitás formájában kapjuk meg az eredményt.

Tehát a select lefut hiba nélkül. Boldogan elkezdesz rajta iterálni, majd mégis szomorú ténnyel kell szembesülnöd. A listában szereplő első entitás még teljesen okés, a második viszont már nincs feltöltve, null értékekkel van inicializálva minden property kivéve az elsődleges kulcsot (amit @Id-vel annotáltunk). Ez pusztán azért érthetetlen, mert a fenti utasítás gyakorlatilag annyit tesz, mintha egy táblából lekérdeznénk minden rekordot.

Abban az esetben, ha nem adod meg a visszatérési típust második paraméterként, az eredményként érkező listában Object tömbök lesznek, minden tömb-elem az adattábla egy-egy mezőjének az értéke.

org.apache.jasper.JasperException: An exception occurred processing JSP page /WEB-INF/jsp/index.jsp at line 6

3: status name by id: ${ust.statusName}<br /><br />
4: status names by UDF:
5: <c:forEach items="${ustl}" var="s">
6:     ${s.id}:${s.statusName},
7: </c:forEach>
8: <br /><br />
9: offer type id: ${ot.id}<br />

[...]

java.lang.NumberFormatException: For input string: "id"
    java.lang.NumberFormatException.forInputString(NumberFormatException.java:48)


Ebben az esetben persze el lehet érni az értékeket ha a fentit úgy módosítjuk pl.: ${s[0]}
Érdekes, hogy így minden listaelem a megfelelő értékeket tartalmazza. Ugyanez miért nem sikerül ORM módjára?

Ugyanez a történet Hibernate esetén úgy működik, ahogy az a nagydoksiban meg van írva.

Mint fentebb írtam, találtam egy ígéretesnek tűnő annotációt. Most jöhetne az a rész, hogy "igen, ez az, megvan a megoldás", de nem. Nincs. Ugyanis az említett annotáció nem képes table valued stored function lekezelésére.

Mivel ez gyakorlatilag egy névvel ellátot lekérdezés, a fenti nativeQuery-n egy apróbb módosítás hajtunk végre:

@SuppressWarnings("unchecked")
 public List<UserStatusType> getEntities() {
  return em.createNamedQuery("getStatusTypes", UserStatusType.class).getResultList();
 }

Majd felannotáljuk a UserStatusType entitásunkat:

@NamedStoredFunctionQuery(
 name="getStatusTypes",
 functionName="GetStatusTypes",
 resultSetMapping="statusResult",
 returnParameter=@StoredProcedureParameter(direction=Direction.OUT, queryParameter="p1")
)

Két dolgot vehetünk észre:
1.) a createNamedQuery továbbra is megkapja második paraméterként a visszatérési típust, annak ellenére, hogy az annotációban is egyértelműen meghatároztuk azt. Ennek a korábban említett Object tömb probléma az oka.
2.) Ebben az esetben a resultSetMapping már fontos, ugyanis kötelező adat.

Eredményként a már előrevetített hibát kapjuk:
org.springframework.web.util.NestedServletException: Request processing failed; nested exception is Exception [EclipseLink-4002] (Eclipse Persistence Services - 2.3.2.v20111125-r10461): org.eclipse.persistence.exceptions.DatabaseException Internal Exception: java.sql.SQLException: The request for procedure 'GetStatusTypes' failed because 'GetStatusTypes' is a table valued function object.

Kis google nyaggatás után arra kellett rájönnöm, hogy függvények hívására a NamedPLSQLStoredFunctionQuery annotációt szokták használni (Oracle-höz mindenképp). Elkezdtem foglalkozni a témával, minek során a következők derültek ki.:

Továbbra is szükségünk van a resultSetMapping-re. Ezen felül definiálnunk kell egy adatstruktúrát, amit adatbázis szinten használ majd a rendszer, gyakorlatilag ennek segítségével állítja össze az eredményben szereplő rekordokat, és ezeknek a listájával tér vissza. A probléma csak az, hogy ezt az objektumot létre kéne hozni adatbázis szinten is, amit a google-ban történő keresés eredményeképpen kidobott első 3 oldal megtekintése után nem állt szándékomban tovább erőltetni MsSQL esetén. Az sem derült ki számomra, hogy lehet-e benne. Oracle-ben no para, nade ez a mikrópuha ez nem az én világom :)

@NamedPLSQLStoredFunctionQuery(
 name="getStatusTypes",
 functionName="GetStatusTypes",
 resultSetMapping="statusResult",
 returnParameter=@PLSQLParameter(name="res", databaseType="UST_REC", direction=Direction.OUT)
)
@PLSQLRecord(name="UST_REC", compatibleType="UST_TYPE", javaType=UserStatusType.class,
 fields = { @PLSQLParameter(name="ID"), @PLSQLParameter(name="StatusName") }
)
@Struct(name="UST_TYPE", fields={"ID", "StatusName"})
@SqlResultSetMapping(name="statusResult", entities=@EntityResult(entityClass=UserStatusType.class))

Továbbá ha az ember vet egy pillantást a fenti definicióra, nem nehéz belátni, hogy ez egy totálisan függvényekkel operáló alkalmazás esetén tarthatatlan. Nem akarunk mi 10-20 sort gépelni csak azért, hogy egy nyamvadt UDF-et meghívjunk, és tán még eredményt is kapjunk belőle.

Mikor idáig jutottam kezdtem el gondolkodni rajta, hogy mi a fene ez, hogy nativeQuery esetén nem tölti fel az entitásunkat EclipseLink. Kipróbáltam, természetesen szimpla select * from tbl lekérdezés esetén is ezzel az anomáliával kerültem szembe.

Fogtam magam, és a korábban feleslegesnek titulált resultSetMapping-et egy kicsit megokosítottam:

@SqlResultSetMapping(
 name="statusResult",
 entities=@EntityResult(
  entityClass=UserStatusType.class,
  fields={
   @FieldResult(name="id", column="ID"),
   @FieldResult(name="statusName", column="StatusName")
  }
 )
)

Ennek hatására a nativeQuery helyes eredményt adott vissza, ami elgondolkodtató. Nem kéne ezt megoldania a JPA-nak, ahogy az Hibernate esetén meg is történik? EclipseLink bug vagy hiányosság? Érdekes, hogy nem találtam erre a problémára utaló bejegyzést sehol a neten, illetőleg elég régóta használok EclipseLinket, és még nem volt ilyen problémám.

Ehhez a projekthez a 2.3.2-es verziójú EclipseLink-et használtam. Letöltöttem a 2.2.1-et, lecseréltem a libeket, és láss csodát... Amivel szivattam magam 2 napig, az tökéletesen működik. Nem kell resultSetMapping sem, sima nativeQuery megoldja amit meg kell.

Micsoda fintor, hogy ehhez a bejegyzéshez egy Eclipse bugreport is fűződik :)

Az eddig leírt litánia tehát a függvényhívásokról szól, amit gyakorlatilag annyival is elintézhettem volna, hogy Hibernate esetén jól működik, EclipseLink esetén pedig használjunk 2.2.1-es verziót, mert a 2.3.2-ben bugos a nativeQuery.

Tárolt eljárások

A tárolt eljárásoknak is entitásokkal ugrottam neki EclipseLink esetén, és szerencsére ezzel nem is volt semmilyen szívás. Az alábbit az entitásunkra kell ráhúznunk:

@NamedStoredProcedureQueries({
 @NamedStoredProcedureQuery(
  name="addUpdateOfferType",
  procedureName="AddUpdateOfferType",
  returnsResultSet=false,
  parameters={
    @StoredProcedureParameter(queryParameter="p1", name="ID",
     direction=Direction.IN, type=Integer.class),
    @StoredProcedureParameter(queryParameter="p2", name="OfferTypeName",
     direction=Direction.IN, type=String.class),
    @StoredProcedureParameter(queryParameter="p3", name="DailyWorkHours",
     direction=Direction.IN, type=Integer.class),
    @StoredProcedureParameter(queryParameter="p4", name="ProfessionalPractice",
     direction=Direction.IN, type=Integer.class),
    @StoredProcedureParameter(queryParameter="p5", name="IsDeleted",
     direction=Direction.IN, type=Boolean.class),
    @StoredProcedureParameter(queryParameter="p6", name="UserId",
     direction=Direction.IN, type=Integer.class),
    @StoredProcedureParameter(queryParameter="p0", name="NewId",
     direction=Direction.OUT, type=Integer.class)
     }
   )
})

Ebben maximum annyi kivetnivaló lehet, hogy minden eljáráshoz meg kell írnunk a fenti kódot, majd mikor meghívjuk azt, tudnunk kell, hogy hogy neveztük el a paramétereket és melyik-melyik. Természetesen egységes névkonvenciók alkalmazása mellett egyszerűsíthetjük az életet. Ezen felül ha a lekérdezés megírásánál figyeled a felannotált entitást is, akkor könnyebb a helyzet. Ennél jobban amúgy sem tudom, hogy lehet-e könnyíteni ezen a dolgon bármilyen technológiát is használjunk.

Ez után az eljárás hívása már egyszerű:

@Transactional
 public OfferType createUpdateOfferType(OfferType offerType) {
  Integer newId = null;
  OfferType newOfferType = null;
  
  Query q = em.createNamedQuery("addUpdateOfferType");
  
  newId = (Integer)q.setParameter("p1", offerType.getId() == null ? 0 : offerType.getId())
     .setParameter("p2", offerType.getOfferTypeName())
     .setParameter("p3", offerType.getDailyWorkHours())
     .setParameter("p4", offerType.getProfessionalPractice())
     .setParameter("p5", offerType.getIsDeleted())
     .setParameter("p6", offerType.getCreatedBy())
     .getSingleResult();
  
  if (newId != null) {
   newOfferType = find(OfferType.class, newId);
  } else {
   throw new IllegalStateException();
  }
  
  return newOfferType;
 }

Hibernate API doksijában sajnos meg vagyon írva, hogy nativeQuery-vel ugyan futtathatsz tárolt eljárásokat, de OUT paramétereket nem tud kezelni a rendszer. Kipróbáltam, úgy tűnik tényleg így van.

Összességében tehát ha tárolt eljárásokat és függvényeket egyaránt szeretnénk kezelni teljes szolgáltatási skálájukat kihasználva, jobb választás az EclipseLink.

Amennyiben csak függvényekkel kell operálnunk, megteszi a Hibernate is.

Nincsenek megjegyzések:

Megjegyzés küldése