Come scrivere stored procedure, trigger e funzioni definite dall'utente in Azure Cosmos DB

SI APPLICA A: NoSQL

L'esecuzione integrata e transazionale di JavaScript con il linguaggio di Azure Cosmos DB permette di scrivere stored procedure, trigger e funzioni definite dall'utente. Quando si usa l'API per NoSQL in Azure Cosmos DB, è possibile scrivere stored procedure, trigger e funzioni definite dall'utente (UDF) utilizzando JavaScript. È possibile scrivere la logica nel linguaggio JavaScript ed eseguirla all'interno del motore di database. È possibile creare ed eseguire trigger, stored procedure e UDF usando il portale di Azure, l'API di query di JavaScript in Azure Cosmos DB e gli Azure Cosmos DB per gli SDK NoSQL.

Per chiamare una stored procedure, un trigger o una funzione definita dall'utente, è necessario registrarla. Per altre informazioni, consultare Come usare stored procedure, trigger e funzioni definite dall'utente in Azure Cosmos DB.

Nota

Per i contenitori partizionati, quando si esegue una stored procedure, è necessario specificare un valore della chiave di partizione nelle opzioni di richiesta. Le stored procedure hanno sempre come ambito una chiave di partizione. Gli elementi con un valore chiave di partizione diverso non saranno visibili alla stored procedure. Questo vale anche per i trigger.

Nota

Le funzionalità JavaScript lato server, inclusi stored procedure, trigger e funzioni definite dall'utente, non supportano l'importazione di moduli.

Suggerimento

Azure Cosmos DB supporta la distribuzione di contenitori con stored procedure, trigger e funzioni definite dall'utente. Per altre informazioni, consultare Creare un contenitore Azure Cosmos DB con funzionalità lato server.

Come scrivere stored procedure

Le stored procedure vengono scritte in JavaScript e possono creare, aggiornare, leggere ed eseguire una query nonché eliminare elementi all'interno di contenitori Azure Cosmos. Le stored procedure vengono registrate per ogni raccolta e funzionano in qualsiasi documento o allegato presente nella raccolta.

Nota

Azure Cosmos DB ha criteri di addebito diversi per le stored procedure. Poiché le stored procedure possono eseguire codici e utilizzare un numero qualsiasi di Unità richiesta (UR), ogni esecuzione richiede un addebito anticipato. In questo modo, gli script delle stored procedure non influiscono sui servizi back-end. L'importo addebitato in anticipo equivale all'addebito medio utilizzato dallo script nelle chiamate precedenti. Le Unità richiesta medie per operazione vengono riservate prima dell'esecuzione. Se le chiamate presentano molte varianze nelle UR, l'utilizzo del budget potrebbe esserne interessato. In alternativa, è consigliabile usare richieste batch o richieste bulk anziché stored procedure per evitare la varianza rispetto agli addebiti delle Unità richiesta.

Ecco una semplice stored procedure che restituisce una risposta "Hello World".

var helloWorldStoredProc = {
    id: "helloWorld",
    serverScript: function () {
        var context = getContext();
        var response = context.getResponse();

        response.setBody("Hello, World");
    }
}

L'oggetto contesto offre accesso a tutte le operazioni che è possibile eseguire in Azure Cosmos DB, oltre all'accesso agli oggetti richiesta e risposta. In questo caso, si usa l'oggetto risposta per impostare il corpo della risposta da restituire al client.

Una volta scritta, la stored procedure deve essere registrata con una raccolta. Per ottenere maggiori informazioni, consultare Come eseguire stored procedure in Azure Cosmos DB.

Creazione di elementi utilizzando le stored procedure

Quando si crea un elemento utilizzando una stored procedure, l'elemento viene inserito nel contenitore Azure Cosmos DB e viene restituito un ID per l'elemento appena creato. La creazione di un elemento è un'operazione asincrona e dipende dalle funzioni di callback JavaScript. La funzione di callback ha due parametri: uno per l'oggetto Error in caso di errore dell'operazione e uno per il valore restituito, in questo caso, l'oggetto creato. All'interno del callback, è possibile gestire l'eccezione oppure generare un errore. Nel caso in cui non sia fornito un callback e si verifichi un errore, il runtime di Azure Cosmos DB genera un errore.

La stored procedure include anche un parametro per impostare la descrizione come valore booleano. Quando il parametro è impostato su true e la descrizione non è presente, la stored procedure genera un'eccezione. In caso contrario, il resto della stored procedure rimane in esecuzione.

L’esempio seguente di stored procedure accetta una matrice di un nuovo elemento di Azure Cosmos DB come input, inserisce l'elemento nel contenitore Azure Cosmos DB e restituisce l'ID dell'elemento appena creato. In questo esempio, viene usato l'esempio ToDoList dall'Avvio rapido .NET API per NoSQL.

function createToDoItems(items) {
    var collection = getContext().getCollection();
    var collectionLink = collection.getSelfLink();
    var count = 0;

    if (!items) throw new Error("The array is undefined or null.");

    var numItems = items.length;

    if (numItems == 0) {
        getContext().getResponse().setBody(0);
        return;
    }

    tryCreate(items[count], callback);

    function tryCreate(item, callback) {
        var options = { disableAutomaticIdGeneration: false };

        var isAccepted = collection.createDocument(collectionLink, item, options, callback);

        if (!isAccepted) getContext().getResponse().setBody(count);
    }

    function callback(err, item, options) {
        if (err) throw err;
        count++;
        if (count >= numItems) {
            getContext().getResponse().setBody(count);
        } else {
            tryCreate(items[count], callback);
        }
    }
}

Matrice come parametri di input per la stored procedure

Quando si scrive una stored procedure nel portale di Azure, i parametri di input vengono sempre inviati sotto forma di stringa alla stored procedure. Anche se si passa una matrice di stringhe come input, la matrice viene convertita in stringa e inviata alla stored procedure. Per aggirare questo problema, è possibile definire una funzione all'interno della stored procedure che analizzi la stringa come matrice. Il codice seguente illustra come analizzare un parametro di input di stringa sotto forma di matrice:

function sample(arr) {
    if (typeof arr === "string") arr = JSON.parse(arr);

    arr.forEach(function(a) {
        // do something here
        console.log(a);
    });
}

Transazioni all'interno di stored procedure

È possibile implementare le transazioni negli elementi all'interno di un contenitore usando una stored procedure. L'esempio seguente usa le transazioni in un'app di gioco di fantacalcio per scambiare giocatori tra due squadre in un'unica operazione. La stored procedure prova a leggere i due elementi di Azure Cosmos DB, ciascuno corrispondente agli ID dei clienti più importanti passati come argomento. Se vengono trovati entrambi i giocatori, la stored procedure aggiorna gli elementi scambiando le rispettive squadre. Se si verificano errori lungo il percorso, la stored procedure genera un'eccezione JavaScript che interrompe implicitamente la transazione.

// JavaScript source code
function tradePlayers(playerId1, playerId2) {
    var context = getContext();
    var container = context.getCollection();
    var response = context.getResponse();

    var player1Document, player2Document;

    // query for players
    var filterQuery =
    {
        'query' : 'SELECT * FROM Players p where p.id = @playerId1',
        'parameters' : [{'name':'@playerId1', 'value':playerId1}] 
    };

    var accept = container.queryDocuments(container.getSelfLink(), filterQuery, {},
        function (err, items, responseOptions) {
            if (err) throw new Error("Error" + err.message);

            if (items.length != 1) throw "Unable to find both names";
            player1Item = items[0];

            var filterQuery2 =
            {
                'query' : 'SELECT * FROM Players p where p.id = @playerId2',
                'parameters' : [{'name':'@playerId2', 'value':playerId2}]
            };
            var accept2 = container.queryDocuments(container.getSelfLink(), filterQuery2, {},
                function (err2, items2, responseOptions2) {
                    if (err2) throw new Error("Error" + err2.message);
                    if (items2.length != 1) throw "Unable to find both names";
                    player2Item = items2[0];
                    swapTeams(player1Item, player2Item);
                    return;
                });
            if (!accept2) throw "Unable to read player details, abort ";
        });

    if (!accept) throw "Unable to read player details, abort ";

    // swap the two players’ teams
    function swapTeams(player1, player2) {
        var player2NewTeam = player1.team;
        player1.team = player2.team;
        player2.team = player2NewTeam;

        var accept = container.replaceDocument(player1._self, player1,
            function (err, itemReplaced) {
                if (err) throw "Unable to update player 1, abort ";

                var accept2 = container.replaceDocument(player2._self, player2,
                    function (err2, itemReplaced2) {
                        if (err) throw "Unable to update player 2, abort"
                    });

                if (!accept2) throw "Unable to update player 2, abort";
            });

        if (!accept) throw "Unable to update player 1, abort";
    }
}

Esecuzione vincolata all'interno di stored procedure

Di seguito è riportato un esempio di una stored procedure che importa in blocco gli elementi in un contenitore Azure Cosmos. La stored procedure gestisce l'esecuzione vincolata verificando il valore booleano restituito da createDocument e quindi usa il numero di elementi inseriti in ogni chiamata della stored procedure per tracciare e riprendere l'avanzamento nei vari batch.

function bulkImport(items) {
    var container = getContext().getCollection();
    var containerLink = container.getSelfLink();

    // The count of imported items, also used as current item index.
    var count = 0;

    // Validate input.
    if (!items) throw new Error("The array is undefined or null.");

    var itemsLength = items.length;
    if (itemsLength == 0) {
        getContext().getResponse().setBody(0);
    }

    // Call the create API to create an item.
    tryCreate(items[count], callback);

    // Note that there are 2 exit conditions:
    // 1) The createDocument request was not accepted.
    //    In this case the callback will not be called, we just call setBody and we are done.
    // 2) The callback was called items.length times.
    //    In this case all items were created and we don’t need to call tryCreate anymore. Just call setBody and we are done.
    function tryCreate(item, callback) {
        var isAccepted = container.createDocument(containerLink, item, callback);

        // If the request was accepted, callback will be called.
        // Otherwise report current count back to the client,
        // which will call the script again with remaining set of items.
        if (!isAccepted) getContext().getResponse().setBody(count);
    }

    // This is called when container.createDocument is done in order to process the result.
    function callback(err, item, options) {
        if (err) throw err;

        // One more item has been inserted, increment the count.
        count++;

        if (count >= itemsLength) {
            // If we created all items, we are done. Just set the response.
            getContext().getResponse().setBody(count);
        } else {
            // Create next document.
            tryCreate(items[count], callback);
        }
    }
}

Funzionalità Async/await con stored procedure

L'esempio di stored procedure seguente utilizza la funzionalità async/await con Suggerimenti usando una funzione di supporto. La stored procedure esegue una query per un elemento e lo sostituisce.

function async_sample() {
    const ERROR_CODE = {
        NotAccepted: 429
    };

    const asyncHelper = {
        queryDocuments(sqlQuery, options) {
            return new Promise((resolve, reject) => {
                const isAccepted = __.queryDocuments(__.getSelfLink(), sqlQuery, options, (err, feed, options) => {
                    if (err) reject(err);
                    resolve({ feed, options });
                });
                if (!isAccepted) reject(new Error(ERROR_CODE.NotAccepted, "queryDocuments was not accepted."));
            });
        },

        replaceDocument(doc) {
            return new Promise((resolve, reject) => {
                const isAccepted = __.replaceDocument(doc._self, doc, (err, result, options) => {
                    if (err) reject(err);
                    resolve({ result, options });
                });
                if (!isAccepted) reject(new Error(ERROR_CODE.NotAccepted, "replaceDocument was not accepted."));
            });
        }
    };

    async function main() {
        let continuation;
        do {
            let { feed, options } = await asyncHelper.queryDocuments("SELECT * from c", { continuation });

            for (let doc of feed) {
                doc.newProp = 1;
                await asyncHelper.replaceDocument(doc);
            }

            continuation = options.continuation;
        } while (continuation);
    }

    main().catch(err => getContext().abort(err));
}

Come scrivere i trigger

Azure Cosmos DB supporta pre-trigger e post-trigger. I pre-trigger vengono eseguiti prima di modificare un elemento del database e i post-trigger vengono eseguiti dopo la modifica di un elemento del database. I trigger non vengono eseguiti automaticamente. È necessario specificare i trigger per ogni operazione di database in cui si desidera eseguirli. Dopo aver scritto un trigger, è necessario registrare e chiamare un pre-trigger utilizzando gli SDK di Azure Cosmos DB.

Pre-trigger

L’esempio seguente illustra come viene utilizzato un pre-trigger per convalidare le proprietà di un elemento di Azure Cosmos DB in corso di creazione. In questo esempio, viene usato l'esempio ToDoList dall'Avvio rapido .NET API per NoSQL per aggiungere una proprietà Timestamp ad un elemento appena aggiunto, se non ne contiene uno.

function validateToDoItemTimestamp() {
    var context = getContext();
    var request = context.getRequest();

    // item to be created in the current operation
    var itemToCreate = request.getBody();

    // validate properties
    if (!("timestamp" in itemToCreate)) {
        var ts = new Date();
        itemToCreate["timestamp"] = ts.getTime();
    }

    // update the item that will be created
    request.setBody(itemToCreate);
}

I pre-trigger non possono avere parametri di input. L'oggetto richiesta nel trigger viene usato per modificare il messaggio di richiesta associato all'operazione. Nell'esempio precedente, si eseguiva il pre-trigger contemporaneamente alla creazione di un elemento di Azure Cosmos DB e il corpo del messaggio di richiesta contiene l'elemento da creare in formato JSON.

Quando i trigger vengono registrati, è possibile specificare le operazioni con le quali è possibile eseguirli. Questo trigger deve essere creato con un valore TriggerOperation di TriggerOperation.Create, quindi non è consentito l'uso del trigger in un'operazione di sostituzione.

Per ottenere esempi su come registrare e chiamare un pre-trigger, vedere pre-trigger e post-trigger.

Post-trigger

L'esempio seguente mostra un post-trigger. Il trigger esegue query sull'elemento dei metadati, aggiornandolo con le informazioni relative all'elemento appena creato.

function updateMetadata() {
    var context = getContext();
    var container = context.getCollection();
    var response = context.getResponse();

    // item that was created
    var createdItem = response.getBody();

    // query for metadata document
    var filterQuery = 'SELECT * FROM root r WHERE r.id = "_metadata"';
    var accept = container.queryDocuments(container.getSelfLink(), filterQuery,
        updateMetadataCallback);
    if(!accept) throw "Unable to update metadata, abort";

    function updateMetadataCallback(err, items, responseOptions) {
        if(err) throw new Error("Error" + err.message);

        if(items.length != 1) throw 'Unable to find metadata document';

        var metadataItem = items[0];

        // update metadata
        metadataItem.createdItems += 1;
        metadataItem.createdNames += " " + createdItem.id;
        var accept = container.replaceDocument(metadataItem._self,
            metadataItem, function(err, itemReplaced) {
                    if(err) throw "Unable to update metadata, abort";
            });

        if(!accept) throw "Unable to update metadata, abort";
        return;
    }
}

Un aspetto importante da tenere presente è l'esecuzione transazionale dei trigger in Azure Cosmos DB. Questo post-trigger viene eseguito come parte della stessa transazione per lo stesso elemento sottostante. Un'eccezione durante l'esecuzione di post-trigger determina l'esito negativo dell'intera transazione. Verrà eseguito il rollback di qualsiasi elemento di cui è stato eseguito il commit e verrà restituita un'eccezione.

Per ottenere esempi su come registrare e chiamare un pre-trigger, vedere pre-trigger e post-trigger.

Come scrivere funzioni definite dall'utente

L'esempio seguente crea una funzione definita dall'utente per calcolare l'imposta sul reddito per varie fasce di reddito. Questa funzione definita dall'utente viene quindi usata all'interno di una query. Ai fini di questo esempio si supponga che ci sia un contenitore denominato Redditi con le proprietà indicate di seguito:

{
   "name": "Daniel Elfyn",
   "country": "USA",
   "income": 70000
}

La seguente definizione di funzione calcola l'imposta sul reddito per varie fasce di reddito:

function tax(income) {
    if (income == undefined)
        throw 'no input';

    if (income < 1000)
        return income * 0.1;
    else if (income < 10000)
        return income * 0.2;
    else
        return income * 0.4;
}

Per ottenere esempi su come registrare e usare una funzione definita dall'utente, consultare Come usare le funzioni definite dall'utente in Azure Cosmos DB.

Registrazione

Quando si usano stored procedure, trigger o funzioni definite dall'utente, è possibile registrare i passaggi abilitando la registrazione degli script. Una stringa per il debug viene generata quando la funzione EnableScriptLogging è impostata su True, come illustrato negli esempi seguenti:

let requestOptions = { enableScriptLogging: true };
const { resource: result, headers: responseHeaders} await container.scripts
      .storedProcedure(Sproc.id)
      .execute(undefined, [], requestOptions);
console.log(responseHeaders[Constants.HttpHeaders.ScriptLogResults]);

Passaggi successivi

Sono disponibili altre informazioni su come scrivere o usare stored procedure, trigger e funzioni definite dall'utente in Azure Cosmos DB: