Developer Guides
Using Batch Operations

Batch Operations

Overview

Batch operations in AD4M allow you to group multiple perspective mutations into a single atomic transaction. This is useful when you need to perform multiple related changes that should either all succeed or all fail together, and when you want to optimize performance by reducing internal engine updates (SurrealDB indexing, and Prolog inference if activated) and network operations.

Key Benefits

1. Atomicity

  • All operations in a batch are applied together or none are applied
  • Prevents partial updates that could leave your data in an inconsistent state
  • Ideal for complex operations that require multiple related changes

2. Performance Optimization

  • Reduces the number of internal engine updates (SurrealDB indexing, query subscriptions, and Prolog inference if activated)
  • Minimizes the number of commits to the neighbourhood's link language
  • Batches network operations for shared perspectives
  • Improves overall system responsiveness when making multiple changes

Using Transactions

The recommended way to use batch operations is Ad4mModel.transaction(), which creates a batch, runs your callback, and commits only on success:

await Ad4mModel.transaction(perspective, async (tx) => {
  const recipe = await Recipe.create(perspective, { name: "Cake" }, { batchId: tx.batchId });
 
  const comment = new Comment(perspective);
  comment.body = "Looks great!";
  await comment.save(tx.batchId);
 
  // Relation methods also accept batchId
  await recipe.addComments(comment, tx.batchId);
});
// All operations commit together, or none do if an error is thrown

If any operation inside the callback throws, the batch is never committed and no changes are persisted.

Complex Data Models

When your application needs to maintain relationships between multiple entities:

await Ad4mModel.transaction(perspective, async (tx) => {
  const userProfile = await UserProfile.create(perspective, { name: "Alice" }, { batchId: tx.batchId });
  const userSettings = await UserSettings.create(perspective, { theme: "dark" }, { batchId: tx.batchId });
 
  for (const preference of userPreferences) {
    await preference.save(tx.batchId);
  }
});

Bulk Operations

When performing operations on multiple items:

await Ad4mModel.transaction(perspective, async (tx) => {
  for (const task of tasks) {
    task.status = 'completed';
    await task.save(tx.batchId);
  }
});

State Transitions

When an operation requires multiple coordinated changes:

await Ad4mModel.transaction(perspective, async (tx) => {
  await sourceList.removeTask(task, tx.batchId);
  await targetList.addTask(task, tx.batchId);
  task.status = 'moved';
  await task.save(tx.batchId);
});

Creating Related Subjects

await Ad4mModel.transaction(perspective, async (tx) => {
  // Create main subject
  const post = await BlogPost.create(perspective, { title: "My First Post" }, { batchId: tx.batchId });
 
  // Create related subjects
  const comments = initialComments.map(text => {
    const c = new Comment(perspective);
    c.content = text;
    c.postId = post.id;
    return c;
  });
 
  // Save all related subjects in the same transaction
  await Promise.all(comments.map(c => c.save(tx.batchId)));
});

Validation Before Commit

Validate data before any writes — if validation fails, the thrown error prevents commit:

await Ad4mModel.transaction(perspective, async (tx) => {
  // Validate all ingredients first
  for (const ingredient of ingredients) {
    if (!await validateIngredient(ingredient)) {
      throw new Error(`Invalid ingredient: ${ingredient}`);
    }
  }
 
  // If all valid, proceed with updates
  recipe.ingredients = ingredients;
  await recipe.save(tx.batchId);
 
  // Create ingredient references
  for (const ingredient of ingredients) {
    const ref = new IngredientReference(perspective);
    ref.recipeId = recipe.id;
    ref.ingredientId = ingredient.id;
    await ref.save(tx.batchId);
  }
});
// Nothing is persisted if validation threw

Advanced: Manual Batch Control

For lower-level control (e.g. mixing model operations with raw link operations), you can manage batches directly via PerspectiveProxy:

const batchId = await perspective.createBatch();
 
try {
  // Add multiple links in the batch
  await perspective.add(link1, 'shared', batchId);
  await perspective.add(link2, 'shared', batchId);
  
  // Remove a link in the same batch
  await perspective.remove(oldLink, batchId);
  
  // Update a link in the batch
  await perspective.update(existingLink, newLink, batchId);
 
  // Model operations also accept batchId
  const recipe = new Recipe(perspective);
  recipe.title = "Chocolate Cake";
  await recipe.save(batchId);
 
  // Commit all changes atomically
  const result = await perspective.commitBatch(batchId);
  console.log('Added:', result.additions.length);
  console.log('Removed:', result.removals.length);
} catch (error) {
  // If any operation fails, none of the changes are applied
  console.error('Batch operation failed:', error);
}

Under the hood, Ad4mModel.transaction() calls createBatch() and commitBatch() for you.

Technical Details

Batch Storage

  • Batches are stored in memory until committed
  • Each batch has a unique UUID
  • Changes are tracked separately for shared and local links

Engine Synchronization

  • Internal indexes (SurrealDB, query subscriptions, and Prolog facts if activated) are updated only once per batch commit
  • Updates are performed using a oneshot channel to ensure completion
  • The engine state remains consistent with all changes

Link Language Integration

  • For shared perspectives, commits to the link language happen only once per batch
  • Reduces network operations in distributed scenarios

Best Practices

  1. Prefer transaction(): Use Ad4mModel.transaction() over manual createBatch()/commitBatch() — if the callback throws, the batch is not committed and no changes are persisted.

  2. Batch Size: Keep batches focused on related operations. Avoid extremely large batches that could impact memory usage — split very large operations into multiple smaller batches.

  3. Error Handling: With transaction(), errors thrown inside the callback automatically prevent commit. With manual batches, wrap operations in try/catch.

  4. Resource Management: Don't keep batches open longer than necessary. With transaction() this is handled for you.

  5. Testing: Test both successful and failed batch scenarios. Verify data consistency after batch operations.

Limitations and Considerations

  1. Memory Usage

    • Batches store changes in memory until committed
    • Very large batches may impact system performance
  2. Concurrency

    • Batch operations don't provide explicit locking
    • Consider application-level synchronization for concurrent operations
  3. Network Connectivity

    • For shared perspectives, ensure stable network connectivity before committing large batches
    • Consider implementing retry logic for network-related failures

Migration Guide

Existing code using manual createBatch()/commitBatch() will continue to work. To modernize:

// Before: manual batch management
const batchId = await perspective.createBatch();
try {
  await recipe.save(batchId);
  await comment.save(batchId);
  await perspective.commitBatch(batchId);
} catch (error) {
  console.error(error);
}
 
// After: transaction() handles it
await Ad4mModel.transaction(perspective, async (tx) => {
  await recipe.save(tx.batchId);
  await comment.save(tx.batchId);
});

No schema changes or data migration is required.