Dela via


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

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 (UDF:er). 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 Registrera och använda lagrade procedurer, utlösare och användardefinierade funktioner.

Anmärkning

När du kör en lagrad procedur för 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.

Anmärkning

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

Tips/Råd

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.

Så här skriver du 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 på valfritt dokument eller en bifogad fil som finns i samlingen.

Anmärkning

Azure Cosmos DB har en annan debiteringsprincip för lagrade procedurer. Eftersom lagrade procedurer kan köra kod och förbruka vilket antal som helst av enheter för begäranden (RU:er), kräver varje körning en förskottsavgift. Detta säkerställer att lagrade procedurskript inte påverkar serverdelstjänster. Det belopp som tas ut i förväg motsvarar den genomsnittliga kostnaden som skriptet har förbrukat vid tidigare anrop. Det genomsnittliga antalet RU per operation reserveras innan körningen. Om anropen har mycket variation 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 använder du svarsobjektet för att ange brödtexten för svaret som ska skickas tillbaka till klienten.

När den lagrade proceduren har skrivits måste den registreras i 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. Att skapa ett objekt är en asynkron åtgärd och beror på JavaScript-återanropsfunktionerna. Å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 återanropet 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. Annars 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. För att kringgå detta kan du definiera en funktion i den lagrade proceduren för att parsa 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 inom lagrade procedurer

Du kan implementera transaktioner på objekt i en container med hjälp av en lagrad procedur. I följande exempel används transaktioner i en fantasyfotbollsspelapp 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, där varje motsvarar ett spelar-ID som skickades in som ett argument. Om båda spelarna hittas uppdaterar den lagrade proceduren objekten genom att byta ut deras lag. Om det uppstår fel längs vägen 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 exekvering i lagrade procedurer

I följande exempel visas 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 createDocumentoch använder sedan antalet objekt som infogas i varje anrop av den lagrade proceduren för att spåra och återuppta förloppet mellan 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å en lagrad procedur använder async/await tillsammans med Promises med hjälp av en hjälpfunktion. 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));
}

Så här skapar du utlösare

Azure Cosmos DB stöder förutlösare 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 ändra begärandemeddelandet som är associerat med åtgärden. I föregående exempel körs förutlösaren när du skapar ett Azure Cosmos DB-objekt, och meddelandetexten för begäran innehåller det objekt som ska skapas i JSON-format.

När utlösare registreras kan du ange de åtgärder som kan användas med dem. 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 förutlösare och efterutlösare.

Efterutlösare

I följande exempel visas 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 den transaktionella körningen av triggers i Azure Cosmos DB. Efterutlösaren körs som en del av samma transaktion för själva det underliggande objektet. Ett undantag under utförandet av efterutlösaren orsakar misslyckande av hela transaktionen. Allt som har begåtts rullas tillbaka och ett undantag returneras.

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

Så här skriver du användardefinierade funktioner

Följande exempel skapar en UDF för att beräkna inkomstskatt för olika inkomstnivåer. Den här UDF:n skulle sedan användas i en sökfrå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.

Loggning / Skogsavverkning

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