Transaction Operations
Transaction operations ensure that multiple database operations either all succeed or all fail, guaranteeing data consistency. Cloud Development supports database transactions and ensures the ACID properties of transactions.
Currently, database transactions are only supported on the server-side, and only the node-sdk
supports transactions.
Use Cases
In most scenarios, the single-document approach fully meets the requirements. However, in certain scenarios, the advantages of using database transactions become more apparent:
- Migrating from traditional relational databases to CloudBase: Smooth migration of data models with low refactoring costs for business code
- Business Processes Involving Multiple Documents/Collections: Ensures that a series of read and write operations either completely succeed or fail entirely, preventing intermediate states
Supported Methods
Transaction Process Methods
API | Description |
---|---|
startTransaction | Start transaction |
commit | Commit transaction |
rollback | Rollback transaction |
runTransaction | Auto-commit transaction |
Data Operations in Transactions
API | Description |
---|---|
get | Query documents |
add | Insert documents |
delete | Delete documents |
update | Update documents |
set | Update documents |
Basic Transactions
Parameter Description
Parameter | Type | Required | Description |
---|---|---|---|
updateFunction | function | ✅ Yes | Transaction update function that receives a transaction parameter |
Code Sample
const cloudbase = require('@cloudbase/node-sdk')
// Initialize the database.
const app = cloudbase.init({
env: 'your-env-id'
})
const db = app.database()
// Basic transaction operations
const result = await db.runTransaction(async transaction => {
// Performing multiple operations within a transaction
const todoDoc = await transaction.collection('todos').doc('todo-id').get()
if (!todoDoc.data) {
throw new Error('Todo does not exist')
}
// Update to-do item status
await transaction.collection('todos').doc('todo-id').update({
data: {
completed: true,
completedAt: new Date()
}
})
// Update user statistics
await transaction.collection('users').doc('user-id').update({
data: {
completedTodos: db.command.inc(1)
}
})
return { success: true }
})
console.log('Transaction execution result:', result)
Response
{
result: { success: true }, // return value of updateFunction
errMsg: "runTransaction:ok"
}
Complex Transaction Example
Clear Shopping Cart Scenario
Using the "Clear Shopping Cart" requirement as an example, this demonstrates the use of database transactions in complex business scenarios:
// Node.js environment
const cloudbase = require('@cloudbase/node-sdk')
// Initialize the database.
const app = cloudbase.init({
env: 'your-env-id'
})
const db = app.database()
exports.main = async (event, context) => {
const userId = 'user1'
const transaction = await db.startTransaction()
try {
const usersCollection = transaction.collection('users')
const goodsCollection = transaction.collection('goods')
// 1. Get user information
const user = await usersCollection.doc(userId).get()
const { cart, balance } = user.data
// 2. Get shopping cart data and corresponding product information
const goods = []
for (const { id } of cart) {
const good = await goodsCollection.doc(id).get()
goods.push(good.data)
}
let totalPrice = 0
for (let i = 0; i < cart.length; ++i) {
// 3. Calculate the total price of the products in the shopping cart
totalPrice += cart[i].num * goods[i].price
// 4. Update product inventory
await goodsCollection.doc(goods[i]._id).set({
inventory: goods[i].inventory - cart[i].num
})
}
// 5. Update user account balance and clear the shopping cart
await usersCollection.doc(userId).set({
balance: balance - totalPrice,
cart: []
})
await transaction.commit()
return { success: true, totalPrice }
} catch (error) {
await transaction.rollback()
throw error
}
}
Transaction Workings
Snapshot Isolation
- After calling
db.startTransaction()
to start a transaction, the "snapshot" is generated only after the first read operation. - Before calling
transaction.commit()
to commit the transaction, all read and write operations are performed on the "snapshot". - After the transaction is successfully committed, the data in the "snapshot" is persisted to disk, and the relevant document data is updated.
Locking and Write Conflicts
- When a transaction modifies a document, it locks the relevant document to prevent it from being affected by other changes until the transaction completes.
- If a transaction cannot acquire the lock for the document it attempts to modify, the transaction will abort and a write conflict will occur.
- Reading documents does not require the same lock as modifying documents.
Error Handling
It is recommended to use try-catch
to catch exceptions when performing transaction operations:
try {
const transaction = await db.startTransaction()
// Transaction operations involving document modifications
await transaction.collection('orders').add({
data: { /* Order data */ }
})
// If an error occurs, throw an exception to trigger a rollback
if (someCondition) {
throw new Error('Business logic error')
}
await transaction.collection('products').doc('product-id').update({
data: { stock: db.command.inc(-1) }
})
await transaction.commit()
} catch (error) {
console.error('Transaction failed, automatically rolled back:', error)
// Handle exceptions when a write conflict occurs
}
Transaction Limitations
Operation Limitations
Limitation | Description | Impact |
---|---|---|
Operation Count | Maximum of 100 operations per transaction | Requires reasonable planning of transaction scope |
Execution Time | Transaction execution time must not exceed 30 seconds | Avoid complex computations and external calls |
Document Operations | Only the doc method is supported; the where method is not supported | The current transaction only supports single-document operations |
DDL Operations | Avoid initiating transactions during DDL operations | May cause transactions to fail to acquire locks |
Notes
// ❌ Error example: Making external calls within a transaction
await db.runTransaction(async transaction => {
// Do not call cloud functions or external APIs within a transaction
const result = await wx.cloud.callFunction({
name: 'someFunction'
})
// Transaction operations...
})
// ✅ Correct example: Get the data first, then execute the transaction
const externalData = await wx.cloud.callFunction({
name: 'someFunction'
})
await db.runTransaction(async transaction => {
// Use pre-fetched data
await transaction.collection('data').add({
data: externalData.result
})
})
Best Practices
Transaction Design Principles
- Avoid long-running transactions: Transactions running for an extended period (exceeding 30s) may be automatically terminated
- Avoid performing excessive operations: When a transaction contains too many operations, it may impact database performance
- Avoid performing transactions during DDL operations: During index creation or database deletion, transactions may fail to acquire locks
- Use try-catch to catch exceptions: Detect and handle issues like write conflicts and network exceptions early
Performance Optimization
- Split transactions into smaller ones: To prevent long-running and excessive operations scenarios
- Index Optimization: Create indexes for fields queried within transactions.
- Data Prefetching: Pre-fetch required data outside the transaction