Schreiben von gespeicherten Prozeduren, Triggern und benutzerdefinierten Funktionen in Azure Cosmos DB

GILT FÜR: NoSQL

Azure Cosmos DB bietet eine in die Sprache integrierte transaktionale Ausführung von JavaScript, mit der Sie gespeicherte Prozeduren, Trigger und benutzerdefinierte Funktionen (User Defined Functions, UDFs) schreiben können. Wenn Sie die API für NoSQL in Azure Cosmos DB verwenden, können Sie die gespeicherten Prozeduren, Trigger und UDFs mithilfe von JavaScript definieren. Sie können Ihre Logik in JavaScript schreiben und in der Datenbank-Engine ausführen. Sie können Trigger, gespeicherte Prozeduren und benutzerdefinierte Funktionen im Azure-Portal, mithilfe der JavaScript-Abfrage-API in Azure Cosmos DB und mit den Azure Cosmos DB for NoSQL SDKs erstellen und ausführen.

Um eine gespeicherte Prozedur, einen Trigger oder eine UDF aufzurufen, müssen Sie sie bzw. ihn registrieren. Weitere Informationen finden Sie unter Arbeiten mit gespeicherten Prozeduren, Triggern und benutzerdefinierten Funktionen in Azure Cosmos DB.

Hinweis

Wenn Sie eine gespeicherte Prozedur in partitionierten Containern ausführen, muss in den Anforderungsoptionen ein Partitionsschlüsselwert angegeben werden. Gespeicherte Prozeduren gelten immer für einen bestimmten Partitionsschlüssel. Elemente, die einen anderen Partitionsschlüsselwert aufweisen, sind in der gespeicherten Prozedur nicht sichtbar. Dies gilt auch für Trigger.

Hinweis

Serverseitige JavaScript-Features, einschließlich gespeicherter Prozeduren, Trigger und UDFs, unterstützen das Importieren von Modulen nicht.

Tipp

Azure Cosmos DB unterstützt die Bereitstellung von Containern mit gespeicherten Prozeduren, Triggern und UDFs. Weitere Informationen finden Sie unter Erstellen eines Azure Cosmos DB-Containers mit serverseitiger Funktionalität.

Schreiben gespeicherter Prozeduren

Gespeicherte Prozeduren werden mithilfe von JavaScript geschrieben und können Elemente in einem Azure Cosmos DB-Container erstellen, aktualisieren, lesen, abfragen und löschen. Gespeicherte Prozeduren werden pro Auflistung registriert und können auf beliebige Dokumente und Anhänge angewendet werden, die sich in dieser Auflistung befinden.

Hinweis

Azure Cosmos DB verfügt über eine andere Gebührenrichtlinie für gespeicherte Prozeduren. Da gespeicherte Prozeduren Code ausführen und eine beliebige Anzahl von Anforderungseinheiten (Request Units, RUs) nutzen können, ist für jede Ausführung eine Vorabzahlung erforderlich. Dadurch wird sichergestellt, dass Skripts für gespeicherte Prozeduren keine Auswirkungen auf Back-End-Dienste haben. Der Betrag, der im Voraus in Rechnung gestellt wird, entspricht der durchschnittlichen Gebühr, die das Skript in früheren Aufrufen verbraucht hat. Die durchschnittlichen RUs pro Vorgang werden vor der Ausführung reserviert. Wenn die Aufrufe eine große Abweichung bei den RUs aufweisen, kann sich dies auf die Budgetnutzung auswirken. Alternativ sollten Sie Batch- oder Massenanforderungen anstelle von gespeicherten Prozeduren verwenden, um Abweichungen bei RU-Gebühren zu vermeiden.

Hier ist eine einfache gespeicherte Prozedur, die die Antwort „Hello World“ zurückgibt.

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

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

Das Kontextobjekt bietet Zugriff auf alle Vorgänge, die in Azure Cosmos DB ausgeführt werden können, sowie Zugriff auf die Anforderungs- und Antwortobjekte. In diesem Fall verwenden Sie das Antwortobjekt dazu, den Text der Antwort festzulegen, der an den Client zurückgesendet werden soll.

Nachdem eine gespeicherte Prozedur geschrieben wurde, muss sie bei einer Auflistung registriert werden. Weitere Informationen finden Sie unter Verwenden von gespeicherten Prozeduren in Azure Cosmos DB.

Erstellen von Elementen mithilfe gespeicherter Prozeduren

Wenn Sie ein Element mithilfe einer gespeicherten Prozedur erstellen, wird dieses Element in den Azure Cosmos DB-Container eingefügt, und es wird eine ID für das neu erstellte Element zurückgegeben. Das Erstellen eines Elements ist ein asynchroner Vorgang und hängt von den JavaScript-Rückruffunktionen ab. Die Rückruffunktion verfügt über zwei Parameter: einen für das Fehlerobjekt für den Fall eines Fehlers im Vorgang und einen anderen für einen Rückgabewert, in diesem Fall für das erstellte Objekt. Innerhalb des Rückrufs können Sie entweder die Ausnahme behandeln oder einen Fehler auslösen. Falls ein Rückruf nicht bereitgestellt wird und ein Fehler auftritt, löst die Azure Cosmos DB-Laufzeitumgebung einen Fehler aus.

Die gespeicherte Prozedur enthält auch einen Parameter zum Festlegen der Beschreibung. Hierbei handelt es sich um einen booleschen Wert. Wenn der Parameter auf TRUE festgelegt wird und die Beschreibung fehlt, löst die gespeicherte Prozedur eine Ausnahme aus. Andernfalls wird der Rest der gespeicherten Prozedur weiter ausgeführt.

Das folgende Beispiel einer gespeicherten Prozedur akzeptiert ein Array neuer Azure Cosmos DB-Elemente als Eingabe, fügt sie in den Azure Cosmos DB-Container ein und gibt die Anzahl der eingefügten Elemente zurück. In diesem Beispiel nutzen wir das To-Do-Listen-Beispiel aus der Schnellstartanleitung zur .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);
        }
    }
}

Arrays als Eingabeparameter für gespeicherte Prozeduren

Beim Definieren einer gespeicherten Prozedur im Azure-Portal werden Eingabeparameter immer als Zeichenfolge an die gespeicherte Prozedur gesendet. Selbst wenn Sie ein Array von Zeichenfolgen als Eingabe übergeben, wird das Array in eine Zeichenfolge konvertiert und an die gespeicherte Prozedur gesendet. Zur Umgehung dieses Problems können Sie eine Funktion in Ihrer gespeicherten Prozedur definieren, mit der die Zeichenfolge als Array analysiert wird. Der folgende Code zeigt, wie ein Zeichenfolgen-Eingabeparameter als Array analysiert wird:

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

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

Transaktionen in gespeicherten Prozeduren

Sie können Transaktionen mithilfe einer gespeicherten Prozedur in Elementen innerhalb eines Containers implementieren. Das folgende Beispiel verwendet Transaktionen in einer Football-Spiele-App, um Spieler in einem einzigen Vorgang zwischen zwei Mannschaften zu transferieren. Die gespeicherte Prozedur versucht, die beiden Azure Cosmos DB-Elemente zu lesen, die jeweils den als Argument übergebenen Spieler-IDs entsprechen. Wenn beide Spieler gefunden werden, aktualisiert die gespeicherte Prozedur die Elemente, indem die Mannschaften getauscht werden. Wenn im Verlauf des Vorgangs Fehler auftreten, löst die gespeicherte Prozedur eine JavaScript-Ausnahme aus, die die Transaktion implizit abbricht.

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

Gebundene Ausführung innerhalb von gespeicherten Prozeduren

Im Folgenden finden Sie ein Beispiel einer gespeicherten Prozedur, die Elemente per Massenvorgang in einen Azure Cosmos DB-Container importiert. Die gespeicherte Prozedur verarbeitet die gebundene Ausführung, indem sie den booleschen Rückgabewert von createDocument prüft und dann die Anzahl der Elemente verwendet, die bei jedem Aufruf der gespeicherten Prozedur eingefügt werden, um den batchübergreifenden Fortschritt nachzuverfolgen und zu übernehmen.

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 mit gespeicherten Prozeduren

Im folgenden Beispiel für eine gespeicherte Prozedur wird async/await mit Promises unter Verwendung einer Hilfsfunktion verwendet. Die gespeicherte Prozedur fragt ein Element ab und ersetzt es.

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

Schreiben von Triggern

Azure Cosmos DB unterstützt vorangestellte und nachgestellte Trigger. Vorangestellte Trigger werden vor dem Ändern eines Datenbankelements ausgeführt, nachgestellte Trigger danach. Trigger werden nicht automatisch ausgeführt. Sie müssen für jeden Datenbankvorgang angegeben werden, in dem sie ausgeführt werden sollen. Nach dem Definieren eines Triggers sollten Sie mithilfe der Azure Cosmos DB SDKs einen vorangestellten Trigger registrieren und aufrufen.

Vorangestellte Trigger

Das folgende Beispiel zeigt, wie ein vorangestellter Trigger zum Überprüfen der Eigenschaften eines Azure Cosmos DB-Elements verwendet werden kann, das erstellt wird. In diesem Beispiel nutzen wir das To-Do-Listen-Beispiel aus der Schnellstartanleitung zur .NET-API für NoSQL, um eine timestamp-Eigenschaft einem neu hinzugefügten Element hinzuzufügen, falls dieses keine solche Eigenschaft enthält.

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

Vorangestellte Trigger können keine Eingabeparameter übernehmen. Das Anforderungsobjekt im Trigger kann verwendet werden, um die Anforderungsnachricht zu verändern, die dem Vorgang zugeordnet ist. Im vorherigen Beispiel wird der vorangestellte Trigger ausgeführt, wenn ein Azure Cosmos DB-Element erstellt wird, und der Text der Anforderungsnachricht enthält das zu erstellende Element im JSON-Format.

Wenn Trigger registriert werden, können Sie die Vorgänge angeben, mit denen sie ausgeführt werden können. Dieser Trigger sollte mit TriggerOperation als TriggerOperation.Create-Wert erstellt werden, d. h., die Verwendung des Triggers in einem Ersetzungsvorgang ist nicht zulässig.

Beispiele für das Registrieren und Aufrufen eines vorangestellten Triggers finden Sie unter Ausführen von vorangestellten Triggern und Ausführen von nachgestellten Triggern.

Nachgestellte Trigger

Das folgende Beispiel zeigt einen nachgestellten Trigger. Dieser Trigger fragt das Metadatenelement ab und aktualisiert es mit den Details zum neu erstellten Element.

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

Ein wichtiger Aspekt, den es zu beachten gilt, ist die transaktionale Ausführung von Triggern in Azure Cosmos DB. Der nachgestellte Trigger wird im Rahmen der gleichen Transaktion für das zugrunde liegende Element ausgeführt. Tritt während der Ausführung des nachgestellten Triggers eine Ausnahme auf, ist die gesamte Transaktion nicht erfolgreich. Sämtliche Commits werden rückgängig gemacht, und eine Ausnahme wird zurückgegeben.

Beispiele für das Registrieren und Aufrufen eines vorangestellten Triggers finden Sie unter Ausführen von vorangestellten Triggern und Ausführen von nachgestellten Triggern.

Schreiben von benutzerdefinierten Funktionen

Das folgende Beispiel erstellt eine benutzerdefinierte Funktion, um die Einkommenssteuer für verschiedene Einkommensgruppen zu berechnen. Diese benutzerdefinierte Funktion wird dann innerhalb einer Abfrage verwendet. Nehmen wir für dieses Beispiel an, dass ein Container namens Incomes mit folgenden Eigenschaften vorhanden ist:

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

Die folgende Funktionsdefinition berechnet die Einkommenssteuer für verschiedene Einkommensgruppen:

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

Beispiele für das Registrieren und Verwenden einer UDF finden Sie unter Verwenden von benutzerdefinierten Funktionen in Azure Cosmos DB.

Protokollierung

Wenn Sie gespeicherte Prozeduren, Trigger oder UDFs verwenden, können Sie die Schritte durch die Aktivierung der Skriptprotokollierung protokollieren. Eine Zeichenfolge für das Debuggen wird generiert, wenn EnableScriptLogging auf true festgelegt ist, wie in den folgenden Beispielen gezeigt:

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ächste Schritte

Lernen Sie weitere Konzepte und Vorgehensweisen zum Schreiben und Verwenden von gespeicherten Prozeduren, Triggern und UDFs in Azure Cosmos DB kennen: