Stored Procedures and Triggers
Complete the full lesson to earn 25 points
Work through each section, then tap “Mark as Complete” on the last one.
✦ Skip the page breaks and see fewer ads — read each lesson on a single page with Pro
Mastering Stored Procedures and Triggers in Azure Cosmos DB
Introduction: The Power of Server-Side Programming
Azure Cosmos DB is a globally distributed, multi-model database service designed to provide low-latency access to data at any scale. While most developers interact with Cosmos DB using client-side SDKs—sending queries and commands from their application code—there is a powerful layer of functionality that exists directly within the database engine itself: server-side programming. By utilizing Stored Procedures, Triggers, and User-Defined Functions (UDFs), developers can execute logic in close proximity to the data, reducing network round-trips and ensuring transactional integrity.
Why does this matter? In a distributed system, network latency is often the primary bottleneck. If you need to perform a series of operations—such as reading a document, modifying it based on complex logic, and writing it back—doing so from a client application requires multiple back-and-forth trips between your server and the database. If any part of that sequence fails, you are responsible for managing the state and rolling back changes. Server-side programming allows you to group these operations into a single atomic transaction, ensuring that either all operations succeed or none of them do. This approach is essential for maintaining data consistency in complex business scenarios where partial updates could lead to data corruption or logic errors.
Understanding the Server-Side Environment
Before diving into code, it is important to understand the execution environment. Cosmos DB uses a JavaScript-based engine to execute server-side logic. This means that if you are familiar with JavaScript, you are already well-equipped to write stored procedures and triggers. The environment provides a specific object called getContext() which serves as the bridge between your code and the underlying database container.
Through the getContext() object, you gain access to the getCollection() method, which allows you to perform CRUD (Create, Read, Update, Delete) operations on documents within the current container. Because these scripts run within the database transaction, they are subject to the same performance and resource constraints as any other database operation. Specifically, they are bound by the Request Unit (RU) limits of the container. If a stored procedure runs for too long or consumes too many resources, the database engine will terminate it to protect the overall health of the system.
Callout: Stored Procedures vs. Client-Side Transactions While client-side SDKs now support multi-document transactions using the
transactionalBatchAPI, stored procedures remain a vital tool for logic that requires complex conditional branching or decision-making based on the data retrieved during the operation. Stored procedures allow you to implement "if-this-then-that" logic on the server, whereas batch operations are generally limited to a predefined set of CRUD commands.
Stored Procedures: Atomic Operations
A stored procedure is a piece of JavaScript code that you register with a container. Once registered, you can invoke it from your client application by name, passing in any required parameters. The most significant advantage of a stored procedure is that it runs as a single, atomic transaction. If you modify five documents within a stored procedure and the fifth modification fails, the database automatically rolls back the previous four, returning the container to its original state.
Anatomy of a Stored Procedure
Every stored procedure follows a standard structure. You define a function, and inside that function, you interact with the getContext() object. The following example demonstrates a simple stored procedure that creates a new document only if a document with the same ID does not already exist.
function createDocumentIfNotExist(document) {
var context = getContext();
var collection = context.getCollection();
var accepted = collection.createDocument(collection.getSelfLink(), document,
function (err, docCreated) {
if (err) throw new Error('Error creating document: ' + err.message);
context.getResponse().setBody(docCreated);
});
if (!accepted) return;
}
In this code, collection.createDocument is an asynchronous operation. The accepted variable indicates whether the request was accepted by the database engine. If the request is not accepted, it means the script is hitting resource limits or other constraints, and you should exit gracefully.
Handling Transactions and Error States
When writing stored procedures, you must manage errors explicitly. Since the environment is JavaScript, you should use standard try-catch blocks where appropriate, but remember that database errors are usually passed via callback functions. If an error occurs, you should use throw to signal the failure to the database engine, which will then trigger the automatic rollback of the transaction.
Tip: Monitoring Resource Usage Always keep an eye on the Request Unit (RU) consumption of your stored procedures. Because they run in an atomic transaction, they hold locks on documents for the duration of the execution. If a stored procedure is poorly optimized or processes too many documents, it can cause contention and slow down other operations in the container.
Triggers: Pre- and Post-Operations
Triggers are specialized scripts that execute automatically before or after a database operation (Create, Replace, or Delete). They are categorized into two types:
- Pre-triggers: Execute before the operation. These are typically used for validation or to inject default values into a document.
- Post-triggers: Execute after the operation. These are useful for cascading updates, such as updating a summary document after a new child document is created.
Implementing a Pre-trigger
A pre-trigger is helpful when you need to ensure that every document inserted into a container follows a specific format or contains certain fields. In this example, we verify that a document has a createdAt timestamp.
function validateTimestamp() {
var context = getContext();
var request = context.getRequest();
var document = request.getBody();
if (!document.hasOwnProperty('createdAt')) {
document.createdAt = new Date().toISOString();
request.setBody(document);
}
}
Implementing a Post-trigger
Post-triggers are useful for maintaining data integrity across related documents. For instance, if you have a SalesOrder container and a CategoryStats container, you might use a post-trigger on the SalesOrder container to increment the total sales count for a category whenever a new order is added.
function updateCategoryStats() {
var context = getContext();
var collection = context.getCollection();
var doc = context.getResponse().getBody();
var query = 'SELECT * FROM root r WHERE r.categoryId = "' + doc.categoryId + '"';
collection.queryDocuments(collection.getSelfLink(), query, function (err, docs) {
if (err) throw new Error(err.message);
// Logic to update stats would go here
});
}
Comparison Table: Stored Procedures vs. Triggers
| Feature | Stored Procedure | Pre-Trigger | Post-Trigger |
|---|---|---|---|
| Execution Trigger | Explicitly called by client | Automatic (before CRUD) | Automatic (after CRUD) |
| Atomic Transaction | Yes | Yes | Yes |
| Purpose | Complex business logic | Validation/Default values | Cascading updates/Logging |
| Parameters | Accepts input parameters | No input parameters | No input parameters |
Step-by-Step: Deploying and Invoking
Deploying server-side logic in Cosmos DB involves a few distinct steps. You cannot simply save a file; you must register the script with the database container.
- Write the Script: Develop your JavaScript code locally. Test it thoroughly, as debugging on the server is more challenging than in a local environment.
- Register the Script: Use the Azure Portal, the Azure CLI, or the SDK to upload the script to your container. You must assign it a unique ID.
- Invoke the Script:
- For Stored Procedures, use the
executeStoredProceduremethod in your SDK. - For Triggers, specify the trigger name in the
RequestOptionswhen performing a CRUD operation.
- For Stored Procedures, use the
Example: Invoking a Stored Procedure via C#
// Define the stored procedure ID
string sprocId = "createDocumentIfNotExist";
// Create the document
var document = new { id = "123", name = "Test Item" };
// Execute
await container.Scripts.ExecuteStoredProcedureAsync<dynamic>(
sprocId,
new PartitionKey("123"),
new[] { document });
Warning: The "Partition Key" Requirement Stored procedures and triggers must be scoped to a single partition key. You cannot perform a transaction that spans multiple partitions. If your container is partitioned by
categoryId, your stored procedure must be executed within the scope of a specificcategoryId. Attempting to access data across different partition keys will result in an error.
Best Practices for Server-Side Development
1. Keep Scripts Lightweight
The most common mistake developers make is writing "heavy" scripts that perform too many operations or iterate over thousands of documents. Remember that server-side scripts are subject to the same RU limits as standard queries. If your script times out or exceeds RU limits, the entire transaction will fail. Always aim for efficiency and batching where possible.
2. Defensive Coding
Because the JavaScript engine in Cosmos DB does not have the full breadth of libraries available in Node.js, stick to basic, standard JavaScript. Avoid using external libraries or complex dependencies. Validate all inputs at the start of your script and handle potential errors gracefully.
3. Use Partition Keys Correctly
As mentioned, server-side logic is bound to a partition key. Design your schema and your stored procedures with this in mind. If you find yourself needing to update data across multiple partitions, a stored procedure is not the correct tool. In such cases, you should manage the transaction at the application layer or use an asynchronous process like an Azure Function triggered by the Change Feed.
4. Logging and Debugging
Debugging server-side scripts can be frustrating because you cannot use standard console logging. Instead, you can use getContext().getResponse().setBody() to return debug information to the client, or store logs in a separate "Audit" container. This is a common pattern for production environments where you need to track what happened inside a complex procedure.
Common Pitfalls to Avoid
Avoiding Infinite Loops
When using triggers, it is easy to accidentally create an infinite loop. For example, if a post-trigger performs an update on the same document that triggered it, and the update triggers the post-trigger again, you will quickly hit your resource limits. Always ensure that your triggers have clear exit conditions and do not cause recursive updates.
Over-Reliance on Server-Side Logic
Do not use stored procedures for logic that can easily be handled by the client application. Client-side code is easier to unit test, version control, and deploy. Use server-side logic only when you specifically need the transactional integrity provided by the database engine.
Ignoring Throughput
Every execution of a stored procedure consumes RUs. If you have a high-frequency application, these costs can add up. Monitor your RU usage metrics in the Azure Portal to ensure that your server-side scripts are not causing performance degradation for other parts of your application.
Callout: The JavaScript Engine Limitations The JavaScript environment in Cosmos DB is a sandboxed engine. It does not have access to the file system, network, or environment variables. It is strictly limited to the database container context. This is a security feature, but it means you cannot perform external API calls or write to local files from within a stored procedure.
Advanced Scenario: Implementing a "Bulk Delete"
One of the most common tasks that benefits from a stored procedure is the bulk deletion of documents. Deleting documents one by one from the client is slow and error-prone. A stored procedure can perform this task efficiently.
function bulkDelete(query) {
var context = getContext();
var collection = context.getCollection();
var response = context.getResponse();
collection.queryDocuments(collection.getSelfLink(), query, {},
function (err, docs) {
if (err) throw new Error(err.message);
if (docs.length === 0) {
response.setBody(0);
return;
}
var count = 0;
function tryDelete(index) {
if (index >= docs.length) {
response.setBody(count);
return;
}
collection.deleteDocument(docs[index]._self, function (err) {
if (err) throw new Error(err.message);
count++;
tryDelete(index + 1);
});
}
tryDelete(0);
});
}
In this example, we query for a set of documents and then recursively delete them. Note that we check for the existence of documents first, and we use a recursive function to handle the deletion one by one. This ensures that we remain within the transaction boundaries and handle the asynchronous nature of the deleteDocument method correctly.
Frequently Asked Questions (FAQ)
Q: Can I use NPM packages in my stored procedures? A: No. The Cosmos DB JavaScript engine does not support external modules or NPM packages. You must write your logic using standard, vanilla JavaScript.
Q: How do I debug a stored procedure?
A: Since you cannot use a debugger, the best approach is to return descriptive error messages or status objects through the response body. You can also write status information to a separate logging container within the same partition.
Q: Are stored procedures thread-safe? A: Yes, because they run as an atomic transaction within a single partition, the database engine ensures that no other operations modify the documents being accessed by your stored procedure until it completes or rolls back.
Q: Can I call one stored procedure from another? A: No, stored procedures cannot be chained or called from one another. Each stored procedure is an independent unit of execution.
Q: How many RUs should I allocate for a stored procedure? A: It depends on the complexity of the script. You should perform load testing to determine the average RU consumption of your scripts and ensure your container has sufficient throughput to handle the spikes caused by these operations.
Key Takeaways
- Atomic Transactions: Stored procedures and triggers provide a way to perform multiple operations as a single, atomic unit of work, ensuring data consistency.
- Partition Sensitivity: All server-side logic is scoped to a single partition key. You cannot perform cross-partition transactions using stored procedures or triggers.
- Efficiency Matters: Because server-side scripts consume Request Units (RUs), they must be optimized. Poorly written scripts can lead to performance bottlenecks and high costs.
- JavaScript Environment: You are working in a restricted, sandboxed JavaScript environment. Avoid complex dependencies and focus on simple, performant code.
- Validation and Defaults: Pre-triggers are excellent for enforcing data quality and ensuring that mandatory fields are present before a document is persisted.
- Cascading Updates: Post-triggers are ideal for maintaining relationships between documents, such as updating summary counts or creating related records.
- Testing: Always test your scripts locally and in a development environment before deploying them to production, as debugging in the cloud is significantly more challenging.
By mastering these server-side tools, you gain a significant advantage in managing complex data workflows within Cosmos DB. While the client-side SDKs are powerful, the ability to execute logic directly inside the database engine is what allows you to build truly robust and consistent applications at scale. Use these tools judiciously—focusing on atomicity and efficiency—and you will find that your database interactions become much cleaner, faster, and more reliable.
Enjoying the courses?
Everything stays free. Pro shows fewer ads, doubles your daily points limit so you progress twice as fast, and lets you read each lesson on one page.
- ✓ Fewer advertisements
- ✓ 2× daily points limit
- ✓ Distraction-free lessons