Solitamente quando si devono creare una serie di item su SharePoint Online in Power Automate l'approccio classico è creare un ciclo con l'azione Apply to each e all'interno del ciclo invocare, uno alla volta, l'azione Create item per SharePoint Online.

Ovviamente l'approccio funziona bene, ma presenta dei limiti quando si vogliono inserire molti item, già con 20 o più si nota un incremento notevole dei tempi di esecuzione dell'ordine di parecchi minuti.

HTTP Request Batch

Un possibile approccio per ridurre i tempi è quello di eseguire la creazione di tutti gli items in Batch con un unica azione Send an HTTP request to SharePoint.

Si tratta di chiamare in POST la url
URL
https://<tenantName>.sharepoint.com/_api/$batch
e passare nel body una stringa con l'elenco di tutte le url che puntano alla lista con i relativi dati da aggiornare, tipo questa
Text: Corpo/Body della richiesta
--batch_c02f1b8d-deb1-486b-87db-d8ff6c7190cb
Content-Type: multipart/mixed;boundary=changeset_6d3d8d17-488d-418e-a3a5-a8e4510d41ad
Content-Lenght: 840
Content-Transfer-Encoding: binary

--changeset_6d3d8d17-488d-418e-a3a5-a8e4510d41ad
Content-Type: application/http
Content-Transfer-Encoding: binary

POST https://<tenantName.sharepoint.com/_api/web/lists/getbytitle('<listName>')/items HTTP/1.1
Content-Type: application/json;odata=nometadata

{"Title":"Batch 1","Date":"2022-10-06T19:30:42.7847708Z","Note":"aaa"}

--changeset_6d3d8d17-488d-418e-a3a5-a8e4510d41ad
Content-Type: application/http
Content-Transfer-Encoding: binary

POST https://<tenantName.sharepoint.com/_api/web/lists/getbytitle('<listName>')/items HTTP/1.1
Content-Type: application/json;odata=nometadata

{"Title":"Batch 2","Date":"2022-10-06T19:31:43.0191527Z","Note":"bbb"}
--changeset_6d3d8d17-488d-418e-a3a5-a8e4510d41ad--

--batch_c02f1b8d-deb1-486b-87db-d8ff6c7190cb--

Batch

In pratica va creata una richiesta batch delimitata dall'identificatore (boundary string) iniziale --batch_<guid> e finale --batch_<guid>-- (doppi meno alla fine)
Text: Struttura del Batch
--batch_c02f1b8d-deb1-486b-87db-d8ff6c7190cb
Content-Type: multipart/mixed;boundary=changeset_6d3d8d17-488d-418e-a3a5-a8e4510d41ad
Content-Lenght: <lunghezza del change set senza la riga di chiusura>
Content-Transfer-Encoding: binary

... contenuto dei changeset

--batch_c02f1b8d-deb1-486b-87db-d8ff6c7190cb--
Teoricamente non è necessario usare un guid, ma il suo uso è più sicuro perché garantisce che non ci sia un testo simile nel corpo della richiesta.
Il valore di Content-Lenght è la lunghezza della stringa che rappresenta tutti i changeset passati, senza contare la riga di chiusura dei changeset.
Dopo Content-Transfer-Encoding va lasciata una riga vuota senza spazi.

Changeset

I changeset saranno all'interno del batch, tanti quante sono le azioni da compiere.
In questo caso tutte create item:
Text: Struttura dei Changeset con 2 insert
--changeset_6d3d8d17-488d-418e-a3a5-a8e4510d41ad
Content-Type: application/http
Content-Transfer-Encoding: binary

POST https://<tenantName.sharepoint.com/_api/web/lists/getbytitle('<listName>')/items HTTP/1.1
Content-Type: application/json;odata=nometadata

{"Title":"Batch 1","Date":"2022-10-06T19:30:42.7847708Z","Note":"aaa"}

--changeset_6d3d8d17-488d-418e-a3a5-a8e4510d41ad
Content-Type: application/http
Content-Transfer-Encoding: binary

POST https://<tenantName.sharepoint.com/_api/web/lists/getbytitle('<listName>')/items HTTP/1.1
Content-Type: application/json;odata=nometadata

{"Title":"Batch 2","Date":"2022-10-06T16:31:43.0191527Z","Note":"BatchIdc02f1b8d-deb1-486b-87db-d8ff6c7190cb, ChangesetId 6d3d8d17-488d-418e-a3a5-a8e4510d41ad"}
--changeset_6d3d8d17-488d-418e-a3a5-a8e4510d41ad--
Ogni changeset inizia con la stringa separatore dei changeset --changeset_<guid 2>, seguita dalle righe che rappresentano gli headers:
  • Content-Type: application/http
  • Content-Transfer-Encoding: binary
Una linea linea vuota segna la fine degli headers e l'inizio del body.

Venendo al corpo della singola richiesta/changeset:
Text: Singola richiesta
POST https://<tenantName.sharepoint.com/_api/web/lists/getbytitle('<listName>')/items HTTP/1.1
Content-Type: application/json;odata=nometadata

{"Title":"Batch 2","Date":"2022-10-06T16:31:43.0191527Z","Note":"BatchIdc02f1b8d-deb1-486b-87db-d8ff6c7190cb, ChangesetId 6d3d8d17-488d-418e-a3a5-a8e4510d41ad"}
La prima riga è composta da:
  • metodo HTTP da usare (POST)
  • uno spazio
  • la url che rappresenta la risorsa
  • un altro spazio
  • la stringa fissa HTTP/1.1

Anche in questo caso subito dopo abbiamo gli headers, in questo caso uno solo (Content-Type), terminati sempre da una linea vuota.

Il body in questo caso è l'oggetto JSON con i dati da usare per creare il nuovo item.

Alla fine di tutti i changeset troviamo la stringa di chiusura --changeset_<guid 2>-- (doppi meno alla fine).

Realizzazione in Power Automate

Step 1: Creiamo 2 variabili con i Guid da usare come delimitatori dei batch e dei changeset e una altra variabile per contenere la stringa con tutti i changeset
Inizializzazione guidInizializzazione guid

Step 2: generiamo la stringa con tutti i changeset tramite un Apply to each (vedi dopo come evitare Apply to each) andando in append sulla variabile
descriptiondescription
Power Automate: Value
--changeset_@{variables('MultipartChangesetId')}
Content-Type: application/http
Content-Transfer-Encoding: binary

POST @{variables('SiteUrl')}_api/web/lists/getbytitle('@{variables('ListTitle')}')/items HTTP/1.1
Content-Type: application/json;odata=verbose

{
  __metadata: {"type": "SP.Data.TestBatchInsertListItem" },
  Title: "@{items('Apply_to_each:_InputData')?['Title']}",
  Date: "@{items('Apply_to_each:_InputData')?['Date']}",
  Note: "@{items('Apply_to_each:_InputData')?['Note']}"
}
In questo caso la mia sorgente dati era un array di nome ArrInputData.

Step 3: Eseguiamo la richiesta HTTP
descriptiondescription
Power Automate: Headers
{
  "Content-Type": "multipart/mixed;boundary=batch_@{variables('MultipartBatchId')}"
}
Power Automate: Body
--batch_@{variables('MultipartBatchId')}
Content-Type: multipart/mixed;boundary=changeset_@{variables('MultipartChangesetId')}
Content-Lenght: @{length(variables('StringBatchItemsToSave'))}
Content-Transfer-Encoding: binary

@{variables('StringBatchItemsToSave')}
--changeset_@{variables('MultipartChangesetId')}--
--batch_@{variables('MultipartBatchId')}--
che viene eseguito in pochi secondi anche con 50 items o più
Post HTTP timePost HTTP time
Anche se dura pochi secondi, la richiesta ha portato a termine tutti gli inserimenti, infatti nella risposta c'è il risultato di ogni singolo inserimento (vedi dopo).

Errori

Una nota particolare va fatta riguardo agli errori.

L'azione ritornerà sempre success, anche se alcuni degli item non sono stati inseriti correttamente.

Per capire quali azioni sono andate in errore o meno, va controllata la risposata JSON
Qui troviamo il dettaglio delle azioni con lo stato di esecuzione.
Ovvero statusCode=201 (Created) in caso di successo nella creazione dell'item, oppure 400 (Bad Request) in caso di errore:
Risposta JSONRisposta JSON
Controllando le proprietà LOCATION è possibile ricavare gli Id degli item inseriti.

Un metodo veloce per capire se tutto è andato a buon fine è quello di fare uno split della stringa di risposta e contare le righe e togliere 1
URL
sub(length(split(string(outputs('Send_an_HTTP:_Batch_create')?['body/$multipart']),'"statusCode":201')),1)
se coincide con il numero di item che dovevano essere inseriti, tutto è andato a buon fine
descriptiondescription

Ottimizzazione Apply to each

Guardando l'esecuzione di può facilmente vedere come la maggior parte del tempo è spesa nell'Apply to each per costruire la stringa con i changeset, mentre l'esecuzione dell'azione Send HTTP è quasi immediata (pochi secondi).

Ad esempio un ciclo su 50 elementi impiega circa 21 secondi
Apply to each timeApply to each time
Suppongo che i tempi elevati siano legati al fatto che il ciclo Apply to each, ogni tanto, venga sospeso dalla piattaforma per distribuire le risorse nell'abiente condiviso, quindi i tempi di esecuzione sono difficilmente prevedibili.

Nel caso di insert, dove le url che identificano la risorsa sono tutte uguali, si può fare un ottimizzazione.

Anziché usare Apply to each, si possono usare una serie di azioni Data Operation e la funzione split per portare i tempi a 0 secondi (sempre su 50 items).
descriptiondescription

Implementazione

L'implementazione prevede 3 step
Ottimizzazione apply to eachOttimizzazione apply to each
Step 1: Creazione di una variabile con il template del changeset (nell'esempio un compose ma può essere anche una variabile)

Step 2: Join per creare una stringa di tutti gli items separati da un separatore (in questo caso @@@###!!!@@@ che rappresenta una stringa che difficilmente comparirà nel templare o nei dati)

Step 3: Replace del separatore con il template e l'aggiunta di un template all'inizio.
La formula è questa:
Power Automate: Concat and Replace
concat(string(outputs('Compose:_BatchChangesetTemplate')), replace(body('Join'), '@@@###!!!@@@', string(outputs('Compose:_BatchChangesetTemplate'))))

Conclusioni

Gli update in batch, ma anche le letture di più items, possono ridurre notevolmente i tempi di esecuzione..

Questa è giusto un introduzione, per maggiori dettagli vedi Make batch requests with the REST APIs.
Potrebbe interessarti anche: