175 lines
4.9 KiB
Markdown
175 lines
4.9 KiB
Markdown
---
|
|
title: Use Batch Operations for Multiple Writes
|
|
impact: HIGH
|
|
impactDescription: Atomic transactions, 10x fewer API calls, consistent state
|
|
tags: query, batch, transactions, performance
|
|
---
|
|
|
|
## Use Batch Operations for Multiple Writes
|
|
|
|
Batch operations combine multiple create/update/delete operations into a single atomic transaction. This ensures consistency and dramatically reduces API calls.
|
|
|
|
**Incorrect (individual requests):**
|
|
|
|
```javascript
|
|
// Creating multiple records individually
|
|
async function createOrderWithItems(order, items) {
|
|
// If any fails, partial data remains!
|
|
const createdOrder = await pb.collection('orders').create(order);
|
|
|
|
for (const item of items) {
|
|
await pb.collection('order_items').create({
|
|
...item,
|
|
order: createdOrder.id
|
|
});
|
|
}
|
|
// 1 + N API calls, not atomic
|
|
}
|
|
|
|
// Updating multiple records
|
|
async function updatePrices(products) {
|
|
for (const product of products) {
|
|
await pb.collection('products').update(product.id, {
|
|
price: product.newPrice
|
|
});
|
|
}
|
|
// N API calls, some might fail leaving inconsistent state
|
|
}
|
|
|
|
// Mixed operations
|
|
async function transferFunds(fromId, toId, amount) {
|
|
// NOT ATOMIC - can leave invalid state!
|
|
await pb.collection('accounts').update(fromId, { 'balance-': amount });
|
|
// If this fails, money disappears!
|
|
await pb.collection('accounts').update(toId, { 'balance+': amount });
|
|
}
|
|
```
|
|
|
|
**Correct (using batch operations):**
|
|
|
|
```javascript
|
|
// Atomic batch create
|
|
async function createOrderWithItems(order, items) {
|
|
const batch = pb.createBatch();
|
|
|
|
// Pre-generate order ID so items can reference it in the same batch
|
|
// PocketBase accepts custom IDs (15-char alphanumeric)
|
|
const orderId = crypto.randomUUID().replaceAll('-', '').slice(0, 15);
|
|
|
|
// Queue order creation with known ID
|
|
batch.collection('orders').create({ ...order, id: orderId });
|
|
|
|
// Queue all items referencing the pre-generated order ID
|
|
items.forEach(item => {
|
|
batch.collection('order_items').create({
|
|
...item,
|
|
order: orderId
|
|
});
|
|
});
|
|
|
|
// Execute atomically
|
|
const results = await batch.send();
|
|
// All succeed or all fail together
|
|
|
|
return {
|
|
order: results[0],
|
|
items: results.slice(1)
|
|
};
|
|
}
|
|
|
|
// Batch updates
|
|
async function updatePrices(products) {
|
|
const batch = pb.createBatch();
|
|
|
|
products.forEach(product => {
|
|
batch.collection('products').update(product.id, {
|
|
price: product.newPrice
|
|
});
|
|
});
|
|
|
|
const results = await batch.send();
|
|
// 1 API call, atomic
|
|
return results;
|
|
}
|
|
|
|
// Batch upsert (create or update)
|
|
async function syncProducts(products) {
|
|
const batch = pb.createBatch();
|
|
|
|
products.forEach(product => {
|
|
batch.collection('products').upsert({
|
|
id: product.sku, // Use SKU as ID for upsert matching
|
|
name: product.name,
|
|
price: product.price,
|
|
stock: product.stock
|
|
});
|
|
});
|
|
|
|
return batch.send();
|
|
}
|
|
|
|
// Mixed operations in transaction
|
|
// NOTE: Batch operations respect API rules per-operation, but ensure your
|
|
// business logic validates inputs (e.g., sufficient balance) server-side
|
|
// via hooks or API rules to prevent unauthorized transfers.
|
|
async function transferFunds(fromId, toId, amount) {
|
|
const batch = pb.createBatch();
|
|
|
|
batch.collection('accounts').update(fromId, { 'balance-': amount });
|
|
batch.collection('accounts').update(toId, { 'balance+': amount });
|
|
|
|
// Create audit record
|
|
batch.collection('transfers').create({
|
|
from: fromId,
|
|
to: toId,
|
|
amount,
|
|
timestamp: new Date()
|
|
});
|
|
|
|
// All three operations atomic
|
|
const [fromAccount, toAccount, transfer] = await batch.send();
|
|
return { fromAccount, toAccount, transfer };
|
|
}
|
|
|
|
// Batch delete
|
|
async function deletePostWithComments(postId) {
|
|
// First get comment IDs
|
|
const comments = await pb.collection('comments').getFullList({
|
|
filter: pb.filter('post = {:postId}', { postId }),
|
|
fields: 'id'
|
|
});
|
|
|
|
const batch = pb.createBatch();
|
|
|
|
// Queue all deletions
|
|
comments.forEach(comment => {
|
|
batch.collection('comments').delete(comment.id);
|
|
});
|
|
batch.collection('posts').delete(postId);
|
|
|
|
await batch.send();
|
|
// Post and all comments deleted atomically
|
|
}
|
|
```
|
|
|
|
**Batch operation limits:**
|
|
- **Must be enabled first** in Dashboard > Settings > Application (disabled by default; returns 403 otherwise)
|
|
- Operations execute in a single database transaction
|
|
- All succeed or all rollback
|
|
- Respects API rules for each operation
|
|
- Configurable limits: `maxRequests`, `timeout`, and `maxBodySize` (set in Dashboard)
|
|
- **Avoid large file uploads** in batches over slow networks -- they block the entire transaction
|
|
- Avoid custom hooks that call slow external APIs within batch operations
|
|
|
|
**When to use batch:**
|
|
|
|
| Scenario | Use Batch? |
|
|
|----------|-----------|
|
|
| Creating parent + children | Yes |
|
|
| Bulk import/update | Yes |
|
|
| Financial transactions | Yes |
|
|
| Single record operations | No |
|
|
| Independent operations | Optional |
|
|
|
|
Reference: [PocketBase Batch API](https://pocketbase.io/docs/api-records/#batch-operations)
|