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

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

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

Чтобы вызвать хранимую процедуру, триггер или определяемую пользователем функцию, необходимо зарегистрировать ее. Дополнительные сведения см. в статье Работа с хранимыми процедурами, триггерами и определяемыми пользователем функциями в 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 для 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, которое неявно отменяет транзакцию.

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

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

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

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

В следующем примере хранимой процедуры используется 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.

При регистрации триггеров можно указать операции, с помощью которых их можно запустить. Этот триггер должен быть создан со значением TriggerOperationTriggerOperation.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. Триггер последующего выполнения запускается как часть транзакции базового элемента. Исключение во время выполнения после триггера завершается сбоем всей транзакции. Выполняется откат всего зафиксированного и возвращается исключение.

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

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

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

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

Примеры регистрации и использования определяемых пользователем функций см. в статье Работа с определяемыми пользователем функциями в 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: