Az előző poszthoz kerestem dolgokat a neten, és találtam egy nagyon jó kis leírást az optimistic és a pessimistic lock-ról (miközben olvasgattam, eszembe jutott, hogy ez az egyik kedvenc kérdés az állásinterjúkon :) ).  Úgy gondoltam megpróbálom összefoglalni magyarul is a dolgot (az eredeti jobb egyébként).

1. Ütközések (collisions) típusai

A tranzakció izolációs szinteknél már volt szó róluk, ezért csak felsorolnám őket:

  • Dirty read
  • Non-repeatable read
  • Phantom read

2. A lock-olás

A lock más userek/alkalmazások hozzáférését limitálja egy adatbázis entity-hez (rekordhoz, táblához, page-hez, …). Két típusa van:

  • Write lock: Ebben az esetben a lockoló user vagy alkalmazás szeretné updatelni/törölni az adott entity-t, ezért nem enged read/update/delete hozzáférést senki másnak.
  • Read lock: Itt a lock holder csak olvassa az adott entity-t, nem fogja módosítani. Ezért mások is olvashatják az adott entity-t, de senki nem updatelheti vagy törölheti azt.

3. Pessimistic lock

Ebben az esetben az entity lockolva lesz egészen addíg, amíg az alkalmazás memóriájában van (általában objektumként). Tehát amikor az alkalmazásnak szüksége van pl.: egy user objectre, akkor az alkalmazás lockolja az adott user rekordját a DB-ben. A lock mindaddig megmarad, amíg a user object az alkalmazás memóriájában van, és az alkalmazás műveleteket végezhet vele. Így nem lehet ütközésünk.

4. Optimistic lock

Az optimistic lock-nál előzetesen azt feltételezzük, hogy nem lesz ütközés. Tehát az alkalmazás beolvassa a memóriába az objectet, de nem lockolja amíg commitolni nem akar. A commit előtt megnézi hogy az adott record változott-e, és ha kiderül hogy igen, tehát ütközés van, akkor ezt a helyzetet a kódban kezelnünk kell (csak logolja, kísérletet tesz az ütközés feloldására, …). Ábra (based on http://www.agiledata.org/essays/concurrencyControl.html):
Alapvetően 2 módszer van az ütközések meghatározására:
  1. Egyedi azonosító: A rekordot egyedi azonosítóval látjuk el – A forrás rekordnak lesz egy egyedi azonosítója, amely minden updatekor változik.
    • Timestamp: A DB-nek kell updatelnie, mert mi kódból nem tudjuk megfelelően…
    • Incremental counter: Növeljünk egy számot…
    • User Id: Ha az ütközések csak különböző userek tranzakcióikor fordulhatnak elő.
    • Generated UUID: Minden updatekor új egyedi azonosítót kap.
  2. Összehasonlítós módszer: Újra lekérjük az adott objectet a DB-től és összehasonlítjuk a property-jeit az eredetileg lekérttel.

5. Overly optimistic lock

Abban az esetben ha biztosan tudjuk, hogy soha nem lesznek ütközések (pl mert csak 1 user/alkalmazás használja a DB-t vagy csak olvasható tábláink vannak a DB-ben), akkor egyáltalán nem kell foglalkoznuk sem az ütközések detektálásával sem a kezelésével.

1. Tranzakció kezelés

Akkor lehet rá szükség ha több logikailag öszefüggő SQL utasítást szeretnénk egyszerre (közvetlenül egymás után, egy tranzakcióként) végrehajtani. Ezeknek az SQL utasításoknak általában van egy logikai sorrendje. Ha bármelyik SQL utasítás bukik akkor a logikailag ráépülő többi eljárás is bukik. Például:

  • Több oldalas regisztráció, ahol minden oldalhoz tartozó adat külön SQL utasítással bekerül az adatbázisba. Ha az x. oldal után a felhasználó nyom egy cancel-t, akkor az eddig végrehajtott SQL insert-eket is érvénytelenítenünk kell
  • Banki tranzakciók – átutalás egy számláról egy másikra, vagy kézpénzfelvét az autómatából: Először megterhelődik az egyik számla a kívánt összeggel, ezután jóváíródik a másik számlán, vagy kiadja az autómata. Ha a második lépés meghiúsul (pl.: Az autómata technikai okok miatt nem tud pénzt kiadni) akkor az első tranzakciót is vissza kell vonni.

Ezekben az esetekben az SQL utasításokat egy tranzakcióban kezeljük. Ez a teljes tranzakció csak akkor hajtódik végre, ha mi erre a programunkból direkt felszólítjuk, egészen addig az adatbázis-kezelő elkülönítve kezeli őket. A tranzakció közben bármikor lehetőség van arra, hogy az addig kiadott SQL utasításokat érvénytelenítsük, ezzel az adatbázis visszaáll az eredeti állapotához.

A tranzakciók kezelésének módját a Connection object-en a setAutoCommit() metódussal tudjuk megváltoztatni:

  • connection.setAutoCommit(true): Az új adatbázis kapcsolatok esetén ez a default beállítás. Ha az AutoCommit mód be van kapcsolva akkor minden SQL utasítás különálló tranzakcióként lesz kezelve és végrahajtva.
  • connection.setAutoCommit(false): Ebben az esetben (AutoCommit mód kikapcsolva), az SQL utasítások csoportokba rendezhetőek, és csak akkor hajtódnak végre/fejeződnek be, ha a connection-ön meghívjuk a commit() vagy rollback() metódusokat. A commit() hatására a módosítások véglegesen bekerülnek az adatbázisba, míg a rollback() visszaállítja az adatbázist a tranzakció kezdetekori állapotába.

2. Tranzakció izolációs szintek

Azt határozzák meg, hogy hogyan kezelje a szerver az egyidejű hozzáférési kérelmeket (read, update, insert) ugyanahhoz az objektumhoz (tábla, rekord, …).

  1. TRANSACTION_NONE: A tranzakciók nem támogatottak, nincs tranzakciókezelés.
  2. TRANSACTION_READ_UNCOMMITED: Olvasáskor mindig az aktuális (módosított) értéket kapjuk, még akkor is, ha az adott insert/update tranzakciót a kezdeményező nem commit-olta. A következő problémák léphetnek fel:
    • Dirty reads: A kiolvasott rekord változhat más tranzakciók által a mi tranzakciónk ideje alatt. Tehát előfordulhat, hogy a tranzakciónk elején és végén az adott rekordban más értékek szerepelnek, holott mi nem is változtattuk, csak olvastuk…
    • Non-repeatable reads: A tábla egy adott sora törlődhet a mi tranzakciónk közben. Így amikor mi a tranzakciónk elején és végén futtatunk egy-egy select-et, akkor a második esetben hiányozhatnak sorok az eredményből.
    • Phantom reads: Hasonló az előzőhöz, csak insert-tel. A táblába új sor (fantom sor) kerülhet egy másik tranzakció által miközben a mi tranzakciónk még fut. Így a tranzakciónk végén több sor lesz az adott táblábam, mint amivel számolhattunk az elején…
  3. TRANSACTION_READ_COMMITED: Olvasáskor mindig az adott rekord véglegesített értéket kapjuk. Ez az esetek 99%-ra használható, a tranzakcióink mindig csak olyan rekordokat olvasnak, amik kommittálva vannak, azaz nincs nyitott tranzakció, ami dolgozna rajtuk. A baj ezzel az, hogy ha sokan írják és olvassák az adott rekordot vagy táblát akkor könnyen kialakulhat az a helyzet, hogy az olvasó tranzakciók arra várnak hogy az írás (pl egy nagy tábla update-je) befejeződjön. Ez a főleg a rekordokra vonatkozik így csak a dirty read-től véd, probléma lehet:
    • a non-repeatable read
    • és a phantom read
  4. TRANSACTION_REPEATABLE_READ: Ez annyival jobb a READ_COMMITTED-nél, hogy már a non-repeatable read hibát is képes kiszűrni a tranzakcióból. Egyszerűbben: csak a recordok commit-olt értékeit használja és a recordok tranzakció közbeni törlése nem befolyásolja a select-eket.  Ebben az esetben csak egy probléma marad:
    • a phantom read
  5. TRANSACTION_SERIALIZABLE: Annyival több a REPEATABLE_READ-től, hogy más tranzakció nem írhatja felül a mi tranzakciónk által olvasott értékeket, azaz addig várakoztatja azokat míg be nem fejeződik a tranzakciónk. Így nem fognak tranzakció közben fantom sorok keletkezni a táblában. Itt elég problémás lehet, ha több résztvevő folyamatosan olvas egy táblát, amíg az updatelő szál várakozik, mert a tábla lock-olva van és nem tud bele írni…

Ezek szintek föntről lefelé egyre költségesebbek lesznek az adatbázisnak, így a tranzakciók végrehajtási sebessége lassul. Az izolációs szinteket a Connection objektumon a  setTransactionIsolation() metódussal állíthatjuk be a tranzakció megkezdése előtt. Például:

con.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ);

Ez az újabb megoldás WS Client készítésére C#-ban. A régi “Web Reference” megoldás megtalálható itt. A .NET 3.0-ás verziójában már Service Reference-eket tudunk hozzáadni a project-hez. A lényeg, hogy itt egyszerűbb lesz a SOAP Header-t módosítani az authentikáció érdekében. Egy nagyon jó leírás a Web reference és a Service Reference közötti különbségről: http://andrewtokeley.net/archive/2008/07/10/the-difference-between-ldquoadd-web-referencerdquo-and-ldquoadd-service-referencerdquo.aspx

1. Készítsünk egy új projectet a Visual Studióban.

Legyen ez egy egyszerű Konzolos alkalmazás:

2. Adjuk hozzá a Service Reference-t a Projectünkhöz:

Ekkor a Solution Explorerben megjelenik a behivatkozott Service.

3. A kliens létrehozása:

Az autómatikusan legenerált Program.cs-t egésszítsük ki a következőképpen:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using ServiceReferenceTest.AtlasServiceReference;
namespace ServiceReferenceTest
{
class Program
{
static void Main(string[] args)
{
ATLASWServicePortClient client = new ATLASWServicePortClient();
AWSProduct[] products = client.findAllProducts();
foreach(AWSProduct product in products) {
Console.Out.WriteLine("-> " + product.wkProductId);
}
Console.In.ReadLine();
}
}
}

Ez csak egy teszt a WS methodok meghívására. Ez így már működne is, ha nem kérne a WS Server SoapHeader authentikációt. Viszont a mi szerverünk a következő formátumú Soap Header-t kér:

<soap:Header>
<LoginCredentials>
<userid>WebService_User</userid>
<password>WebService_Password</password>
<groupid>WebService_Group</groupid>
</LoginCredentials>
</soap:Header>

4. A LoginCredentials class

Csinálnunk kell egy Data class-t a login credentials -hoz. Legyen benne userid, password és groupid property (Ezek szükségesek a header-be). Ez a LoginCredentials class származzon a SoapHeader ősosztályból (Ahhoz, hogy ez így működjön, hozzá kell adnunk a System.Web.Services referenciát). Vigyázni! A LoginCredentials class legyen publikus, különben nem lehet serializálni.

public class LoginCredentials : SoapHeader
{
private string useridField;
...
public LoginCredentials() { }
...
[System.Xml.Serialization.SoapElementAttribute(IsNullable = false)]
public string userid
{
get { return this.useridField; }
set { this.useridField = value; }
}
...
}

5. A “behavior” class

Egy jó leírás a következő linken: http://weblogs.asp.net/jdanforth/archive/2008/10/27/wcf-client-calling-asmx-service-with-soap-headers.aspx.

Kell csinálnunk egy ServiceBehavior classt, amivel manipulálni tudjuk a kimenő SOAP messageket. Ehhez a következők kellenek:

Példa:

public class SoapHeaderBehaviour : BehaviorExtensionElement, IClientMessageInspector, IEndpointBehavior
{
#region IClientMessageInspector Members
public void AfterReceiveReply(ref Message reply, object correlationState)
{
}
public object BeforeSendRequest(ref Message request, IClientChannel channel)
{
LoginCredentials login = new LoginCredentials("username", "password", "group");
MessageHeader<LoginCredentials> messageHeader = new MessageHeader<LoginCredentials>() { Actor = "Anyone", Content = login };
request.Headers.Add(messageHeader.GetUntypedHeader("LoginCredentials", "atlas"));
return null;
}
#endregion
#region IEndpointBehavior Members
public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters) { }
public void ApplyClientBehavior(ServiceEndpoint serviceEndpoint, ClientRuntime behavior) { behavior.MessageInspectors.Add(this); }
public void ApplyDispatchBehavior(ServiceEndpoint serviceEndpoint, EndpointDispatcher endpointDispatcher) { }
public void Validate(ServiceEndpoint serviceEndpoint) { }
#endregion
#region BehaviorExtensionElement Members
protected override object CreateBehavior() { return new AtlasSoapHeaderBehaviour(); }
public override Type BehaviorType { get { return GetType(); } }
#endregion
}

6. Az app.config: EndPoitBehavior beállítása:

Ha megvan a “behavior” class-unk, akkor már csak be kell állítanunk az app config-ban, hogy az alkalmazás használja is ezt az osztályt:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.serviceModel>
<bindings>
...
</bindings>
<behaviors>
<endpointBehaviors>
<behavior name="MyEndpointBehaviors">
<ClientSoapHeaderAdderBehavior />
</behavior>
</endpointBehaviors>
</behaviors>
<extensions>
<behaviorExtensions>
<add name="ClientSoapHeaderAdderBehavior" type="ServiceReferenceTest.SoapHeaderBehaviour, ServiceReferenceTest, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
</behaviorExtensions>
</extensions>
<client>
<endpoint address="http://servername:80/remote_ws/AWService" behaviorConfiguration="MyEndpointBehaviors" binding="basicHttpBinding" bindingConfiguration="AWServicePort" contract="ServiceReference.AWServicePort" name="AWServicePort" />
</client>
</system.serviceModel>
</configuration>

Mostmár futtathatjuk is az alkalmazást. A kliens által elküldött SOAP message-eknek mostmár lesz header-je és a header tartalmazni fogja a credentials-t.

Property validálás Java-ban

2010. Március 10.

Az egész ott kezdődött, hogy a Kanadaiak kedvéért kénytelen voltam megnézni hogyan validálja az Amerikaiak indiaiakkal íratott csodarendszere a first/last name property-ket. És kicsit megrémültem:

Rossz példa, ne használd!

private static Vector getFirstLastNameAlphaChars(Vector inputvectors){
for(int i=65; i < 91; i++){
char[] charbuff = new char[1];
char ch = (char) i;
charbuff[0] = ch;
String letters = new String(charbuff);
inputvectors.add(letters);
}
for(int i=97; i < 123; i++){
char[] charbuff = new char[1];
char ch = (char) i;
charbuff[0] = ch;
String letters = new String(charbuff);
inputvectors.add(letters);
}
String exStr = new String(new char[]{'!'});
inputvectors.add(exStr);
String aposStr = new String(new char[]{'\''});
inputvectors.add(aposStr);
String dotStr = new String(new char[]{'.'});
inputvectors.add(dotStr);
String plusStr = new String(new char[]{'+'});
inputvectors.add(plusStr);
String hypenStr = new String(new char[]{'-'});
inputvectors.add(hypenStr);
String underScoreStr = new String(new char[]{'_'});
inputvectors.add(underScoreStr);
String apos1Str = new String(new char[]{'`'});
inputvectors.add(apos1Str);
String atStr = new String(new char[]{'@'});
inputvectors.add(atStr);
String spaceStr = new String(new char[]{' '});
inputvectors.add(spaceStr);
return inputvectors;
}
public static boolean validateString(String inputString) {
boolean result = true;
Vector allowedchars = getFirstLastNameAlphaChars(new Vector());
allowedchars = getNumericChars(allowedchars);
if(inputString!=null && inputString.trim()!=null)
{
inputString = inputString.trim();
for(int i = 0; i < inputString.length(); i++)
{
char letters = inputString.charAt(i);
char[] charbuff = {letters};
if(!allowedchars.contains(new String(charbuff)))
{
result = false;
break;
}
}
}
return result;
}

Nem is részletezném a hibákat, amik első ránézésre látszanak. Az hogy ez a kód lassú, az egy dolog. De! Ha engedélyezni kellene egy új karaktert, akkor rögtön kód módosítás + új release kell. Ami sok idő.

Aztán felmerült bennem hogyan is kellene ezt megoldani gyorsabban.

1. Property validálás Regexp-pel:

Egy jó leírás a regexpekről: http://www.regular-expressions.info/java.html

public class RegExpValidation {
private static String disabledCharacters = "[^A-Za-z0-9!.+-_'`@ ]+";
private static Pattern pattern = Pattern.compile(disabledCharacters);
private static boolean result = true;
public static boolean validateString(String inputString) {
result = true;
Matcher matcher = pattern.matcher(inputString);
if (matcher.find()) {
result = false;
}
return result;
}
}

2. OVal (Object Validation) Framework:

Nagyon jó kis cucc, a Java objectek property-jeire és a method-ok argumentumaira tudunk annotációkban feltételeket tenni. Itt találhatunk bővebb leírást róla. Csak annyit kell tenni, hogy az OVal jar file-ját felvesszük a projectre. Rögtön két lehetséges megoldást találtam a fenti problémára:

a.) Business Object Property validálása:

Kell egy Business Object, amit annotációkkal preparálunk:

public class BusinessObject {
@MatchPattern(pattern="[A-Za-z0-9!.+-_'`@ ]+")
private String inputString;
public BusinessObject(String inputString) {
this.inputString = inputString;
}
}

Majd amikor ezt létrehozzuk egy másik class-ban, a következő képpen tudjuk validálni:

public boolean validateString(String inputString) {
result = true;
bo = new BusinessObject(inputString);
violations = validator.validate(bo);
if (!violations.isEmpty()) {
result = false;
}
return result;
}

b.) Method Argumentum validálása:

Egy kicsit bonyolultabb a helyzet ha a methodok argumentumait szeretnénk validálni. Ehhez az Eclips-es projectet kicsit preparálnunk kell. Ehhez nem árt elolvasnunk a következő leírást: http://oval.sourceforge.net/userguide.html#d4e183 (Telepítsük az AspectJ plugin-t, Konvertáljuk a projectet AspectJ projectre, hozzunk létre egy új Aspect-et a projecten, amely a net.sf.oval.guard.GuardAspect -ből származik. )

A validálni kívánt methodunk a következőképpen néz ki:

public void validateStringDirectly(@MatchPattern(pattern="[A-Za-z0-9!.+-_'`@ ]+") String inputString) {
}

Ha a project megfelelően van preparálva, akkor ha ezt a method-ot egy invalid (pl.: “ááá”) stringgel hívjuk meg, akkor ConstraintsViolatedException-t fog dobni a method.

3. Statisztikák:

14 db különböző (valid/invalid vegyesen) string-et validáltam mind a 4 megoldással 10.000-szer egymás után. Az idők ms-ben a következők lettek:

  • Bad Solution: 2000 ms
  • RegExp validation: 312 ms
  • OVal property validation: 1844 ms
  • OVal method argument validation: 1813 ms
Tehát a papírforma bejött. A RegExp a leggyorsabb.

4. Miért lehet mégis jó választás az OVal:

Egy nagyon jó leírás található az OVal használatáról a következő blogon: http://jee-bpel-soa.blogspot.com/2008/12/jsr-303-and-oval-validation-framework.html
A validációs hibákat nagyon jól tudjuk kezelni, mivel minden feltételre különböző error code-okat tudunk definiálni:

@MinLength(value=2, errorCode = "Customer.lastName.error.minLength")
@MaxLength(value=20, errorCode = "Customer.lastName.error.maxLength")
private String lastName;

Természetesen az OVal esetében is van lehetőség kölső XML fileban definiálni a validációs szabályokat, így elég konfigurálhatóvá válik a validálás…

1. Feladat:

Az ügyfél csodálatos alkalmazása Back Office adatok szinkronizálásra biztosít tárolt eljárásokat. (Eredetileg úgy wolt, hogy a WS getter metódusai szarok, hibásak és lassúak voltak. Erre kitalálták, hogy úr isten, itt nagyon nagy mennyiségű adat van, meg hogy milyen nehéz már a WS-t kijavítani, és mennyivel egyszerűbb írni néhány tárolt eljárást. A business unit érje el közvetlenül a DB-t, és hívja meg az erre gyártott tárolt eljárásokat. Advanced Architecture!) A verzióváltás miatt ezeket is le akartam tesztelni. Még a végén nem működik az új rendszeren valamelyik, és a BU jön nekem sírni, hogy de miért…

2. A tárolt eljárás

A tárolt eljárás egy kicsit egyszerűsítve következőképpen van létrehozva:

CREATE PROCEDURE [bosuser].Bos_GetData @RequestType VARCHAR(32), @RequestFlag VARCHAR(5) OUTPUT
AS
BEGIN
DECLARE @xmlOut XML
IF @RequestType='Accounts'
EXEC Bos_GetAllAccounts @RequestType, @xmlOut OUTPUT, @RequestFlag OUTPUT
ELSE IF @RequestType='Groups'
EXEC Bos_GetAllGroups @RequestType, @xmlOut OUTPUT, @RequestFlag OUTPUT
ELSE IF @RequestType='Users'
EXEC Bos_GetAllUsers @RequestType, @xmlOut OUTPUT, @RequestFlag OUTPUT
ELSE
RAISERROR('Invalid RequestType %s.',16,1,@RequestType)
SELECT @xmlOut AS 'XmlData'
END
GO

Ahogy látható, a tárolt eljárásunk belső tárolt eljárásokat hív meg. A paraméterei:

  • “RequestType” input paraméter: Melyik belső SP-t hívja meg.
  • “RequestFlag” in/out paramétere: Pager megoldás. Ha több mint X record van amit vissza kellene adni, akkor visszaadja az első X-et, és a “RequestFlag” false lesz. Ilyenkor akkor újra meg kell hívni az SP-nket az előzőleg használt “RequestType”-pal és erre visszatér a következő X recorddal.
  • XML visszatérési érték.

3. C# megoldás

Először lefuttattam azt a C# kódot, amit még az előző verzióhoz csináltam a business unitnak. (Igen, kicsit kézi vezérlésűek, meg kell nekik mutatni, hogy hogyan kell csinálni.) A pagerrel (“RequestFlag”) most nem foglalkozunk. Ez a következőképpen néz ki:

string connect = System.Configuration.ConfigurationManager.AppSettings["connection"];
SqlConnection sqlConnection = new SqlConnection(connect);
sqlConnection.Open();
SqlCommand sqlCommand = new SqlCommand("Bos_GetAtlasData", sqlConnection);
sqlCommand.CommandType = CommandType.StoredProcedure;
sqlCommand.Parameters.Add("@RequestType", SqlDbType.VarChar, 32);
sqlCommand.Parameters["@RequestType"].Value = requestType;
sqlCommand.Parameters.Add("@RequestFlag", SqlDbType.VarChar, 5);
sqlCommand.Parameters["@RequestFlag"].Direction = ParameterDirection.Output;
XmlReader xmlReader = sqlCommand.ExecuteXmlReader();
xmlDocument.Load(xmlReader);
xmlReader.Close();
if ("FALSE".Equals(sqlCommand.Parameters["@RequestFlag"].Value.ToString()))
{
// Skip this case
}
sqlConnection.Close();

Ne felejtsük el az App.config-ba betenni a DB connection részleteit:

<appSettings>
<add key="connection" value="Data Source='server.db.int, 1302';Initial Catalog=db;User ID=user;Password=password" />
</appSettings>

Ha lefuttatjuk pl.: “Users” requestType-pal, akkor az XmlDocument-ünk tartalmazni fogja a usereket.

4. A Java megoldás

Kis naívan azt hittem, hogy a Connection-ön meghívom a prepareCall() metódust ami ugye létrehozza nekem a CallableStatementet, ezen beállítom a paramétereit, hívok rajta egy execute()-ot, és a ResultSet-ből kiolvasom a végeredményt. Csak hogy ez így nem működik. Futottam vele néhány kört, mire debuggolás közben rájöttem, hogy ha sikerül feltartanom a programot az execute és a ResultSet processzálása között, akkor a ResultSet-ben feltűnik egy “XmlData” property amiben benne van a várt eredmény. Megpróbáltam az MS jdbc drivere helyett a JTDS jdbc drivert is használni, de azzal is hasonló lett az eredmény.

A következző thread segített megérteni mi is a probléma: http://dbaspot.com/forums/ms-sqlserver/223916-problem-boolean-return-parameter-callablestatement-execute.html (Thanks to Joe Weinstein @ BEA Systems).

A mi esetünkben a példa Java kód a következő:

CallableStatement callableStatement = null;
ResultSet resultSet = null;
Connection con = null;
try {
con = getConnection();
String sql = "{call Bos_GetData(?, ?)}";
callableStatement = con.prepareCall(sql);
callableStatement.setString(1, requestType);
callableStatement.registerOutParameter(2, java.sql.Types.VARCHAR);
boolean getResultSetNow = callableStatement.execute();
int updateCount = -1;
DocumentBuilderFactory dbfac = DocumentBuilderFactory.newInstance();
DocumentBuilder docBuilder = dbfac.newDocumentBuilder();
Document doc = null;
while (true) { // handle all in-line results from any procedure
if (getResultSetNow) {
resultSet = callableStatement.getResultSet();
while (resultSet.next()) {
InputStream is = resultSet.getAsciiStream(1);
doc = docBuilder.parse(is);
}
resultSet.close();
} else {
updateCount = callableStatement.getUpdateCount();
// if (updateCount != -1) {
// process update count if desired
// }
}
if ((!getResultSetNow) && (updateCount == -1))
break; // done with loop
getResultSetNow = callableStatement.getMoreResults();
// go to next return if a Callable statement, get any output
// parameters after the loop.
}
boolean hasAllElementRetrived = Boolean.parseBoolean(callableStatement.getString(2));
callableStatement.close();
return new AtlasDataResult(hasAllElementRetrived, doc);
} catch (Exception e) {
log.error(e);
throw new RuntimeException(e);
} finally {
if (con != null) {
try { con.close(); } catch (SQLException e) {
log.error(e); }
}
}

Némi magyarázat: Azért van erre az egészre szükség, mert a tárolt eljárás végrehajtása elég sokáig is eltarthat. A CallableStatement getMoreResults() metódusa true-val tér vissza, ha a következő result egy ResultSet object. Az getUpdateCount() -1 -gyel tér vissza, ha befejeződött a tárolt eljárás. Tehát a kilépési feltételünk: nincs több result (a getMoreResult() false-sal tért vissza), és nincs is több updatelendő sora az SP-nek (a getUpdateCount() -1-et adott.)

A szép az egészben hogy a coding standards alapján ezért a while(true)/break megoldásért minimum nyilvánosan kivégeznének (De ahogy Joe@BEA írja, ez a legjobb kód feldolgozni bármilyen statementet).

Follow

Get every new post delivered to your Inbox.