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

APLICA-SE A: NoSQL

O Azure Cosmos DB fornece uma execução transacional integrada em linguagem do JavaScript que lhe permite escrever procedimentos armazenados, acionadores e funções definidas pelo utilizador (UDFs). Quando utiliza a API para NoSQL no Azure Cosmos DB, pode definir os procedimentos armazenados, acionadores e UDFs com JavaScript. Pode escrever a sua lógica no JavaScript e executá-la dentro do motor de base de dados. Pode criar e executar acionadores, procedimentos armazenados e UDFs com o portal do Azure, a API de consulta JavaScript no Azure Cosmos DB e os SDKs do Azure Cosmos DB para NoSQL.

Para chamar um procedimento armazenado, acionador ou UDF, tem de registá-lo. Para obter mais informações, veja Como trabalhar com procedimentos armazenados, acionadores e funções definidas pelo utilizador no Azure Cosmos DB.

Nota

Para contentores particionados, ao executar um procedimento armazenado, tem de ser fornecido um valor de chave de partição nas opções de pedido. Os procedimentos armazenados estão sempre no âmbito de uma chave de partição. Os itens com um valor de chave de partição diferente não são visíveis para o procedimento armazenado. Isto também se aplica aos acionadores.

Nota

As funcionalidades javaScript do lado do servidor, incluindo procedimentos armazenados, acionadores e UDFs, não suportam a importação de módulos.

Dica

O Azure Cosmos DB suporta a implementação de contentores com procedimentos armazenados, acionadores e UDFs. Para obter mais informações, veja Criar um contentor do Azure Cosmos DB com a funcionalidade do lado do servidor.

Como escrever procedimentos armazenados

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

Nota

O Azure Cosmos DB tem uma política de carregamento diferente para procedimentos armazenados. Uma vez que os procedimentos armazenados podem executar código e consumir qualquer número de unidades de pedido (RUs), cada execução requer um custo inicial. Isto garante que os scripts de procedimentos armazenados não afetam os serviços de back-end. O montante cobrado antecipadamente é igual ao custo médio consumido pelo script em invocações anteriores. As RUs médias por operação são reservadas antes da execução. Se as invocações tiverem muita variância nas RUs, a utilização do orçamento poderá ser afetada. Como alternativa, deve utilizar pedidos em lote ou em massa em vez de procedimentos armazenados para evitar a variação em torno dos custos de RU.

Eis um procedimento armazenado simples 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, vai utilizar o objeto de resposta para definir o corpo da resposta a ser enviado de volta para o cliente.

Depois de escrito, o procedimento armazenado tem de ser registado numa coleção. Para saber mais, veja Como utilizar procedimentos armazenados no Azure Cosmos DB.

Criar itens com procedimentos armazenados

Quando cria um item através de um procedimento armazenado, o item é inserido no contentor do Azure Cosmos DB e é devolvido um ID para o item criado recentemente. A criação de um item é uma operação assíncrona e depende das funções de chamada de retorno javaScript. A função de chamada de retorno tem dois parâmetros: um para o objeto de erro, caso a operação falhe e outro para um valor devolvido, neste caso, o objeto criado. Dentro da chamada de retorno, pode processar a exceção ou gerar um erro. Se não for fornecida uma chamada de retorno e existir um erro, o runtime do Azure Cosmos DB gera um erro.

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

O exemplo seguinte de um procedimento armazenado utiliza uma matriz de novos itens do Azure Cosmos DB como entrada, insere-o no contentor do Azure Cosmos DB e devolve a contagem dos itens inseridos. Neste exemplo, estamos a utilizar o exemplo ToDoList da API .NET de Início Rápido 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);
        }
    }
}

Matrizes como parâmetros de entrada para procedimentos armazenados

Quando define um procedimento armazenado no portal do Azure, os parâmetros de entrada são sempre enviados como uma cadeia para o procedimento armazenado. Mesmo que transmita uma matriz de cadeias como uma entrada, a matriz é convertida numa cadeia e enviada para o procedimento armazenado. Para contornar esta situação, pode definir uma função no seu procedimento armazenado para analisar a cadeia como uma matriz. O código seguinte mostra como analisar um parâmetro de entrada de cadeia como uma matriz:

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

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

Transações nos procedimentos armazenados

Pode implementar transações em itens num contentor através de um procedimento armazenado. O exemplo seguinte utiliza transações numa aplicação de jogos de futebol de fantasia para trocar jogadores entre duas equipas numa única operação. O procedimento armazenado tenta ler os dois itens do Azure Cosmos DB, cada um correspondente aos IDs de leitor transmitidos como argumento. Se ambos os jogadores forem encontrados, o procedimento armazenado atualiza os itens ao trocar as respetivas equipas. Se forem encontrados erros ao longo do caminho, o procedimento armazenado gera uma exceção JavaScript que aborta implicitamente a transação.

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

Execução limitada nos procedimentos armazenados

Segue-se um exemplo de um procedimento armazenado que importa itens em massa para um contentor do Azure Cosmos DB. O procedimento armazenado processa a execução vinculada ao verificar o valor booleano devolvido de createDocumente, em seguida, utiliza a contagem de itens inseridos em cada invocação do procedimento armazenado para controlar e retomar o progresso em lotes.

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

Assíncrona/aguardar com os procedimentos armazenados

O seguinte exemplo de procedimento armazenado utiliza async/await com Promessas através de 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 acionadores

O Azure Cosmos DB suporta pré-acionadores e pós-acionadores. Os pré-acionadores são executados antes de modificar um item de base de dados e os pós-acionadores são executados após modificar um item de base de dados. Os acionadores não são executados automaticamente. Têm de ser especificadas para cada operação de base de dados onde pretende que sejam executadas. Depois de definir um acionador, deve registar e chamar um pré-acionador com os SDKs do Azure Cosmos DB.

Pré-acionadores

O exemplo seguinte mostra como um pré-acionador é utilizado para validar as propriedades de um item do Azure Cosmos DB que está a ser criado. Este exemplo utiliza o exemplo ToDoList da API .NET de Início Rápido para NoSQL para adicionar uma propriedade de carimbo de data/hora a um item adicionado recentemente se 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é-acionadores não podem ter parâmetros de entrada. O objeto de pedido no acionador é utilizado para manipular a mensagem de pedido associada à operação. No exemplo anterior, o pré-acionador é executado ao criar um item do Azure Cosmos DB e o corpo da mensagem de pedido contém o item a ser criado no formato JSON.

Quando os acionadores são registados, pode especificar as operações com as quais pode ser executado. Este acionador deve ser criado com um TriggerOperation valor de TriggerOperation.Create, o que significa que a utilização do acionador numa operação de substituição não é permitida.

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

Pós-acionadores

O exemplo seguinte mostra um pós-acionador. Este acionador consulta o item de metadados e atualiza-o com detalhes sobre o item criado recentemente.

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 ter em conta é a execução transacional de acionadores no Azure Cosmos DB. O pós-acionador é executado como parte da mesma transação para o próprio item subjacente. Uma exceção durante a execução pós-acionador falha na transação completa. Tudo o que for consolidado é revertido e é devolvida uma exceção.

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

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 seria então utilizada dentro de uma consulta. Para efeitos deste exemplo, suponha que existe um contentor chamado Rendimentos com propriedades da seguinte forma:

{
   "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 obter exemplos de como registar e utilizar um UDF, veja Como trabalhar com funções definidas pelo utilizador no Azure Cosmos DB.

Registo

Ao utilizar procedimentos armazenados, acionadores ou UDFs, pode registar os passos ao ativar o registo de scripts. É gerada uma cadeia de carateres para depuração quando EnableScriptLogging está definida como verdadeira, conforme 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]);

Passos seguintes

Saiba mais conceitos e como escrever ou utilizar procedimentos armazenados, acionadores e UDFs no Azure Cosmos DB: