OData Batch met DataJS: Twee vliegen in één slag

Zoals ik in mijn vorige blog over datajs al vermeldde, is het aanroepen
van een OData service met deze DataJS  bibliotheek stukken
eenvoudig geworden. Hoewel we dicht tegen het ijzer (lees: de soap berichten)
aan programmeren, blijft het samenstellen van de te versturen opdrachten simpel en leesbaar.

En de grootste winst van datajs is de mogelijkheid om meerdere opdrachten
tegelijkertijd te versturen. Het Open Data Protocol ondersteunt namelijk batch operaties. Hierdoor kunnen in één keer verschillende zoekopdrachten verstuurd worden of juist meerdere gewijzigde entiteiten in één keer opgeslagen worden.

Hier duiken we vandaag eens in.

Gremlins2

OData maakt bij batch opdrachten gebruik van twee krachten. Ten eerste beidt
Soap de mogelijkheid om in één keer een “multi-part” bericht te sturen.
Daarnaast kan de WCF Data Service meerdere gewijzigde entiteiten in één keer
submitten naar de data-context via de SaveChanges methode.

Dit is dus twee (of meer) vliegen in één slag. Er moet minder over de
lijn gestuurd worden wat dus een besparing van bandbreedte en tijd is. En dit
maakt het ook mogelijk om meerdere wijzigingen als één transactie te
verwerken.

Om dit te demonstreren gaan we eerst twee aparte queries uitvoeren. Kijk
naar het volgende voorbeeld. Op zich zijn dit niet de meest spannende zoekacties
(we vragen twee entiteiten apart van elkaar op via de unieke sleutels) maar dit
zijn aparte selecties!

De code is eenvoudig en goed leesbaar, los van de {} en [] tekens hier en
daar…

// GET two calls within
  BatchOData.request({
    requestUri: "htt p://localhost:2976/WcfDataService1.svc/$batch",
    method: "POST",
    data: { __batchRequests:
    [
      { requestUri: "Machines(1)?$select=MachineId,MachineName",
        method: "GET" },
      { requestUri: "Machines(2)?$select=MachineId,MachineName",
        method: "GET" }
    ]
    }
    },
      BatchSuccess,
      Error,
      OData.batchHandler
);

Als we dit aanroepen wordt met slechts één call beide requests uitgevoerd
en als één call afgehandeld. Dit is mooi te bekijken met Fiddler. Deze geeft
een mooi overzicht van wat er bij de request en response over het lijntje
gestuurd wordt:

POST http://localhost:2976/WcfDataService1.svc/$batch HTTP/1.1Content-Type:
multipart/mixed;boundary=batch_f2ba-bfa6-d506
Accept-Language: nl,en-US;q=0.5
dataserviceversion: 1.0
Accept: multipart/mixed
Referer: http://localhost:2976/datajs_GET_HTMLPage.htm
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0;
 .NET CLR 2.0.50727; .NET CLR 3.0.04506.30; .NET CLR 3.0.04506.648; .NET CLR
 3.5.21022; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; .NET4.0C; .NET4.0E)
Host: localhost:2976
Content-Length: 482
Connection: Keep-Alive
Pragma: no-cache
--batch_f2ba-bfa6-d506
Content-Type: application/http
Content-Transfer-Encoding: binary
GET Machines(1)?$select=MachineId,MachineName HTTP/1.1
Accept: application/atomsvc+xml;q=0.8, application/json;q=0.5, */*;q=0.1
--batch_f2ba-bfa6-d506
Content-Type: application/http
Content-Transfer-Encoding: binary
GET Machines(2)?$select=MachineId,MachineName HTTP/1.1
Accept: application/atomsvc+xml;q=0.8, application/json;q=0.5, */*;q=0.1
--batch_f2ba-bfa6-d506--

Wat opvalt is dat de aanroep met context-type : multipart wordt
opgemaakt. Vervolgens staan de twee aparte GET acties in de body.

Dit geeft het volgende response als antwoord:

HTTP/1.1 202 AcceptedCache-Control: no-cache
Content-Length: 903
Content-Type: <strong>multipart</strong>/mixed;
 boundary=batchresponse_f259fb24-2f68-41d6-95a4-fbe89cf29362
Server: Microsoft-IIS/7.5
DataServiceVersion: 1.0;
X-AspNet-Version: 4.0.30319
X-SourceFiles:
 =?UTF-8?B?QzpcU0RFXFN0 [knip] mF0Y2g=?=
X-Powered-By: ASP.NET
Date: Mon, 04 Apr 2011 16:58:10 GMT
--batchresponse_f259fb24-2f68-41d6-95a4-fbe89cf29362
Content-Type: application/http
Content-Transfer-Encoding: binary
HTTP/1.1 <strong>200</strong> OK
Cache-Control: no-cache
DataServiceVersion: 1.0;
Content-Type: application/json;charset=utf-8
{
"d" : {
"__metadata": {
"uri": "http://localhost:2976/WcfDataService1.svc/Machines(1)", "type":
"SdnODataModel.Machine"
}, "MachineId": 1, "MachineName": "Blender"
}
}
--batchresponse_f259fb24-2f68-41d6-95a4-fbe89cf29362
Content-Type: application/http
Content-Transfer-Encoding: binary
HTTP/1.1 <strong>200</strong> OK
Cache-Control: no-cache
DataServiceVersion: 1.0;
Content-Type: application/json;charset=utf-8
{
"d" : {
"__metadata": {
"uri": "http://localhost:2976/WcfDataService1.svc/Machines(2)", "type":
"SdnODataModel.Machine"
}, "MachineId": 2, "MachineName": "Machine that goes Ping"
}
}
--batchresponse_f259fb24-2f68-41d6-95a4-fbe89cf29362--

Ook de response heeft een multipart context-type. En in hetzelfde bericht
zijn de responses van beide aanvragen opgenomen, ieder met een 200 OK antwoord. Overigens zijn de geretourneerde entiteiten in JSON formaat beschreven. Dit wordt door datajs weer netjes in objecten omgezet.

function BatchSuccess(data) {
var html = data.__batchResponses[0].data.MachineId
             + "-" +
             data.__batchResponses[0].data.MachineName
             + " / " +
             data.__batchResponses[1].data.MachineId
             + "-" +
             data.__batchResponses[1].data.MachineName;
  $("#responsePlaceHolder").html(html);
}

En op deze manier kan dus heel efficiënt data opgehaald worden waarbij de
aanvraag over meerdere queries is verdeeld.

Maar hoe zit het met wijzigingen van data? Nou, dat zit wel goed. Met
hetzelfde gemak kunnen ook meerdere insert, updates en deletes uitgevoerd
worden.

Pas echt interessant is de mogelijkheid om verschillende types aan
wijzigingen te combineren.

Stel je voor dat we een soort van master-detail scherm hebben. Zou het
mogelijk zijn zowel wijzigingen aan de master (bv. klantgegevens) als
toevoegingen aan de details (bv. orders) in één keer te versturen? In mijn
vorige blog bleek al dat een MERGE (het opsturen van alleen de gewijzigde
kolommen) veel efficienter is dan een PUT maar dat de MERGE een buitenbeentje is qua notatie.

Zou een merge samen met een POST (het toevoegen van records) kunnen
plaatsvinden?

Nou, dat kan. Kijk maar eens naar onderstaande voorbeeld. Hierin
combineer ik een wijziging van een bestaande entiteit (MERGE) met het opvoeren van een nieuwe entiteit (POST):

// BATCH MERGE-POSTOData.request({
requestUri: "htt p://localhost:2976/WcfDataService1.svc/$batch",
method: "POST",
data: {
 __batchRequests: [
{ __changeRequests:
 [
{ requestUri: "Machines(1)",
 method: "MERGE",
 data: { MachineName: 'MixerNew'} }
, { requestUri: "Machines", method: "POST",
 data: { MachineName: 'DataNew', SupervisorName:
 'userNew'} }
] }
] }
},
Success,
Error,
OData.batchHandler
);

Ook hier komt Fiddler ons helpen met het doorgronden van de request en de
response.

De request ziet er als volgt uit:

POST http://localhost:2976/WcfDataService1.svc/$batch HTTP/1.1Content-Type:
multipart/mixed;boundary=batch_1451-c1ce-8a01
Accept-Language: nl,en-US;q=0.5
dataserviceversion: 1.0
Accept: multipart/mixed
Referer: http://localhost:2976/datajs_POST_HTMLPage.htm
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0;
 .NET CLR 2.0.50727; .NET CLR 3.0.04506.30; .NET CLR 3.0.04506.648; .NET CLR
 3.5.21022; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; .NET4.0C; .NET4.0E)
Host: localhost:2976
Content-Length: 746
Connection: Keep-Alive
Pragma: no-cache
--batch_1451-c1ce-8a01
Content-Type: multipart/mixed; boundary=changeset_b3dd-3981-60b2
--changeset_b3dd-3981-60b2
Content-Type: application/http
Content-Transfer-Encoding: binary
MERGE Machines(1) HTTP/1.1
Accept: application/atomsvc+xml;q=0.8, application/json;q=0.5, */*;q=0.1
DataServiceVersion: 1.0
Content-Type: application/json
{"MachineName":"MixerNew"}
--changeset_b3dd-3981-60b2
Content-Type: application/http
Content-Transfer-Encoding: binary
POST Machines HTTP/1.1
Accept: application/atomsvc+xml;q=0.8, application/json;q=0.5, */*;q=0.1
DataServiceVersion: 1.0
Content-Type: application/json
{"MachineName":"DataNew","SupervisorName":"userNew"}
--changeset_b3dd-3981-60b2--
--batch_1451-c1ce-8a01--

Ook hier is het een multipart bericht met daarin de MERGE en de POST
gebroederlijk naast elkaar.

De daarop volgende response is niet veel spannender:

HTTP/1.1 202 AcceptedCache-Control: no-cache
Content-Length: 1137
Content-Type: multipart/mixed;
 boundary=batchresponse_a689df61-3d2a-4533-9e88-522b3d519196
Server: Microsoft-IIS/7.5
DataServiceVersion: 1.0;
X-AspNet-Version: 4.0.30319
X-SourceFiles:
 =?UTF-8?B?QzpcU0RFXFN [knip] wkYmF0Y2g=?=
X-Powered-By: ASP.NET
Date: Mon, 04 Apr 2011 17:36:25 GMT
--batchresponse_a689df61-3d2a-4533-9e88-522b3d519196
Content-Type: multipart/mixed;
 boundary=changesetresponse_17e960ab-699f-40f0-8f1b-6003cf4dd4ae
--changesetresponse_17e960ab-699f-40f0-8f1b-6003cf4dd4ae
Content-Type: application/http
Content-Transfer-Encoding: binary
HTTP/1.1 204 No Content
Cache-Control: no-cache
DataServiceVersion: 1.0;
--changesetresponse_17e960ab-699f-40f0-8f1b-6003cf4dd4ae
Content-Type: application/http
Content-Transfer-Encoding: binary
HTTP/1.1 201 Created
Cache-Control: no-cache
DataServiceVersion: 1.0;
Content-Type: application/json;charset=utf-8
Location: http://localhost:2976/WcfDataService1.svc/Machines(54)
{
"d" : {
"__metadata": {
"uri": "http://localhost:2976/WcfDataService1.svc/Machines(54)", "type":
"SdnODataModel.Machine"
}, "MachineId": 54, "MachineName": "DataNew", "SupervisorName": "userNew",
"Image": null, "ImageName": null, "Parts": {
"__deferred": {
"uri": "htt p://localhost:2976/WcfDataService1.svc/Machines(54)/Parts"
}
}
}
}
--changesetresponse_17e960ab-699f-40f0-8f1b-6003cf4dd4ae--
--batchresponse_a689df61-3d2a-4533-9e88-522b3d519196--

De MERGE wordt met een 204 (no content) beantwoord. Dit betekent dat de
opslag heeft plaatsgevonden maar er is geen nieuwe entiteit opgestuurd. Vanuit
het oogpunt van javascript heeft de client al alle kennis over het gewijzigde
object op de client.

De POST wordt met een 201 (Created) afgehandeld. Dit betekent dat de
entiteit in de context op de server is geaccepteerd. En we krijgen tevens de
gehele entiteit terug, compleet met de unieke sleutel van het object.

Met bovenstaande voorbeelden wordt duidelijk hoe efficient over het
netwerk gecommuniceerd kan worden als men zich verdiept in het Open Data
Protocol. Natuurlijk zal een batch opdracht niet altijd op zijn plaats zijn,
maar het is een krachtig stuk gereedschap die iedere OData ontwikkelaar moet
kunnen beheersen. En datajs maakt het wel heel eenvoudig te gebruiken.

Datajs: javascript bibliotheek speciaal voor browser data

Voor mij is jQuery een verademing. Waar in het verleden het manipuleren van
objecten in de DOM van de browser een lastige bezigheid was, vooral als de
pagina dynamisch opgebouwd wordt, kan met jQuery zo’n klusje snel geklaard zijn. En jQuery is dan ook vooral populair vanwege die goede eigenschappen om door de DOM te navigeren.

odata

Momenteel ben ik bezig met een andere javascript bibliotheek, datajs

Datajs wilt DE oplossing worden voor ‘data-centric’ webapplicaties, op de browser welteverstaan. Met datajs moet het ophalen en wegschrijven van data eenvoudig, snel en transparant worden.  Hoewel met jQuery ook heel prettig ajax calls uitgevoerd kunnen worden, heeft datajs wel degelijk meerwaarde.

Op dit moment wordt eigenlijk alleen OData ondersteund. Dit is het formaat wat een WCF Data Service communiceert. Zo’n service is ook netjes met b.v. jQuery aan te spreken maar dit is net wat bewerkelijker en beperkter.

Het is bewerkelijk omdat communicatie tussen javascript client en OData service graag in Json formaat wordt uitgevoerd. Datajs snapt dit direct, voor jQuery moet je de objecten eerst naar Json serialiseren via Json stringify. En datajs kan ook op een elegante manier meerdere call tegelijkertijd als batch versturen. Dit scheelt gebruik van bandbreedte en maakt het omgaan met meerdere asynchrone callbacks overbodig.

Helaas bevindt datajs zich nog in een vroeg stadium. Dus de toekomstige scope is nog niet uitgewerkt, er zitten nog bugs in het framework die er uit moeten en de documentatie is kreupel. Maar met een beetje doorzetten wordt de wereld toch een beetje vrolijker met datajs. Ik vind het grootste voordeel dat de javascript code een stukje cleaner en leesbaarder is.

Want wat vind je van deze leesactie?

GET

OData.read(
  "http://localhost:2976/WcfDataService1.svc/Machines?
    $filter=SupervisorName eq  'user'",Success);

Dit is voldoende om een lijst van machines op te vragen, compleet met filter.
Dit filter is overigens de verdienste van OData.

Het opslaan van een
nieuw object kost bijna net zo weinig moeite:

POST

var requestPost =
  {method: "POST",
   requestUri: "htt p://localhost:2976/WcfDataService1.svc/Machines",
   data: {
     MachineName: 'Data',
     SupervisorName: 'user'
   }
  }

OData.request(requestPost, Success);

Hier versturen we een nieuw object om opgeslagen te laten
worden. En het verwijderen van een object is ook niet bepaald
spannend:

DELETE

var requestDelete =
  { method: "DELETE",
    requestUri: "htt p://localhost:2976/WcfDataService1.svc/Machines(31)"
  }

OData.request(requestDelete, Success); // geretourneerde data object is leeg

Natuurlijk moeten op de server wel de rechten op de tabel en in de service
opengezet zijn om respectivelijk een insert of delete te kunnen
uitvoeren.

Het wijzigen van een record in de database kan op twee
manieren:

1. Het te wijzigen object is al ooit in zijn geheel opgehaald in de browser.
Deze wordt in zijn geheel, maar gewijzigd, weer opgestuurd om opgeslagen te
worden. Dit is een PUT.

2. Als je van een object wel de structuur weet (naam en type van de
properties) en je weet ook de primary key en de waardes van de gewijzigde
kolommen, dan kan je een MERGE uitvoeren. De niet meegezonden, ongewijzigde (en wellicht onbekende) kolommen, blijven dan ongemoeid.

De eerste variant, de volledige update is heel eenvoudig. Je stuurt
gewoon het hele object op. Op moment van schrijven zit er een serialisatie bug
in DataServiceVersion 2.0 dus geef die niet mee. De documentatie op de site ten
spijt. Versie 1.0 werkt wel correct of laat deze weg.

PUT

var requestPut =
   {headers: { "DataServiceVersion": "1.0" },method: "PUT",
    requestUri: "htt p://localhost:2976/WcfDataService1.svc/Machines(33)",
    data: {
       MachineId: '33',
       SupervisorName: 'User',
       MachineName: 'Mixer'
           }
    }

OData.request(requestPut, Success);

Nogmaals de waarschuwing: als er nog meer, niet meegegeven, kolommen in het
record bestaan of als deze later toegevoegd worden aan de tabel, dan zal een PUT
die kolommen op NULL stellen…

Wil je dit voorkomen of als je het totale object niet compleet in een javascript object hebt zitten, dan kan je voor de tweede optie kunnen kiezen:

MERGE

var requestPut = {
    headers: { "DataServiceVersion": "1.0",
                "X-HTTP-Method": "MERGE"},
    method: "POST",
    requestUri: "htt p://localhost:2976/WcfDataService1.svc/Machines(33)",
    data: {
      MachineName: 'Mixer'
          }
   }

OData.request(requestPut, Success);

Helaas schijnt de MERGE niet integraal in internetland ondersteunt te worden
als HTTP verb. Daarom hebben de makers van datajs momenteel bovenstaande header entry bedacht. Ik heb vernomen dat ze nog nadenken over een meer high level notatie…

Met bovenstaande voorbeelden wordt een
aardige indruk gegeven van de verschillende calls die gemaakt kunnen worden. Ik verwijs toch nog even door naar de documentatie voor meer details zoals de batch opdrachten.

Nog een laatste tip, of eigenlijk vier… Ik liep in het begin tegen enkele vreemde
communicatieproblemen aan. En dan is het prettig om alle facetten van de
dataoverdracht goed te kunnen testen. Ik maak hierbij gebruik van de volgende
simpele hulpmiddelen:

  • 1. Plaats de javascript in aparte .JS bestanden die je als script verwijzing in je html pagina’s aanroept. Hiermee wordt het heel simpel om een breakpoint te zetten en te steppen in de javascript.
  • 2. Gebruik Fiddler of een gelijkwaardige tool om te zien wat er over de lijn gaat. Fiddler zal de eerste keer even duizelen met wat er te zien valt maar probeer daar even door heen te bijten. Volg gewoon request en response en negeer de rest. Het is voor mij onmisbaar geworden. Vergeet niet om voor lokaal debuggen het pac script aan te zetten in de opties.

Fiddler

  • 3. Zet op de server tijdens de testfase de “UseVerboseErrors” aan. Dit geeft voor de WCF Data Service eventuele fouten uit het entity framework of zelfs de database door.
public static void InitializeService(DataServiceConfiguration config)
{
  config.SetEntitySetAccessRule("*", EntitySetRights.All);
  config.SetEntitySetPageSize("*", 10);
  config.DataServiceBehavior.MaxProtocolVersion =
    DataServiceProtocolVersion.V2;
  config.UseVerboseErrors = true;
}
  • 4. Gebruik eventueel QueryInterceptors en ChangeInterceptors om de data bij binnenkomst op de WCF Data Service te bekijken, nog voordat het de context ingeschoten wordt.

Hiermee is javascript een volwaardige client voor OData geworden, en die
onbekende “niet .Net” client ondersteuning is één van de mooiste pijlers onder
OData.