Azure Cosmos DB 提供語言整合的 JavaScript 交易執行,讓你能撰寫儲存程序、觸發器及使用者自訂函式(UDF)。 當你在 Azure Cosmos DB 中使用 NoSQL 的 API 時,你可以使用 JavaScript 來定義儲存程序、觸發器和 UDF。 你可以用 JavaScript 寫邏輯,然後在資料庫引擎裡執行。 你可以透過 Azure 入口網站、 Azure Cosmos DB 中的 JavaScript 查詢 API,以及 NoSQL SDK 的 Azure Cosmos DB,來建立並執行觸發器、儲存程序和 UDF。
要呼叫儲存程序、觸發器或 UDF,你需要先註冊它。 欲了解更多資訊,請參閱 「如何註冊與使用儲存程序、觸發器及使用者自訂函式」。
備註
對於分割容器,執行儲存程序時,必須在請求選項中提供分割區鍵值。 儲存程序總是以分割區鍵為作用域。 擁有不同分割鍵值的項目,對儲存程序來說是看不到的。 這同樣適用於觸發器。
備註
伺服器端的 JavaScript 功能,包括儲存程序、觸發器和 UDF,不支援匯入模組。
小提示
Azure Cosmos DB 支援部署帶有儲存程序、觸發器和 UDF 的容器。 欲了解更多資訊,請參閱 「建立具備伺服器端功能的 Azure Cosmos 資料庫容器」。
如何撰寫儲存程序
儲存程序是使用 JavaScript 撰寫,並且可以在 Azure Cosmos 資料庫容器中建立、更新、讀取、查詢及刪除項目。 儲存程序會依每個集合註冊,並可操作該集合中任何文件或附件。
備註
Azure Cosmos DB 對儲存程序有不同的收費政策。 由於儲存程序可以執行程式碼並消耗任意數量的請求單元(RU),每次執行都需要一筆前期費用。 這確保了儲存程序腳本不會影響後端服務。 前期收費金額等於腳本在先前呼叫中平均消耗的費用。 每次操作的平均 RU 會被保留,然後才執行。 如果 RU 的呼叫差異很大,你的預算使用可能會受到影響。 作為替代方案,你應該使用批次或批量請求,而非儲存程序,以避免 RU 費用的變動。
這裡有一個簡單的儲存程序,可以回傳一個「Hello World」回應。
var helloWorldStoredProc = {
id: "helloWorld",
serverScript: function () {
var context = getContext();
var response = context.getResponse();
response.setBody("Hello, World");
}
}
上下文物件提供 Azure Cosmos DB 中所有可執行的操作存取,以及請求與回應物件的存取權。 在這種情況下,你會使用 response 物件來設定回應內容要回傳給用戶端。
寫入後,儲存程序必須註冊於集合中。 欲了解更多,請參閱 如何在 Azure Cosmos DB 中使用儲存過程。
使用儲存程序建立項目
當你使用儲存程序建立項目時,該項目會插入 Azure Cosmos 資料庫容器,並回傳新建立項目的 ID。 建立項目是一種非同步操作,依賴 JavaScript 回調函式。 回調函式有兩個參數:一個是錯誤物件,以防操作失敗,另一個是回傳值,此處是已建立的物件。 在回呼函式中,你可以選擇處理例外或拋出例外。 如果沒有提供回調且發生錯誤,Azure Cosmos DB 執行時會拋出錯誤。
儲存程序還包含一個參數,可將描述設定為布林值。 當參數設為 true 且缺少描述時,儲存程序會拋出例外。 否則,儲存程序的其餘部分會繼續執行。
以下儲存程序範例是輸入一組新的 Azure Cosmos 資料庫項目陣列,插入 Azure Cosmos DB 容器,並回傳插入項目的數量。 在這個例子中,我們使用的是 NoSQL 快速入門 .NET API 中的 ToDoList 範例。
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 資料庫項目,分別對應作為參數傳入的玩家 ID。 如果兩位玩家都被找到,儲存程序會透過交換隊伍來更新物品。 如果途中遇到錯誤,儲存程序會拋出一個 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 資料庫容器。 儲存程序透過檢查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 和 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 支援預先觸發與後觸發。 預觸發器在修改資料庫項目前執行,後觸發器則在修改資料庫項目後執行。 觸發器不會自動執行。 它們必須針對每個你想執行的資料庫操作指定。 定義觸發器後,你應該註冊並使用 Azure Cosmos DB SDK 呼叫 預觸發 器。
預觸發器
以下範例展示了如何使用預觸發器來驗證正在建立的 Azure Cosmos 資料庫項目的屬性。 這個範例使用 Quickstart .NET API for NoSQL 的 ToDoList 範例,若新新增的項目不包含時間戳記屬性,則可為該項目新增時間戳記屬性。
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 資料庫項目時執行,請求訊息主體會以 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 中觸發器的交易性執行。 後置觸發器作為底層項目本身相同交易的一部分執行。 觸發後執行中的例外會使整個交易失敗。 任何已提交的事項都會被回滾並回傳例外。
如何撰寫使用者自訂函式
以下範例建立一個UDF,用於計算不同收入階層的所得稅。 這個 UDF 將被用於查詢中。 在此範例中,假設有一個稱為 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;
}
關於如何註冊並使用 UDF 的範例,請參見 「如何操作使用者定義函式」。
森林伐木業
使用儲存程序、觸發器或 UDF 時,你可以啟用腳本日誌來記錄步驟。 當 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]);