Partilhar via


Como escrever procedimentos armazenados, triggers e funções definidas pelo utilizador no Azure Cosmos DB

O Azure Cosmos DB oferece execução transacional e integrada em linguagem de JavaScript que lhe permite escrever procedimentos armazenados, triggers e funções definidas pelo utilizador (UDFs). Quando usa a API para NoSQL no Azure Cosmos DB, pode definir os procedimentos armazenados, triggers e UDFs usando JavaScript. Podes escrever a tua lógica em JavaScript e executá-la dentro do motor da base de dados. Pode criar e executar triggers, procedimentos armazenados e UDFs usando o portal Azure, a API de consultas JavaScript no Azure Cosmos DB e o Azure Cosmos DB para SDKs NoSQL.

Para chamar um procedimento armazenado, gatilho ou UDF, necessita de o registar. Para mais informações, veja Como registar e usar procedimentos armazenados, triggers e funções definidas pelo utilizador.

Observação

Para contentores particionados, ao executar um procedimento armazenado, deve ser fornecido um valor de chave de partição nas opções de pedido. Os procedimentos armazenados estão sempre associados a uma chave de partição. Itens que têm um valor de chave de partição diferente não são visíveis para o procedimento armazenado. Isto aplica-se também aos gatilhos.

Observação

Funcionalidades JavaScript do lado do servidor, incluindo procedimentos armazenados, triggers e UDFs, não suportam a importação de módulos.

Sugestão

O Azure Cosmos DB suporta a implementação de containers com stored procedures, triggers e UDFs. Para mais informações, consulte Criar um contentor Azure Cosmos DB com funcionalidade do lado do servidor.

Como escrever procedimentos armazenados

Os procedimentos armazenados são escritos usando JavaScript e podem criar, atualizar, ler, consultar e eliminar itens dentro de um contentor Azure Cosmos DB. Os procedimentos armazenados são registados por coleção e podem operar em qualquer documento ou anexo presente nessa coleção.

Observação

O Azure Cosmos DB tem uma política de cobrança diferente para procedimentos armazenados. Como os procedimentos armazenados podem executar código e consumir qualquer número de unidades de pedido (RUs), cada execução requer uma taxa inicial. Isto garante que os scripts de procedimentos armazenados não afetam os serviços de backend. O montante cobrado inicialmente é igual à cobrança média consumida pelo script nas invocações anteriores. O número médio de RUs por operação é reservado antes da execução. Se as invocações tiverem muita variação nas RUs, a utilização do teu orçamento pode ser afetada. Como alternativa, deve usar pedidos em lote ou em massa em vez de procedimentos armazenados para evitar variações em relação às cobranças de RU.

Aqui está um procedimento simples armazenado que devolve uma resposta "Hello World".

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

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

O objeto de contexto fornece acesso a todas as operações que podem ser realizadas no Azure Cosmos DB, bem como acesso aos objetos de pedido e resposta. Neste caso, usas o objeto de resposta para definir o corpo da resposta a ser enviado de volta ao cliente.

Uma vez escrito, o procedimento armazenado deve ser registado numa coleção. Para saber mais, veja Como usar procedimentos armazenados no Azure Cosmos DB.

Criar itens usando procedimentos armazenados

Quando cria um item usando um procedimento armazenado, o item é inserido no contentor Azure Cosmos DB e um ID para o item recém-criado é devolvido. Criar um item é uma operação assíncrona e depende das funções de callback do JavaScript. A função de callback tem dois parâmetros: um para o objeto de erro caso a operação falhe, e outro para um valor de retorno, neste caso, o objeto criado. Dentro do callback, pode optar por gerir a exceção ou lançar um erro. Se não for fornecido um callback e houver um erro, o runtime da base de dados do Azure Cosmos gera um erro.

O procedimento armazenado inclui também um parâmetro para definir a descrição como um valor booleano. Quando o parâmetro está definido como verdadeiro e a descrição está em falta, o procedimento armazenado lança uma exceção. Caso contrário, o resto do procedimento armazenado continua a executar.

O seguinte exemplo de procedimento armazenado recebe um array de novos itens da base de dados Azure Cosmos como entrada, insere-o no contentor da base de dados Azure Cosmos e devolve a contagem dos itens inseridos. Neste exemplo, estamos a usar o exemplo ToDoList da API Quickstart .NET para 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 como parâmetros de entrada para procedimentos armazenados

Quando defines um procedimento armazenado no portal Azure, os parâmetros de entrada são sempre enviados como uma string para o procedimento armazenado. Mesmo que passe um array de strings como entrada, o array é convertido numa string e enviado para o procedimento armazenado. Para contornar isto, pode definir uma função dentro do seu procedimento armazenado para analisar a cadeia como um array. O código seguinte mostra como analisar um parâmetro de entrada de string como um array:

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

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

Transações dentro de procedimentos armazenados

Pode implementar transações em itens dentro de um contentor usando um procedimento armazenado. O exemplo seguinte utiliza transações dentro de uma aplicação de jogos de fantasy football para trocar jogadores entre duas equipas numa única operação. O procedimento armazenado tenta ler os dois itens da base de dados Azure Cosmos, cada um correspondendo aos IDs dos jogadores passados como argumento. Se ambos os jogadores forem encontrados, o procedimento armazenado atualiza os itens trocando as suas equipas. Se forem encontrados erros pelo caminho, o procedimento armazenado lança uma exceção JavaScript que aborta implicitamente a transação.

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

Execução limitada dentro de procedimentos armazenados

O exemplo seguinte mostra um procedimento armazenado que importa itens em massa para um contentor Azure Cosmos DB. O procedimento armazenado lida com a execução limitada verificando o valor de retorno booleano de createDocument, e depois utiliza a contagem de itens inseridos em cada invocação do procedimento armazenado para monitorizar e retomar o progresso entre os lotes.

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 com procedimentos armazenados

O exemplo seguinte de procedimento armazenado utiliza async/await com Promises usando uma função auxiliar. O procedimento armazenado consulta um item e substitui-o.

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

Como escrever gatilhos

O Azure Cosmos DB suporta gatilhos pré e pós. Os pré-gatilhos são executados antes de modificar um item de base de dados, e os pós-gatilhos são executados após a modificação de um item de base de dados. Os gatilhos não são executados automaticamente. Devem ser especificados para cada operação de base de dados onde pretende que sejam executados. Depois de definir um trigger, deve registar-se e chamar um pré-trigger usando os SDKs da base de dados do Azure Cosmos.

Pré-disparadores

O exemplo seguinte mostra como um pré-trigger é usado para validar as propriedades de um item do Azure Cosmos DB que está a ser criado. Este exemplo usa o exemplo ToDoList da API Quickstart .NET para NoSQL para adicionar uma propriedade de carimbo temporal a um item recém-adicionado se este não contiver um.

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

Os pré-triggers não podem ter parâmetros de entrada. O objeto de pedido no gatilho é usado para manipular a mensagem de pedido associada à operação. No exemplo anterior, o pré-trigger é executado ao criar um item Azure Cosmos DB, e o corpo da mensagem de pedido contém o item a criar em formato JSON.

Quando os gatilhos são registados, pode especificar as operações que ele pode executar. Este gatilho deve ser criado com o TriggerOperation valor de TriggerOperation.Create, o que significa que o uso do gatilho numa operação de substituição não é permitido.

Para obter exemplos de como registar e chamar um pré-acionador, veja pré-acionadores e pós-acionadores.

Pós-gatilhos

O exemplo seguinte mostra um pós-trigger. Este gatilho faz consultas ao item de metadados e atualiza-o com detalhes sobre o item recém-criado.

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

Uma coisa importante a destacar é a execução transacional dos triggers no Azure Cosmos DB. O gatilho pós-processado é executado como parte da mesma transação para o próprio item subjacente. Uma exceção durante a execução pós-gatilho causa a falha de toda a transação. Tudo o que for feito é revertido e uma exceção é devolvida.

Para exemplos de como registar e chamar um pré-trigger, consulte pré-triggers e pós-triggers.

Como escrever funções definidas pelo utilizador

O exemplo seguinte cria um UDF para calcular o imposto sobre o rendimento para vários escalões de rendimento. Esta Função Definida pelo Utilizador (UDF) seria então usada dentro de uma consulta. Para efeitos deste exemplo, suponha que existe um contentor chamado Incomes com as seguintes propriedades:

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

A seguinte definição de função calcula o imposto sobre o rendimento para vários escalões de rendimento:

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

Para exemplos de como registar e usar um UDF, veja Como trabalhar com funções definidas pelo utilizador.

Exploração Florestal

Ao utilizar procedimentos armazenados, gatilhos ou UDFs, pode registar os passos ao ativar o registo de scripts. Uma cadeia para depuração é gerada quando EnableScriptLogging é definida como verdadeira, como mostrado nos seguintes exemplos:

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

Próximos passos