Last active
November 21, 2023 12:27
-
-
Save simolus3/b881d1a5cbb308d549dd484204e25776 to your computer and use it in GitHub Desktop.
Shopping carts in moor
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'package:moor/moor.dart'; | |
import 'package:moor/moor_vm.dart'; | |
import 'package:rxdart/rxdart.dart'; | |
part 'shopping_cart.g.dart'; | |
class ShoppingCarts extends Table { | |
IntColumn get id => integer().autoIncrement()(); | |
} | |
class BuyableItems extends Table { | |
IntColumn get id => integer().autoIncrement()(); | |
// price, description, idk | |
} | |
@DataClassName('ShoppingCartEntry') | |
class ShoppingCartEntries extends Table { | |
IntColumn get shoppingCart => integer()(); | |
IntColumn get item => integer()(); | |
} | |
class CartWithItems { | |
final ShoppingCart cart; | |
final List<BuyableItem> items; | |
CartWithItems(this.cart, this.items); | |
} | |
@UseMoor(tables: [ShoppingCarts, BuyableItems, ShoppingCartEntries]) | |
class Database extends _$Database { | |
@override | |
final int schemaVersion = 1; | |
Database(QueryExecutor e) : super(e); | |
@override | |
MigrationStrategy get migration { | |
return MigrationStrategy( | |
beforeOpen: (_, details) async { | |
if (details.wasCreated) { | |
// write some fake items | |
for (var i = 0; i < 10; i++) { | |
await into(buyableItems).insert(const BuyableItemsCompanion()); | |
} | |
} | |
}, | |
); | |
} | |
/// Create a new empty shopping cart | |
Future<ShoppingCart> createShoppingCart() async { | |
final id = await into(shoppingCarts).insert(const ShoppingCartsCompanion()); | |
return ShoppingCart(id: id); | |
} | |
Future<List<BuyableItem>> loadAllItems() async { | |
return await select(buyableItems).get(); | |
} | |
Stream<CartWithItems> watchCart(int id) { | |
final cartQuery = select(shoppingCarts) | |
..where((cart) => cart.id.equals(id)); | |
final contentQuery = select(shoppingCartEntries).join( | |
[ | |
innerJoin( | |
buyableItems, | |
buyableItems.id.equalsExp(shoppingCartEntries.item), | |
), | |
], | |
)..where(shoppingCartEntries.shoppingCart.equals(id)); | |
final cartStream = cartQuery.watchSingle(); | |
final contentStream = contentQuery.watch().map((rows) { | |
return rows.map((row) => row.readTable(buyableItems)).toList(); | |
}); | |
return Observable.combineLatest2(cartStream, contentStream, | |
(ShoppingCart cart, List<BuyableItem> items) { | |
return CartWithItems(cart, items); | |
}); | |
} | |
Stream<List<CartWithItems>> watchAllCarts() { | |
// start by watching all carts | |
final cartStream = Observable(select(shoppingCarts).watch()); | |
return cartStream.switchMap((carts) { | |
// this method is called whenever the list of carts changes. For each | |
// cart, we want to load all the items in it. | |
// (we create a map from id to cart here just for performance reasons) | |
final idToCart = {for (var cart in carts) cart.id: cart}; | |
final ids = idToCart.keys; | |
// select all entries that are included in a cart that we found | |
final entryQuery = select(shoppingCartEntries).join( | |
[ | |
innerJoin( | |
buyableItems, | |
buyableItems.id.equalsExp(shoppingCartEntries.item), | |
) | |
], | |
)..where(isIn(shoppingCartEntries.shoppingCart, ids)); | |
return entryQuery.watch().map((rows) { | |
// Store the list of entries for each cart, again using maps for faster | |
// lookups. | |
final idToItems = <int, List<BuyableItem>>{}; | |
// for each entry (row) that is included in a cart, put it in the map | |
// of items. | |
for (var row in rows) { | |
final item = row.readTable(buyableItems); | |
final id = row.readTable(shoppingCartEntries).shoppingCart; | |
idToItems.putIfAbsent(id, () => []).add(item); | |
} | |
// finally, all that's left is to merge the map of carts with the map of | |
// entries | |
return [ | |
for (var id in ids) | |
CartWithItems(idToCart[id], idToItems[id] ?? []), | |
]; | |
}); | |
}); | |
} | |
Future<void> writeShoppingCart(CartWithItems entry) { | |
return transaction((_) async { | |
final cart = entry.cart; | |
// first, we write the shopping cart | |
await into(shoppingCarts).insert(cart, orReplace: true); | |
// replace entries of the cart | |
await (delete(shoppingCartEntries) | |
..where((entry) => entry.shoppingCart.equals(cart.id))) | |
.go(); | |
await into(shoppingCartEntries).insertAll([ | |
for (var item in entry.items) | |
ShoppingCartEntry(shoppingCart: cart.id, item: item.id), | |
]); | |
}); | |
} | |
} | |
void main() async { | |
final db = Database(VMDatabase.memory()); | |
final cart = await db.createShoppingCart(); | |
final availableItems = await db.loadAllItems(); | |
final sub = db.watchCart(cart.id).listen((updated) { | |
print('Cart ${updated.cart} now consists of ${updated.items}'); | |
}); | |
await db.writeShoppingCart(CartWithItems(cart, availableItems.sublist(0, 3))); | |
await db.writeShoppingCart(CartWithItems(cart, availableItems.sublist(3, 6))); | |
await db.writeShoppingCart(CartWithItems(cart, availableItems.sublist(6))); | |
await Future.delayed(const Duration(seconds: 2)); | |
await sub.cancel(); | |
await db.close(); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I spend all my free time online shopping. I often look for bargains. And after reading this article I learned how to do it more efficiently. Now all my purchases are more thoughtful.