Как писать хранимые процедуры, триггеры и определяемые пользователем функции в Azure Cosmos DB

ОБЛАСТЬ ПРИМЕНЕНИЯ: NoSQL

Azure Cosmos DB обеспечивает встроенное в язык транзакционное выполнение JavaScript, которое позволяет писать хранимые процедуры, триггеры и определяемые пользователем функции. При использовании API для NoSQL в Azure Cosmos DB можно определить хранимые процедуры, триггеры и определяемые пользователем функции с помощью JavaScript. Вы можете написать свою логику на JavaScript и выполнить ее в ядре СУБД. Вы можете создавать и выполнять триггеры, хранимые процедуры и определяемые пользователем функции с помощью портал Azure, API запросов JavaScript в Azure Cosmos DB и пакетов SDK Azure Cosmos DB для NoSQL.

Чтобы вызвать хранимую процедуру, триггер или UDF, необходимо зарегистрировать ее. Дополнительные сведения см. в статье о работе с хранимыми процедурами, триггерами и пользовательскими функциями в Azure Cosmos DB.

Примечание.

Для секционированных контейнеров при выполнении хранимой процедуры в параметрах запроса необходимо указать значение ключа секции. Хранимые процедуры всегда ограничиваются ключом секции. Элементы, имеющие другое значение ключа секции, не видны хранимой процедуре. Это также относится к триггерам.

Примечание.

Функции JavaScript на стороне сервера, включая хранимые процедуры, триггеры и определяемые пользователем функции, не поддерживают импорт модулей.

Совет

Azure Cosmos DB поддерживает развертывание контейнеров с хранимыми процедурами, триггерами и пользовательскими файлами. Дополнительные сведения см. в статье "Создание контейнера Azure Cosmos DB с помощью функций на стороне сервера".

Запись хранимых процедур

Хранимые процедуры записываются с помощью JavaScript, и они могут создавать, обновлять, читать, запрашивать и удалять элементы в контейнере Azure Cosmos DB. Хранимые процедуры регистрируются в коллекциях и могут работать с любым документом и вложением, существующим в этой коллекции.

Примечание.

Azure Cosmos DB имеет другую политику зарядки для хранимых процедур. Так как хранимые процедуры могут выполнять код и использовать любое количество единиц запросов (ЕЗ), каждое выполнение требует предоплаты. Это гарантирует, что скрипты хранимой процедуры не влияют на внутренние службы. Сумма, взимаемая заранее, равна средней сумме, используемой скриптом в предыдущих вызовах. Среднее количество единиц запросов на операцию зарезервировано перед выполнением. Если вызовы имеют много дисперсии в единицах ЕЗ, то может повлиять использование бюджета. В качестве альтернативы следует использовать пакетные или массовые запросы вместо хранимых процедур, чтобы избежать дисперсии по затратам на ЕЗ.

Давайте начнем с простой хранимой процедуры, которая возвращает ответ "Hello World".

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

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

Объект контекста предоставляет доступ ко всем операциям, которые можно выполнить в Azure Cosmos DB, а также доступ к объектам запросов и ответов. В этом примере вы применяете объект ответа, чтобы задать текст ответа для отправки клиенту.

После написания хранимую процедуру нужно зарегистрировать с помощью коллекции. Дополнительные сведения см. в статье Об использовании хранимых процедур в Azure Cosmos DB.

Создание элементов с помощью хранимых процедур

При создании элемента с помощью хранимой процедуры элемент вставляется в контейнер Azure Cosmos DB и возвращается идентификатор только что созданного элемента. Создание элемента является асинхронной операцией и зависит от функций обратного вызова JavaScript. Функция обратного вызова имеет два параметра: один для объекта ошибки в случае сбоя операции, а другой — для возвращаемого значения, в данном случае созданный объект. Внутри функции обратного вызова можно либо обработать исключение, либо вызвать ошибку. Если обратный вызов не указан и возникает ошибка, среда выполнения Azure Cosmos DB выдает ошибку.

Хранимая процедура также включает параметр для задания описания в качестве логического значения. Если параметр имеет значение true, а описание отсутствует, хранимая процедура создает исключение. В противном случае остальная часть хранимой процедуры продолжит выполняться.

Следующий пример хранимой процедуры принимает массив новых элементов Azure Cosmos DB в качестве входных данных, вставляет его в контейнер Azure Cosmos DB и возвращает количество вставленных элементов. В этом примере мы используем пример ToDoList из API .NET для .NET для 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);
        }
    }
}

Массивы в качестве входных параметров для хранимых процедур

При определении хранимой процедуры в портал Azure входные параметры всегда отправляются в виде строки в хранимую процедуру. Даже если вы передаете массив строк в качестве входных данных, массив преобразуется в строку и отправляется в хранимую процедуру. Чтобы обойти эту проблему, можно определить функцию в хранимой процедуре для анализа строки как массива. В следующем коде показано, как проанализировать строковый входной параметр как массив.

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

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

Транзакции в рамках хранимых процедур

Вы можете реализовать транзакции для элементов в контейнере с помощью хранимой процедуры. В следующем примере транзакции в игровом приложении для фэнтези-футбола используются для обмена игроками между двумя командами за одну операцию. Хранимая процедура пытается прочитать два элемента Azure Cosmos DB, каждый из которых соответствует идентификаторам проигрывателя, переданным в качестве аргумента. Если оба игрока найдены, то хранимая процедура обновляет элементы, меняя их команды. При обнаружении ошибок вызывается исключение JavaScript, которое неявно отменяет транзакцию.

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

Ограниченное выполнение в рамках хранимых процедур

Ниже приведен пример хранимой процедуры, которая выполняет массовый импорт элементов в контейнер Azure Cosmos DB. Хранимая процедура обрабатывает ограниченное выполнение, проверяя логическое возвращаемое значение из createDocument, а затем использует подсчет элементов, помещенных в каждом вызове хранимой процедуры, чтобы отслеживать и возобновлять выполнение пакетного задания.

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 с хранимыми процедурами

В следующем примере хранимой процедуры используется async/await с promises с помощью вспомогательной функции. Эта хранимая процедура запрашивает элемент и заменяет его.

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

Запись триггеров

Azure Cosmos DB поддерживает триггеры предварительного и последующего выполнения. Перед изменением элемента базы данных выполняются предварительные триггеры, а после изменения элемента базы данных выполняются после изменения элемента базы данных. Триггеры не выполняются автоматически. Они должны быть указаны для каждой операции базы данных, в которой они должны выполняться. После определения триггера следует зарегистрировать и вызвать предварительный триггер с помощью пакетов SDK для Azure Cosmos DB.

Триггеры предварительного выполнения

В следующем примере показано, как используется предварительный триггер для проверки свойств создаваемого элемента Azure Cosmos DB. В этом примере используется пример ToDoList из API .NET быстрого запуска для NoSQL , чтобы добавить свойство метки времени в только что добавленный элемент, если он не содержит его.

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

Предварительные триггеры не могут иметь входные параметры. Объект запроса в триггере используется для управления сообщением запроса, связанным с операцией. В предыдущем примере триггер предварительного выполнения запускается при создании элемента Azure Cosmos DB, а текст сообщения запроса содержит элемент, который будет создан в формате JSON.

При регистрации триггеров можно указать операции, с помощью которых их можно запустить. Этот триггер должен быть создан со TriggerOperation значением TriggerOperation.Create, что означает, что использование триггера в операции замены запрещено.

Примеры регистрации и вызова предварительного триггера см. в разделе о предварительных триггерах и после триггеров.

Триггеры последующего выполнения

В примере ниже показан триггер последующего выполнения. Этот триггер запрашивает элемент метаданных и обновляет его дополнительными сведениями о вновь созданном элементе.

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

Важно отметить, что транзакционный запуск триггеров в Azure Cosmos DB. Триггер последующего выполнения запускается как часть транзакции базового элемента. Исключение во время выполнения после триггера завершается сбоем всей транзакции. Все зафиксированные события откатываются и возвращается исключение.

Примеры регистрации и вызова предварительного триггера см. в разделе о предварительных триггерах и после триггеров.

Запись определяемых пользователем функций

В следующем примере создается определяемая пользователем функция для расчета подоходного налога для различных доходов. Эта функция затем будет использоваться в запросе. В этом примере предполагается, что есть контейнер с именем "Доходы" со свойствами следующим образом:

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

Следующее определение функции вычисляет налог на доходы для различных квадратных скобок дохода:

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

Примеры регистрации и использования UDF см. в статье "Как работать с определяемыми пользователем функциями в Azure Cosmos DB".

Ведение журнала

При использовании хранимых процедур, триггеров или определяемых пользователем функций можно выполнить журнал действий, включив ведение журнала скриптов. Строка для отладки создается, если EnableScriptLogging задано значение true, как показано в следующих примерах:

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

Следующие шаги

Узнайте больше о понятиях и способах написания или использования хранимых процедур, триггеров и определяемых пользователем функций в Azure Cosmos DB: