Como escrever procedimentos armazenados, gatilhos e funções definidas pelo usuário no Azure Cosmos DB

APLICA-SE A: NoSQL

O Azure Cosmos DB fornece execução transacional integrada à linguagem de JavaScript que permite escrever procedimentos armazenados, gatilhos e funções definidas pelo usuário (UDFs). Ao usar a API para NoSQL no Azure Cosmos DB, você pode definir os procedimentos armazenados, gatilhos e UDFs usando JavaScript. Você pode escrever sua lógica em JavaScript e executá-la dentro do mecanismo de banco de dados. Você pode criar e executar gatilhos, procedimentos armazenados e UDFs usando o portal do Azure, a API de consulta JavaScript no Azure Cosmos DB e o Azure Cosmos DB para SDKs NoSQL.

Para chamar um procedimento armazenado, gatilho ou UDF, você precisa registrá-lo. Para obter mais informações, consulte Como trabalhar com procedimentos armazenados, gatilhos e funções definidas pelo usuário no Azure Cosmos DB.

Nota

Para contêineres particionados, ao executar um procedimento armazenado, um valor de chave de partição deve ser fornecido nas opções de solicitação. Os procedimentos armazenados são sempre definidos como uma chave de partição. Os itens que têm um valor de chave de partição diferente não são visíveis para o procedimento armazenado. Isso também se aplica aos gatilhos.

Nota

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

Gorjeta

O Azure Cosmos DB dá suporte à implantação de contêineres com procedimentos armazenados, gatilhos e UDFs. Para obter mais informações, consulte Criar um contêiner do Azure Cosmos DB com funcionalidade do lado do servidor.

Como escrever procedimentos armazenados

Os procedimentos armazenados são escritos em JavaScript e podem criar, atualizar, ler, consultar e excluir itens dentro de um contêiner do Azure Cosmos DB. Os procedimentos armazenados são registrados por coleção e podem operar em qualquer documento ou anexo presente nessa coleção.

Nota

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 solicitação (RUs), cada execução requer uma cobrança inicial. Isso garante que os scripts de procedimento armazenado não afetem os serviços de back-end. O valor cobrado antecipadamente é igual à cobrança média consumida pelo script em invocações anteriores. A média de RUs por operação é reservada antes da execução. Se as invocações tiverem muita variação nas RUs, a utilização do orçamento poderá ser afetada. Como alternativa, você deve usar solicitações em lote ou em massa em vez de procedimentos armazenados para evitar variação em torno das cobranças de RU.

Aqui está um procedimento armazenado simples que retorna 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 executadas no Azure Cosmos DB, bem como acesso aos objetos de solicitação e resposta. Nesse caso, você usa o objeto response para definir o corpo da resposta a ser enviado de volta ao cliente.

Uma vez escrito, o procedimento armazenado deve ser registrado com uma coleção. Para saber mais, consulte Como usar procedimentos armazenados no Azure Cosmos DB.

Criar itens usando procedimentos armazenados

Quando você cria um item usando um procedimento armazenado, o item é inserido no contêiner do Azure Cosmos DB e uma ID para o item recém-criado é retornada. Criar um item é uma operação assíncrona e depende das funções de retorno de chamada JavaScript. A função de retorno de chamada tem dois parâmetros: um para o objeto de erro no caso de a operação falhar e outro para um valor de retorno, neste caso, o objeto criado. Dentro do retorno de chamada, você pode manipular a exceção ou lançar um erro. Se um retorno de chamada não for fornecido e houver um erro, o tempo de execução do Azure Cosmos DB lançará 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 true e a descrição está ausente, o procedimento armazenado lança uma exceção. Caso contrário, o restante do procedimento armazenado continuará a ser executado.

O exemplo a seguir de um procedimento armazenado usa uma matriz de novos itens do Azure Cosmos DB como entrada, a insere no contêiner do Azure Cosmos DB e retorna a contagem dos itens inseridos. Neste exemplo, estamos usando 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);
        }
    }
}

Matrizes como parâmetros de entrada para procedimentos armazenados

Quando você define um procedimento armazenado no portal do Azure, os parâmetros de entrada são sempre enviados como uma cadeia de caracteres para o procedimento armazenado. Mesmo se você passar uma matriz de cadeias de caracteres como uma entrada, a matriz será convertida em uma cadeia de caracteres e enviada para o procedimento armazenado. Para contornar isso, você pode definir uma função dentro do procedimento armazenado para analisar a cadeia de caracteres como uma matriz. O código a seguir mostra como analisar um parâmetro de entrada de cadeia de caracteres 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 dentro de procedimentos armazenados

Você pode implementar transações em itens dentro de um contêiner usando um procedimento armazenado. O exemplo a seguir usa transações dentro de um aplicativo de jogos de futebol fantástico para negociar jogadores entre duas equipes em uma única operação. O procedimento armazenado tenta ler os dois itens do Azure Cosmos DB, cada um correspondendo às IDs de jogador passadas como um argumento. Se ambos os jogadores forem encontrados, o procedimento armazenado atualiza os itens trocando suas equipes. Se algum erro for encontrado ao longo do caminho, o procedimento armazenado lançará uma exceção JavaScript que anula 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

A seguir está um exemplo de um procedimento armazenado que importa itens em massa para um contêiner do Azure Cosmos DB. O procedimento armazenado lida com a execução limitada verificando o valor de retorno booleano de createDocumente usa a contagem de itens inseridos em cada chamada do procedimento armazenado para controlar e retomar o progresso entre 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);
        }
    }
}

Sincronizar/aguardar com procedimentos armazenados

O exemplo de procedimento armazenado a seguir usa async/await com Promises usando uma função auxiliar. O procedimento armazenado consulta um item e o substitui.

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 dá suporte a pré-gatilhos e pós-disparadores. Os pré-gatilhos são executados antes de modificar um item de banco de dados e os pós-gatilhos são executados depois de modificar um item de banco de dados. Os gatilhos não são executados automaticamente. Eles devem ser especificados para cada operação de banco de dados onde você deseja que eles sejam executados. Depois de definir um gatilho, você deve registrar e chamar um pré-gatilho usando os SDKs do Azure Cosmos DB.

Pré-acionadores

O exemplo a seguir mostra como um pré-gatilho é usado para validar as propriedades de um item do Azure Cosmos DB que está sendo criado. Este exemplo usa 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 recém-adicionado se ele 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é-gatilhos não podem ter parâmetros de entrada. O objeto request no gatilho é usado para manipular a mensagem de solicitação associada à operação. No exemplo anterior, o pré-gatilho é executado ao criar um item do Azure Cosmos DB e o corpo da mensagem de solicitação contém o item a ser criado no formato JSON.

Quando os gatilhos são registrados, você pode especificar as operações com as quais ele pode ser executado. Esse gatilho deve ser criado com um TriggerOperation valor de , o que significa que o uso do gatilho em uma operação de TriggerOperation.Createsubstituição não é permitido.

Para obter exemplos de como registrar e chamar um pré-gatilho, consulte pré-gatilhos e pós-gatilhos.

Pós-acionadores

O exemplo a seguir mostra um pós-gatilho. Esse gatilho consulta o item de metadados e o atualiza 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 que é importante observar é a execução transacional de gatilhos no Azure Cosmos DB. O pós-gatilho é executado como parte da mesma transação para o próprio item subjacente. Uma exceção durante a execução pós-gatilho falha toda a transação. Qualquer coisa confirmada é revertida e uma exceção é retornada.

Para obter exemplos de como registrar e chamar um pré-gatilho, consulte pré-gatilhos e pós-gatilhos.

Como escrever funções definidas pelo usuário

O exemplo a seguir cria um UDF para calcular o imposto de renda para várias faixas de renda. Essa função definida pelo usuário seria então usada dentro de uma consulta. Para os fins deste exemplo, suponha que haja um contêiner chamado Entradas com propriedades da seguinte maneira:

{
   "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 registrar e usar um UDF, consulte Como trabalhar com funções definidas pelo usuário no Azure Cosmos DB.

Registo

Ao usar procedimentos armazenados, gatilhos ou UDFs, você pode registrar as etapas habilitando o log de script. Uma cadeia de caracteres para depuração é gerada quando EnableScriptLogging é definida como true, conforme mostrado nos exemplos a seguir:

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

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