Isar Plus

Transactions

Ensure data consistency with ACID-compliant transactions

Transactions

Transactions combine multiple database operations in a single atomic unit of work. Isar provides ACID-compliant transactions with automatic rollback.

All Isar transactions are ACID compliant - Atomic, Consistent, Isolated, Durable.

Overview

Transactions ensure data consistency:

  • Atomic - All operations succeed or none do
  • Consistent - Data remains valid
  • Isolated - Concurrent transactions don't interfere
  • Durable - Committed changes persist

Transaction Types

TypeSync MethodAsync MethodUse Case
Read.read().readAsync()Consistent reads
Write.write().writeAsync()Data modifications

Most read operations use implicit transactions automatically.

Read Transactions

Read transactions provide a consistent snapshot of the database:

@collection
class Contact {
  Id? id;
  late String name;
  late int age;
}
// Explicit async read transaction
final result = await isar.readAsync((isar) async {
  final contacts = isar.contacts.where().findAll();
  final count = isar.contacts.count();
  
  return {
    'contacts': contacts,
    'count': count,
  };
});
// Synchronous read transaction
final result = isar.read((isar) {
  final contacts = isar.contacts.where().findAll();
  final count = isar.contacts.count();
  
  return {
    'contacts': contacts,
    'count': count,
  };
});
// Implicit transaction (automatic)
final contacts = await isar.contacts.where().findAllAsync();
// Isar wraps this in a transaction automatically

Async read transactions run in parallel with other transactions!

Write Transactions

All write operations must be wrapped in an explicit transaction:

await isar.writeAsync((isar) async {
  final contact = Contact()
    ..name = 'John Doe'
    ..age = 25;
  
  isar.contacts.put(contact);
});

Auto Commit

Transactions auto-commit on success:

await isar.writeAsync((isar) async {
  isar.contacts.put(contact1);
  isar.contacts.put(contact2);
  isar.contacts.put(contact3);
  // All changes committed together ✅
});

Auto Rollback

Transactions auto-rollback on error:

try {
  await isar.writeAsync((isar) async {
    isar.contacts.put(contact1); // ✅ Executed
    isar.contacts.put(contact2); // ✅ Executed
    throw Exception('Error!');
    isar.contacts.put(contact3); // ❌ Not executed
  });
} catch (e) {
  // All changes rolled back ↩️
  print('Transaction failed: $e');
}

When a transaction fails, it must not be used again, even if you catch the error.

Best Practices

✅ DO: Batch Operations

// ✅ Good - Single transaction
await isar.writeAsync((isar) async {
  for (var contact in contacts) {
    isar.contacts.put(contact);
  }
});

// ✅ Even better - Bulk operation
await isar.writeAsync((isar) async {
  isar.contacts.putAll(contacts);
});

❌ DON'T: Multiple Transactions

// ❌ Bad - Many transactions (slow!)
for (var contact in contacts) {
  await isar.writeAsync((isar) async {
    isar.contacts.put(contact);
  });
}

✅ DO: Minimize Transaction Scope

// ✅ Good - Prepare data outside transaction
final processedContacts = contacts.map((c) => 
  Contact()
    ..name = c.name.toUpperCase()
    ..age = c.age
).toList();

await isar.writeAsync((isar) async {
  isar.contacts.putAll(processedContacts);
});

❌ DON'T: Heavy Operations Inside

// ❌ Bad - Heavy processing in transaction
await isar.writeAsync((isar) async {
  final processedContacts = contacts.map((c) => 
    Contact()
      ..name = c.name.toUpperCase()
      ..age = c.age
  ).toList();
  
  isar.contacts.putAll(processedContacts);
});

❌ DON'T: Network Calls

// ❌ Very Bad - Network call in transaction
await isar.writeAsync((isar) async {
  final response = await http.get('https://api.example.com/data');
  final contacts = parseContacts(response.body);
  isar.contacts.putAll(contacts);
});

// ✅ Good - Network call outside
final response = await http.get('https://api.example.com/data');
final contacts = parseContacts(response.body);

await isar.writeAsync((isar) async {
  isar.contacts.putAll(contacts);
});

Never perform network calls, file I/O, or other long-running operations inside transactions!

Complex Transactions

Multiple Collections

await isar.writeAsync((isar) async {
  // Create user
  final user = User()..name = 'John';
  isar.users.put(user);
  
  // Create profile linked to user
  final profile = Profile()
    ..userId = user.id
    ..bio = 'Developer';
  isar.profiles.put(profile);
  
  // Create posts
  final posts = [
    Post()..userId = user.id..title = 'First Post',
    Post()..userId = user.id..title = 'Second Post',
  ];
  isar.posts.putAll(posts);
  
  // All operations committed together ✅
});

Conditional Operations

await isar.writeAsync((isar) async {
  final user = await isar.users.get(userId);
  
  if (user != null && user.age >= 18) {
    user.verified = true;
    isar.users.put(user);
  } else {
    throw Exception('User not eligible');
  }
});

Update with Validation

await isar.writeAsync((isar) async {
  final users = isar.users
    .where()
    .ageGreaterThan(18)
    .findAll();
  
  for (var user in users) {
    if (!user.verified) {
      user.verified = true;
      user.verifiedAt = DateTime.now();
      isar.users.put(user);
    }
  }
});

Transaction Isolation

// Multiple read transactions run in parallel
final future1 = isar.readAsync((isar) async {
  return isar.contacts.where().findAll();
});

final future2 = isar.readAsync((isar) async {
  return isar.contacts.count();
});

// Both execute simultaneously ⚡
final results = await Future.wait([future1, future2]);
// Readers get consistent snapshot
unawaited(isar.writeAsync((isar) async {
  await Future.delayed(Duration(seconds: 2));
  isar.contacts.put(newContact);
}));

// This read sees old data (consistent snapshot)
final contacts = await isar.contacts.where().findAllAsync();
// newContact is NOT included
// Write transactions execute serially
final future1 = isar.writeAsync((isar) async {
  await Future.delayed(Duration(seconds: 1));
  isar.contacts.put(contact1);
});

final future2 = isar.writeAsync((isar) async {
  isar.contacts.put(contact2);
});

// future2 waits for future1 to complete

Error Handling

Basic Error Handling

try {
  await isar.writeAsync((isar) async {
    isar.contacts.put(contact);
  });
  print('Transaction succeeded');
} catch (e) {
  print('Transaction failed: $e');
  // Changes automatically rolled back
}

Custom Validation

class ValidationException implements Exception {
  final String message;
  ValidationException(this.message);
}

try {
  await isar.writeAsync((isar) async {
    if (contact.age < 0) {
      throw ValidationException('Age cannot be negative');
    }
    isar.contacts.put(contact);
  });
} on ValidationException catch (e) {
  print('Validation error: ${e.message}');
} catch (e) {
  print('Unexpected error: $e');
}

Retry Logic

Future<void> putWithRetry(Contact contact, {int maxAttempts = 3}) async {
  for (var attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      await isar.writeAsync((isar) async {
        isar.contacts.put(contact);
      });
      return; // Success
    } catch (e) {
      if (attempt == maxAttempts) rethrow;
      await Future.delayed(Duration(milliseconds: 100 * attempt));
    }
  }
}

Synchronous vs Asynchronous

// ✅ Use async in UI isolate
await isar.writeAsync((isar) async {
  isar.contacts.put(contact);
});

// Doesn't block UI
// ✅ Use sync in background isolate
isar.write((isar) {
  isar.contacts.put(contact);
});

// Faster, but blocks current isolate

Default to async in UI code. Use sync only in background isolates for maximum performance.

Performance Tips

  1. Batch Operations

    // ✅ Fast - 1 transaction
    await isar.writeAsync((isar) => isar.contacts.putAll(list));
    
    // ❌ Slow - N transactions
    for (var item in list) {
      await isar.writeAsync((isar) => isar.contacts.put(item));
    }
  2. Minimize Duration

    // ✅ Fast
    final data = prepareData();
    await isar.writeAsync((isar) => isar.contacts.putAll(data));
    
    // ❌ Slow
    await isar.writeAsync((isar) {
      final data = prepareData(); // Heavy operation
      isar.contacts.putAll(data);
    });
  3. Use Bulk Operations

    // ✅ Optimized
    await isar.writeAsync((isar) async {
      isar.contacts.putAll(contacts);
      isar.posts.deleteAll(postIds);
    });

Common Patterns

Create or Update

await isar.writeAsync((isar) async {
  final existing = isar.users
    .where()
    .emailEqualTo(user.email)
    .findFirst();
  
  if (existing != null) {
    user.id = existing.id; // Reuse ID
  }
  
  isar.users.put(user);
});

Atomic Counter

Future<int> incrementCounter(String key) async {
  return await isar.writeAsync((isar) async {
    final counter = isar.counters
      .where()
      .keyEqualTo(key)
      .findFirst() ?? Counter()..key = key..value = 0;
    
    counter.value++;
    isar.counters.put(counter);
    return counter.value;
  });
}

Bulk Update

await isar.writeAsync((isar) async {
  final users = isar.users
    .where()
    .statusEqualTo('pending')
    .findAll();
  
  for (var user in users) {
    user.status = 'active';
  }
  
  isar.users.putAll(users);
});

Next Steps

Last Update