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
| Type | Sync Method | Async Method | Use 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 automaticallyAsync 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 completeError 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 isolateDefault to async in UI code. Use sync only in background isolates for maximum performance.
Performance Tips
-
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)); } -
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); }); -
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