Skriva lagrade procedurer, utlösare och användardefinierade funktioner i Azure Cosmos DB

GÄLLER FÖR: NoSQL

Azure Cosmos DB tillhandahåller språkintegrerad, transaktionell körning av JavaScript som gör att du kan skriva lagrade procedurer, utlösare och användardefinierade funktioner. När du använder API:et för NoSQL i Azure Cosmos DB kan du definiera lagrade procedurer, utlösare och UDF:er med hjälp av JavaScript. Du kan skriva logiken i JavaScript och köra den i databasmotorn. Du kan skapa och köra utlösare, lagrade procedurer och UDF:er med hjälp av Azure-portalen, JavaScript-fråge-API:et i Azure Cosmos DB och Azure Cosmos DB för NoSQL SDK:er.

Om du vill anropa en lagrad procedur, utlösare eller UDF måste du registrera den. Mer information finns i Så här arbetar du med lagrade procedurer, utlösare och användardefinierade funktioner i Azure Cosmos DB.

Kommentar

När du kör en lagrad procedur med partitionerade containrar måste ett partitionsnyckelvärde anges i alternativen för begäran. Lagrade procedurer är alltid begränsade till en partitionsnyckel. Objekt som har ett annat partitionsnyckelvärde är inte synliga för den lagrade proceduren. Detta gäller även för utlösare.

Kommentar

JavaScript-funktioner på serversidan, inklusive lagrade procedurer, utlösare och UDF:er, stöder inte import av moduler.

Dricks

Azure Cosmos DB stöder distribution av containrar med lagrade procedurer, utlösare och UDF:er. Mer information finns i Skapa en Azure Cosmos DB-container med funktioner på serversidan.

Skriva lagrade procedurer

Lagrade procedurer skrivs med JavaScript och de kan skapa, uppdatera, läsa, fråga och ta bort objekt i en Azure Cosmos DB-container. Lagrade procedurer registreras per samling och kan användas med alla dokument eller bifogade filer i den samlingen.

Kommentar

Azure Cosmos DB har en annan debiteringsprincip för lagrade procedurer. Eftersom lagrade procedurer kan köra kod och använda valfritt antal enheter för begäranden (RU: er), kräver varje körning en startavgift. Detta säkerställer att lagrade procedurskript inte påverkar serverdelstjänster. Det belopp som debiteras i förväg motsvarar den genomsnittliga debitering som används av skriptet i tidigare anrop. Den genomsnittliga RU:er per åtgärd är reserverad före körning. Om anropen har stor varians i RU:er kan din budgetanvändning påverkas. Alternativt bör du använda batch- eller massbegäranden i stället för lagrade procedurer för att undvika avvikelser kring RU-avgifter.

Här är en enkel lagrad procedur som returnerar ett "Hello World"-svar.

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

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

Kontextobjektet ger åtkomst till alla åtgärder som kan utföras i Azure Cosmos DB, samt åtkomst till begärande- och svarsobjekten. I det här fallet kan du använda svarsobjekt för att ange svarsinnehållet som ska skickas tillbaka till klienten.

När den har skrivits måste den lagrade proceduren registreras med en samling. Mer information finns i Använda lagrade procedurer i Azure Cosmos DB.

Skapa objekt med lagrade procedurer

När du skapar ett objekt med en lagrad procedur infogas objektet i Azure Cosmos DB-containern och ett ID för det nyligen skapade objektet returneras. Genereringen av ett objekt är en asynkron åtgärd och är beroende av JavaScript-motringningsfunktionerna. Återanropsfunktionen har två parametrar: en för felobjektet om åtgärden misslyckas och en annan för ett returvärde, i det här fallet det skapade objektet. I motringningen kan du antingen hantera undantaget eller utlösa ett fel. Om ett återanrop inte har angetts och det finns ett fel genererar Azure Cosmos DB-körningen ett fel.

Den lagrade proceduren innehåller också en parameter för att ange beskrivningen som ett booleskt värde. När parametern är inställd på true och beskrivningen saknas utlöser den lagrade proceduren ett undantag. I annat fall fortsätter resten av den lagrade proceduren att köras.

Följande exempel på en lagrad procedur tar en matris med nya Azure Cosmos DB-objekt som indata, infogar den i Azure Cosmos DB-containern och returnerar antalet infogade objekt. I det här exemplet använder vi ToDoList-exemplet från snabbstartens .NET API för 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);
        }
    }
}

Matriser som indataparametrar för lagrade procedurer

När du definierar en lagrad procedur i Azure-portalen skickas alltid indataparametrar som en sträng till den lagrade proceduren. Även om du skickar en matris med strängar som indata konverteras matrisen till en sträng och skickas till den lagrade proceduren. Du kan undvika detta genom att definiera en funktion i den lagrade proceduren som parsar strängen som en matris. Följande kod visar hur du parsar en strängindataparameter som en matris:

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

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

