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
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
--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)
--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:
--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:
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 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
description--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
description{
"Content-Type": "multipart/mixed;boundary=batch_@{variables('MultipartBatchId')}"
}
--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 timeAnche 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
JSONQui 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 JSONControllando 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
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
descriptionOttimizzazione 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 timeSuppongo 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).
descriptionImplementazione
L'implementazione prevede 3 step
Ottimizzazione 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:
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.