tekartik / sembast.dart

Simple io database
BSD 2-Clause "Simplified" License
780 stars 64 forks source link

transaction precision #277

Closed flutter-painter closed 3 years ago

flutter-painter commented 3 years ago

Hi Alex ! Thanks again for the great work on sembast.

Please could you enlighten on transactions, From the documentation it seems they are only for bulks add operations :

// Update within transaction (not necessary, update is already done in
// a transaction

Would transaction make any sense for bulk deletions ? I fail to see what is the most appropriate way for bulk updates and often resort to deleteAll(), addAll().

alextekartik commented 3 years ago

Write operations should be grouped in transactions for consistency and performance You can read/add/delete/update/write in the same transaction.

// Let's assume a store with the following products
var store = intMapStoreFactory.store('product');
await store.addAll(db, [
  {'name': 'Lamp', 'price': 10, 'id': 'lamp'},
  {'name': 'Chair', 'price': 100, 'id': 'chair'},
  {'name': 'Table', 'price': 250, 'id': 'table'}
]);

Let's assume you want a function to update all of your products

await updateProducts(
  [
    {'name': 'Lamp', 'price': 17, 'id': 'lamp'},    // Price modified
    {'name': 'Bike', 'price': 999, 'id': 'bike'},   // Added
    {'name': 'Chair', 'price': 100, 'id': 'chair'}, // Unchanged
                                                    // Product 'table' had been removed           
  ],
);

A first basic implementation would be (but could be improved as in fact 2 transactions are created here)

// Update without using transactions
Future<void> updateProducts(
  List<Map<String, Object?>> products) async {
  // Delete all existing products first.
  // One transaction is created here
  await store.delete(db);
  // Add all products
  // One transaction is created here
  await store.addAll(db, products);
}

A small improvment would be to use a transaction:

// Update in a transaction
Future<void> updateProducts(
    List<Map<String, Object?>> products) async {
  await db.transaction((transaction) async {
    // Delete all
    await store.delete(transaction);
    // Add all
    await store.addAll(transaction, products);
  });
}

However we are still deleting everything first, so even if a product does not change, a write is performed.

A better optimized version would check for deleted/updated/added items to only perform the necessary writes:

/// Read products by ids and return a map
Future<Map<String, RecordSnapshot<int, Map<String, Object?>>>>
getProductsByIds(DatabaseClient db, List<String> ids) async {
  var snapshots = await store.find(db,
      finder: Finder(
          filter: Filter.or(
              ids.map((e) => Filter.equals('id', e)).toList())));
  return <String, RecordSnapshot<int, Map<String, Object?>>>{
    for (var snapshot in snapshots)
      snapshot.value['id']!.toString(): snapshot
  };
}

/// Update products
/// 
/// - Unmodified records remain untouched
/// - Modified records are updated
/// - New records are added.
/// - Missing one are deleted
Future<void> updateProducts(
    List<Map<String, Object?>> products) async {
  await db.transaction((transaction) async {
    var productIds =
    products.map((map) => map['id'] as String).toList();
    var map = await getProductsByIds(db, productIds);
    // Watch for deleted item
    var keysToDelete = (await store.findKeys(transaction)).toList();
    for (var product in products) {
      var snapshot = map[product['id'] as String];
      if (snapshot != null) {
        // The record current key
        var key = snapshot.key;
        // Remove from deletion list
        keysToDelete.remove(key);
        // Don't update if no change
        if (const DeepCollectionEquality()
            .equals(snapshot.value, product)) {
          // no changes
          continue;
        } else {
          // Update product
          await store.record(key).put(transaction, product);
        }
      } else {
        // Add missing product
        await store.add(transaction, product);
      }
    }
    // Delete the one not present any more
    await store.records(keysToDelete).delete(transaction);
  });
}
flutter-painter commented 3 years ago

Thank you so much for this comprehensive example, I'll aim at the last one !