Files
shiftcraft/.claude/skills/pocketbase-best-practices/rules/query-batch-operations.md
2026-04-17 23:26:01 +00:00

4.9 KiB

title, impact, impactDescription, tags
title impact impactDescription tags
Use Batch Operations for Multiple Writes HIGH Atomic transactions, 10x fewer API calls, consistent state 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):

// 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):

// 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