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