Comment écrire des procédures stockées, des déclencheurs et des fonctions définies par l’utilisateur dans Azure Cosmos DB

S’APPLIQUE À : NoSQL

Azure Cosmos DB fournit une exécution transactionnelle avec langage intégré de JavaScript qui vous permet d’écrire des procédures stockées, des déclencheurs et des fonctions définies par l’utilisateur. Lorsque vous utilisez l’API pour NoSQL dans Azure Cosmos DB, vous pouvez définir les procédures stockées, les déclencheurs et les fonctions définies par l’utilisateur à l’aide de JavaScript. Vous pouvez écrire votre logique dans JavaScript et l’exécuter dans le moteur de base de données. Vous pouvez créer et exécuter des déclencheurs, des procédures stockées et des fonctions définies par l’utilisateur à l’aide du portail Azure, de l’API de requête JavaScript dans Azure Cosmos DB et des SDK Azure Cosmos DB for NoSQL.

Pour appeler une procédure stockée, un déclencheur ou une fonction définie par l’utilisateur (UDF), vous devez l’inscrire. Pour plus d’informations, consultez Comment utiliser des procédures stockées, des déclencheurs et des fonctions définies par l’utilisateur dans Azure Cosmos DB.

Notes

Pour les conteneurs partitionnés, lorsque vous exécutez une procédure stockée, vous devez fournir une valeur de clé de partition dans les options de requête. Les procédures stockées se limitent toujours à une clé de partition. Les éléments qui ont une valeur de clé de partition différente ne sont pas visibles dans la procédure stockée. Cela vaut également pour les déclencheurs.

Notes

Les fonctionnalités JavaScript côté serveur, notamment les procédures stockées, les déclencheurs et les fonctions définies par l’utilisateur, ne prennent pas en charge l’importation de modules.

Conseil

Azure Cosmos DB prend en charge le déploiement de conteneurs avec des procédures stockées, des déclencheurs et des fonctions définies par l’utilisateur. Pour plus d’informations, consultez Créer un conteneur Azure Cosmos DB avec des fonctionnalités côté serveur.

Comment écrire des procédures stockées

Les procédures stockées sont écrites à l’aide de JavaScript. Elles peuvent créer, mettre à jour, lire, interroger et supprimer des éléments à l’intérieur d’un conteneur Azure Cosmos DB. Les procédures stockées sont enregistrées par collection, et elles peuvent s’appliquer à tout document ou pièce jointe figurant dans cette collection.

Notes

Azure Cosmos DB a une stratégie de facturation différente pour les procédures stockées. Étant donné que les procédures stockées peuvent exécuter du code et consommer n’importe quel nombre d’unités de requête (RU), chaque exécution nécessite des frais initiaux. Cela garantit que les scripts de procédure stockée n’ont pas d’impact sur les services back-end. Le montant facturé à l’avance est égal au coût moyen des précédents appels du script. Le nombre moyen d’unités de requête par opération est réservé avant l’exécution. Si les appels entraînent une grande variation des unités de requête, l’utilisation de votre budget peut être affectée. Vous pouvez également utiliser des demandes par lots ou en bloc plutôt que des procédures stockées pour éviter toute variation des frais d’unités de requête.

Voici une simple procédure stockée qui renvoie une réponse « Hello World ».

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

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

L’objet de contexte donne accès à toutes les opérations pouvant être effectuées dans Azure Cosmos DB, ainsi que l’accès aux objets de requête et de réponse. En l’occurrence, vous utilisez l’objet de réponse pour définir le corps de la réponse renvoyée au client.

Une fois écrite, la procédure stockée doit être inscrite auprès d’une collection. Pour en savoir plus, consultez Comment utiliser des procédures stockées dans Azure Cosmos DB.

Créer des éléments avec des procédures stockées

Quand vous créez un élément à l’aide d’une procédure stockée, il est inséré dans le conteneur Azure Cosmos DB et un ID pour l’élément nouvellement créé est retourné. La création d’un élément est une opération asynchrone, et varie selon les fonctions de rappel JavaScript. La fonction de rappel comporte deux paramètres : un pour l’objet d’erreur en cas d’échec de l’opération et un autre pour une valeur de retour ; dans le cas présent, l’objet créé. À l’intérieur du rappel, vous pouvez gérer l’exception ou générer une erreur. Si aucun rappel n’est fourni et qu’une erreur se produit, le runtime Azure Cosmos DB génère une erreur.

La procédure stockée inclut également un paramètre pour définir la description sous forme de valeur booléenne. Lorsque le paramètre est défini sur true et que la description est manquante, la procédure stockée lève une exception. Sinon, le reste de la procédure stockée continue de s’exécuter.

L’exemple de procédure stockée suivant prend un tableau de nouveaux éléments Azure Cosmos DB en tant qu’entrée, l’insère dans le conteneur Azure Cosmos DB et retourne le nombre d’éléments insérés. Dans cet exemple, nous utilisons l’exemple ToDoList mentionné du guide Démarrage rapide : API .NET pour 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);
        }
    }
}

Tableaux en tant que paramètres d’entrée pour les procédures stockées

Quand vous définissez une procédure stockée dans le portail Azure, les paramètres d’entrée sont toujours envoyés sous forme de chaîne à la procédure stockée. Même si vous passez un tableau de chaînes comme entrée, le tableau est converti en chaîne et envoyé à la procédure stockée. Pour contourner cela, vous pouvez définir une fonction à l’intérieur de votre procédure stockée pour que la chaîne soit analysée en tant que tableau. Le code suivant montre comment analyser un paramètre d’entrée de chaîne en tant que tableau :

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

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

Transactions dans des procédures stockées

Vous pouvez implémenter des transactions sur des éléments dans un conteneur à l’aide d’une procédure stockée. L’exemple suivant utilise des transactions au sein d’une application de jeu de football fantastique pour échanger des joueurs entre deux équipes dans une seule opération. La procédure stockée essaie de lire les deux éléments Azure Cosmos DB, qui correspondent chacun aux ID de joueur passés comme argument. Si deux joueurs sont trouvés, la procédure stockée met à jour les éléments en intervertissant leurs équipes. Si des erreurs se produisent, elle génère une exception JavaScript qui annule implicitement la transaction.

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

Exécution liée dans des procédures stockées

Voici un exemple de procédure stockée qui importe en bloc des éléments dans un conteneur Azure Cosmos DB. La procédure stockée gère l’exécution liée en vérifiant la valeur de retour booléenne à partir de createDocument, puis utilise le nombre d’éléments insérés dans chaque appel de la procédure stockée pour effectuer le suivi de la progression et la reprendre d’un lot à un autre.

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

async/await avec des procédures stockées

L’exemple de procédure stockée utilise async/await avec des promesses (Promises) à l’aide d’une fonction d’assistance. La procédure stockée interroge un élément et le remplace.

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

Comment écrire des déclencheurs

Azure Cosmos DB prend en charge les prédéclencheurs et les post-déclencheurs. Les prédéclencheurs sont exécutés avant la modification d’un élément de base de données, et les post-déclencheurs sont exécutés après la modification d’un élément de base de données. Les déclencheurs ne sont pas exécutés automatiquement. Ils doivent être spécifiés pour chaque opération de base de données où vous souhaitez qu’ils s’exécutent. Après avoir défini un déclencheur, vous devez inscrire et appeler un prédéclencheur à l’aide des kits SDK Azure Cosmos DB.

Prédéclencheurs

L’exemple suivant montre comment un prédéclencheur est utilisé pour valider les propriétés d’un élément Azure Cosmos DB en cours de création. Cet exemple utilise l’exemple ToDoList du guide Démarrage rapide : API .NET pour NoSQL pour ajouter une propriété timestamp à un élément nouvellement ajouté qui n’en contient pas.

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

Les prédéclencheurs ne peuvent pas avoir de paramètres d’entrée. L’objet de requête dans le déclencheur est utilisé pour manipuler le message de requête associé à l’opération. Dans l’exemple précédent, le prédéclencheur est exécuté lors de la création d’un élément Azure Cosmos DB, et le corps du message de requête contient l’élément qui doit être créé au format JSON.

Lorsque les déclencheurs sont inscrits, vous pouvez spécifier les opérations avec lesquelles ils peuvent s’exécuter. Ce déclencheur doit être créé avec TriggerOperation.Create comme valeur TriggerOperation, ce qui signifie que l’utilisation du déclencheur dans une opération de remplacement n’est pas autorisée.

Pour obtenir des exemples d’inscription et d’appel d’un prédéclencheur, consultez les sections relatives aux pprédéclencheurs et aux post-déclencheurs.

Post-déclencheurs

L’exemple suivant illustre un post-déclencheur. Ce déclencheur interroge l’élément de métadonnées et le met à jour avec des informations relatives à l’élément qui vient d’être créé.

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 élément important à noter est l’exécution transactionnelle des déclencheurs dans Azure Cosmos DB. Le post-déclencheur s’exécute dans le cadre de la même transaction pour l’élément sous-jacent lui-même. Une exception pendant l’exécution du post-déclencheur fait échouer toute la transaction. Tout ce qui est déjà validé est annulé et une exception est retournée.

Pour obtenir des exemples d’inscription et d’appel d’un prédéclencheur, consultez les sections relatives aux pprédéclencheurs et aux post-déclencheurs.

Comment écrire des fonctions définies par l’utilisateur

L’exemple suivant crée une fonction définie par l’utilisateur pour calculer les impôts sur le revenu en fonction de diverses tranches de revenu. Cette fonction définie par l’utilisateur peut ensuite être utilisée dans une requête. Dans le cadre de cet exemple, supposons qu’il existe un conteneur appelé Incomes (Revenus) avec des propriétés semblables à ce qui suit :

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

La définition de fonction suivante calcule les impôts sur le revenu en fonction de diverses tranches de revenu :

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

Pour obtenir des exemples de la façon d’inscrire une fonction définie par l’utilisateur, consultez Comment utiliser des fonctions définies par l’utilisateur dans Azure Cosmos DB.

Journalisation

Lorsque vous utilisez une procédure stockée, des déclencheurs ou des fonctions définies par l’utilisateur, vous pouvez journaliser les étapes en activant la journalisation des scripts. Une chaîne de débogage est générée lorsque EnableScriptLogging a la valeur true, comme indiqué dans les exemples suivants :

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

Étapes suivantes

Découvrez plus de concepts et la façon d’écrire et d’utiliser des procédures stockées, des déclencheurs et des fonctions définies par l’utilisateur dans Azure Cosmos DB :