Transaktioner i lagrade procedurer

Du kan implementera transaktioner för objekt i en container med hjälp av en lagrad procedur. I följande exempel används transaktioner i en fotbollsapp för att byta spelare mellan två lag i en enda åtgärd. Den lagrade proceduren försöker läsa de två Azure Cosmos DB-objekten, som var och en motsvarar de spelar-ID:t som skickades som ett argument. Om båda spelarna hittas uppdaterar den lagrade proceduren objekten genom att byta deras lag. Om det uppstår ett fel utlöser den lagrade proceduren ett JavaScript-undantag som implicit avbryter transaktionen.

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

    var player1Item, player2Item;

    // 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 player 1";
            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 player 2";
                    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";
    }
}

Begränsad körning i lagrade procedurer

Följande är ett exempel på en lagrad procedur som massimportera objekt till en Azure Cosmos DB-container. Den lagrade proceduren hanterar begränsad körning genom att kontrollera det booleska returvärdet från createDocument, och använder sedan antalet objekt som infogats i varje anrop av den lagrade proceduren för att spåra och återuppta förloppet över batchar.

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

    // The count of imported items, also used as the 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, the callback will be called.
        // Otherwise report the current count back to the client,
        // which will call the script again with the 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 the next document.
            tryCreate(items[count], callback);
        }
    }
}

Async/await med lagrade procedurer

Följande exempel på lagrad procedur används async/await med Promises med hjälpfunktionen. Den lagrade proceduren frågar efter ett objekt och ersätter det.

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));
}

Skriva utlösare

Azure Cosmos DB stöder för- och efterutlösare. Förutlösare körs innan du ändrar ett databasobjekt, och efterutlösare körs efter att ett databasobjekt har modifierats. Utlösare körs inte automatiskt. De måste anges för varje databasåtgärd där du vill att de ska köras. När du har definierat en utlösare bör du registrera och anropa en förutlösare med hjälp av Azure Cosmos DB SDK:er.

Förutlösare

I följande exempel visas hur en förutlösare används för att verifiera egenskaperna för ett Azure Cosmos DB-objekt som skapas. I det här exemplet används ToDoList-exemplet från .NET API för snabbstart för NoSQL för att lägga till en tidsstämpelegenskap i ett nyligen lagt objekt om det inte innehåller något.

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);
}

Förutlösare kan inte ha några indataparametrar. Begärandeobjektet i utlösaren används för att manipulera begärandemeddelandet som är associerat med åtgärden. I exemplet ovan körs förutlösaren när ett Azure Cosmos DB-objekt skapas, och själva begärandemeddelandet innehåller objektet som ska skapas i JSON-format.

När utlösare har registrerats kan du ange vilka åtgärder som de kan köras med. Den här utlösaren ska skapas med värdet TriggerOperationTriggerOperation.Create, vilket innebär att det inte är tillåtet att använda utlösaren i en ersättningsåtgärd.

Exempel på hur du registrerar och anropar en förutlösare finns i pre-triggers och post-triggers.

Efterutlösare

Följande exempel visar en efterutlösare. Den här utlösaren frågar efter metadataobjektet och uppdaterar det med information om det nyligen skapade objektet.

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;
    }
}

En sak som är viktig att notera är transaktionskörningen av utlösare i Azure Cosmos DB. Efterutlösaren körs som en del av samma transaktion för själva det underliggande objektet. Ett undantag under körningen efter utlösaren misslyckas med hela transaktionen. Allt som har checkats in återställs och ett undantag returneras.

Exempel på hur du registrerar och anropar en förutlösare finns i pre-triggers och post-triggers.

Skriva användardefinierade funktioner

Följande exempel skapar en användardefinierad funktion som beräknar inkomstskatten för olika inkomstintervall. Den här användardefinierade funktionen används sedan i en fråga. I det här exemplet förutsätter vi att det finns en container med namnet Intäkter med egenskaper enligt följande:

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

Följande funktionsdefinition beräknar inkomstskatt för olika inkomstparenteser:

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;
}

Exempel på hur du registrerar och använder en UDF finns i Så här arbetar du med användardefinierade funktioner i Azure Cosmos DB.

Loggning

När du använder lagrade procedurer, utlösare eller UDF:er kan du logga stegen genom att aktivera skriptloggning. En sträng för felsökning genereras när EnableScriptLogging är inställd på true, som du ser i följande exempel:

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

Nästa steg

Lär dig mer om begrepp och hur du skriver eller använder lagrade procedurer, utlösare och UDF:er i Azure Cosmos DB: