「这是我参与11月更文挑战的第28天,活动详情查看:2021最后一次更文挑战」。
Flutter持久化库drift(原moor)官方文档翻译汇总 - 掘金 (juejin.cn)
本文翻译自 drift 的 官方文档 Many to many relationships (simonbinder.eu)。
肉翻多有不足,不吝赐教。
多对多关系
用 drift 对购物车建模的一个示例
定义Model(模型)
在本例中,我们要对一个购物系统建模,其中一些查询是在 drift 中。 首先,我们需要存储一些可购买的物品。
class BuyableItems extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get description => text()();
IntColumn get price => integer()();
// 可根据需要添加更多列
}
我们要定义为购物车定义两个表:一个是购物车本身,另外一个是购物车中的物品。
class ShoppingCarts extends Table {
IntColumn get id => integer().autoIncrement()();
// 创建购物车时也可以存储更多的信息。
}
@DataClassName('ShoppingCartEntry')
class ShoppingCartEntries extends Table {
// 购物车就该包含 id 这个项目。
IntColumn get shoppingCart => integer()();
// id of the item in this cart
// 在此购物车中的物品的 id
IntColumn get item => integer()();
// 可以存储一些附加信息,如添加物品的数量。
}
drift 会为这三个表生成匹配的类。但是在应用中为购物车建模要使用三个类会很让人烦恼。让我们写一个简单的类来代表整个购物车:
/// 代表一个包含装在其中所有物品的完整的购物车
class CartWithItems {
final ShoppingCart cart;
final List<BuyableItem> items;
CartWithItems(this.cart, this.items);
}
插入
我们想向数据库中写一个 CartWithItems
实例。我们假定所有的 BuyableItem
已经在数据库中存在(我们需要通过 into(buyableItems).insert(BuyableItemsCompanion(...))
来存储它们)。然后,我们可以如下插入整个购物车:
Future<void> writeShoppingCart(CartWithItems entry) {
return transaction((_) async {
final cart = entry.cart;
// 首先,我们写入 购物车
await into(shoppingCarts).insert(cart, mode: InsertMode.replace);
// 然后我们替换购物车中的所有物品,所以要先删除原有的
await (delete(shoppingCartEntries)
..where((entry) => entry.shoppingCart.equals(cart.id)))
.go();
// 然后写入新的物品
for (final item in entry.items) {
await into(shoppingCartEntries).insert(ShoppingCartEntry(shoppingCart: cart.id, item: item.id));
}
});
}
我们也可以定义一个辅助方法用来创建一个新的空购物车:
Future<CartWithItems> createEmptyCart() async {
final id = await into(shoppingCarts).insert(const ShoppingCartsCompanion());
final cart = ShoppingCart(id: id);
// 我们将 items 属性设置为 [],因为我们刚刚创建了购物车,它还是空的。
return CartWithItems(cart, []);
}
选择一个购物车
因为包含多个组件的 CartWithItems
类在数据库中是相互分离的(购物车的信息,添加的物品的信息),所以我们需要合并两个流。
rxdart
库提供 combineLatest2
方法在会有帮助,它允许我们:
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) {
// 合并带 buyableItems 的 shoppingCartEntries。
// 这里我们只关注物品。
return rows.map((row) => row.readTable(buyableItems)).toList();
});
// 现在,我们可以把两个查询合并到一个流里。
return Rx.combineLatest2(cartStream, contentStream,
(ShoppingCart cart, List<BuyableItem> items) {
return CartWithItems(cart, items);
});
}
选择所有的购物车
现在我们要监视所有的购物车和每个购物车里的物品,而不是单个购物车和与其关联的物品。对于这类变换,RxDart's switchMap
就派上用场了:
Stream<List<CartWithItems>> watchAllCarts() {
// 启动对所有购物车的监视
final cartStream = select(shoppingCarts).watch();
return cartStream.switchMap((carts) {
// 无论何时,购物车发生变动,这个方法都会被调用。
// 对于每个购物车,现在我们想加载所有的物品到里面。
// (这里我们创建一个购物车 id 的 Map 只是为了性能原因)
final idToCart = {for (var cart in carts) cart.id: cart};
final ids = idToCart.keys;
// 选择在任何发现的购物车中的所有物品
final entryQuery = select(shoppingCartEntries).join(
[
innerJoin(
buyableItems,
buyableItems.id.equalsExp(shoppingCartEntries.item),
)
],
)..where(shoppingCartEntries.shoppingCart.isIn(ids));
return entryQuery.watch().map((rows) {
// 为每一个购物车存储物品的列表,为了更快的查询,再次使用了 Map。
final idToItems = <int, List<BuyableItem>>{};
// 对于一个购物车中包含的每一个物品(数据行),把它们放到物品的 Map 中。
for (var row in rows) {
final item = row.readTable(buyableItems);
final id = row.readTable(shoppingCartEntries).shoppingCart;
idToItems.putIfAbsent(id, () => []).add(item);
}
// 最终,只剩下合并带着所有物品 Map 的购物车的 Map。
return [
for (var id in ids)
CartWithItems(idToCart[id], idToItems[id] ?? []),
];
});
});
}