如何在 Azure Cosmos DB 中撰寫預存程序、觸發程序和使用者定義函式

適用於:NoSQL

Azure Cosmos DB 所提供的語言整合式、交易式 JavaScript 執行,可讓您撰寫預存程序觸發程序使用者定義函式 (UDF)。 當您在 Azure Cosmos DB 中使用 API for NoSQL 時,可以使用 JavaScript 來定義預存程序、觸發程序和 UDF。 您可以使用 JavaScript 撰寫邏輯,並在資料庫引擎內加以執行。 您可以使用 Azure 入口網站Azure Cosmos DB 中的 JavaScript 查詢 APIAzure Cosmos DB for NoSQL SDK,來建立與執行觸發程序、預存程序和 UDF。

若要呼叫預存程序、觸發程序和 UDF,您需要加以註冊。 如需詳細資訊,請參閱如何在 Azure Cosmos DB 中使用預存程序、觸發程序、使用者定義函數

注意

對分割容器執行預存程序時,必須在要求選項中提供分割區索引鍵值。 預存程序的範圍一律為分割區索引鍵。 預存程序看不到具有不同分割區索引鍵值的項目。 這也適用於觸發程序。

注意

伺服器端 JavaScript 功能 (包括預存程序、觸發程序和 UDF) 不支援匯入模組。

提示

Azure Cosmos DB 支援使用預存程序、觸發程序和 UDF 來部署容器。 如需詳細資訊,請參閱使用伺服器端功能建立 Azure Cosmos DB 容器

如何撰寫預存程序

預存程序是使用 JavaScript 所撰寫,而且可以建立、更新、讀取、查詢和刪除 Azure Cosmos DB 容器內的項目。 預存程序會按照集合進行註冊,而且可執行於該集合中現有的文件或附件。

注意

針對預存程序,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 中執行的所有作業,以及存取要求和回應物件。 在此案例中,您會使用回應物件來設定要傳回給用戶端的回應本文。

撰寫完成後,預存程序必須對集合註冊。 若要深入了解,請參閱如何在 Azure Cosmos DB 中使用預存程序

使用預存程序來建立項目

當您使用預存程序建立項目時,該項目會插入至 Azure Cosmos DB 容器,並傳回新建立項目的識別碼。 建立項目是非同步作業,並依存於 JavaScript 回呼函式。 此回呼函數有兩個參數:一個用於作業失敗時的錯誤物件,一個用於傳回值 (在此案例中為已建立的物件)。 在回呼內,您可以處理例外狀況或擲回錯誤。 如果未提供回呼,而且發生錯誤,則 Azure Cosmos DB 執行階段會擲回錯誤。

預存程序也包括用來將描述設定為布林值的參數。 當此參數設定為 true 時,若沒有描述,則預存程序會擲回例外狀況。 否則,預存程序的其餘部分會繼續執行。

下列預存程序範例採用新的 Azure Cosmos DB 項目陣列作為輸入,並將其插入 Azure Cosmos DB 容器中,然後傳回插入的項目計數。 在此範例中,我們要使用快速入門 .NET API for NoSQL 中的 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 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

下列預存程序範例使用 helper 函數來搭配使用 async/awaitPromises。 預存程序會查詢並取代項目。

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 DB 項目驗證屬性。 此範例使用快速入門 .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 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 中觸發程序的交易式執行。 後續觸發程序會在基礎項目本身的相同交易中執行。 後續觸發程序執行期間發生的例外狀況,會導致整個交易失敗。 系統會復原已認可的所有項目,並傳回例外狀況。

如需如何註冊和呼叫預先觸發程序的範例,請參閱預先觸發程序後續觸發程序

如何撰寫使用者定義函數

下列範例會建立 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 的範例,請參閱如何在 Azure Cosmos DB 中使用使用者定義函數

記錄

使用預存程序、觸發程序或 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]);

下一步

了解更多概念,以及如何在 Azure Cosmos DB 中撰寫或使用預存程序、觸發程序和 UDF: