本博客的主题是事件驱动方法。我坚信这是一种让我们的应用程序更贴近业务的方法。通过这样做,我们可以在系统设计和代码中更好地反映业务流程。这很棒,因为它带来了多种好处:更容易演进、弹性以及更好地管理和跟踪工作流。
不过,有时你并不需要所有这些。
有时您只需要一个用于存放数据包的袋子,或者我们大多数人更喜欢称之为CRUD 。
CRUD 源自我们对数据执行的一组常见操作:创建、读取、更新和删除。这是一种适合内容管理系统的实现方式。其职责是存储和管理数据。当然,我们用基本验证、一致性规则和授权来包装它。我们运行简单的业务逻辑或使用仅在后端可用的信息来丰富数据。您只需输入一些数据并检索它。您输入的就是您得到的。
在 CRUD 中,您没有特定的行为。而且,从系统的角度来看,这些数据没有太多的业务背景。这就是实现是通用的原因。它只对将这些数据放入并检索的用户有意义。用户在阅读时对其进行解释,并根据它在系统之外做出决策。
CRUD 也可以成为概念验证的有效方法。例如,如果我们有一个产品创意,我们会启动一个基本应用程序,无需大量业务工作流,只需基本的数据提取和可视化,以确保这个想法具有潜力。
总体而言,进行 CRUD 没有什么可羞耻的。这是一种有效的实现方式,但用例有限。如果我们明智地选择它,那么一切都很好。
我们还需要记住,我们不应该把这个决定一成不变。
这就是为什么我喜欢将 CRUD 与CQRS搭配使用。为什么?它们不是互相矛盾的吗?在我的世界里不是这样!
CQRS 代表命令查询职责分离。这是一种结构模式。它告诉我们根据行为对业务应用程序进行切分,然后将其分为两个职责:
- 命令处理——可以改变状态但不应返回业务数据的业务逻辑,
- 查询处理 - 返回数据但不改变状态(“提出问题不应该改变答案”)。
如果满足这些规则,我们的内部实现可以是 CRUD,我们可以应用 CQRS 原则。这对于概念验证至关重要。我们从简单、通用的实现开始,一旦出现新的业务需求,我们就会对其进行改进。由于我们已经分离了业务功能,因此我们可以精确调整需要更改的地方。CRUD 不一定是难以维护的混合体!
不过,无论你选择哪种方式,我相信 Pongo 是一款不错的 CRUD 工具。我行我素还是随心所欲?不,我不会告诉你如何生活!
为什么是 Pongo?
- 它是一款 Node.js 工具,因此它是一个轻量级且易于访问的环境,拥有庞大而充满活力的社区。你不应该在潜在的招聘方面遇到问题,
- 它在 PostgreSQL 上运行,因此操作起来很简单,并且可以轻松地在云提供商、本地或Neon、Supabase、Vercel等服务中设置托管。
- 类似 MongoDB 的 API 很容易学习并且为我们许多人所熟知。
- 文档方法和非规范化数据有助于更轻松地设置数据。它非常适合 CRUD 模型,在这种模型中,您希望以与输入相同的形式存储和检索数据,
- Pongo 将为您提供内置迁移,因此无需过多关心数据库模式。
所有这些为快速引导提供了良好的组合。
对于基础知识,你可以查看以前的文章:和文档。
所以请原谅,我不会重复,特别是因为您可能已经读过它们了。让我向您展示一些特别的东西:Pongo 命令处理。
CRUD 中更新记录的典型流程如下:
- 验证传入的请求。
- 读取当前状态。
- 在此基础上做必要的验证。
- 运行业务逻辑。
- 更新状态。
- 保存它。
同样,对于删除:- 验证传入的请求。
- 您读到了当前状态。
- 进行必要的验证,检查是否可以删除它。
- 删除它。
对于创作来说,它甚至更简单;你只需:- 验证传入的请求。
- 根据请求数据生成新状态。
- 保存它。
如果我们想偷偷摸摸地做点什么,我们可以把它包装成一个涵盖所有这些情况的单一流程。- 验证传入的请求。
- 读取当前状态。
- 如果某个状态存在并且您想要更新它,请基于它进行必要的验证。
- 运行业务逻辑。返回新状态、更新状态或 null(如果要删除状态)。
- 取决于业务逻辑的结果:
- 如果状态不存在且结果状态不为空,则创建
- 如果状态存在且结果状态不同且不为空,则更新,
- 如果状态存在且结果状态为空,则删除,
- 否则不做任何事,并安全地处理幂等性。
猜猜怎么着?这正是 Pongo 能为您做的事情。
让我们使用我们最喜欢的购物车示例。其类型可能如下所示:
interface ProductItem { productId: string; quantity: number; }
type PricedProductItem = ProductItem & { unitPrice: number; };
type ShoppingCart = { _id: string; clientId: string; productItems: PricedProductItem[]; productItemsCount: number; totalAmount: number; status: 'Opened' | 'Confirmed'; openedAt: Date; confirmedAt?: Date | undefined; cancelledAt?: Date | undefined; };
|
我们可以定义一组基本的操作:
- 添加产品项目 - 可能未确认或购物车不存在,
- 删除产品项目 - 当我们在未确认的购物车中已经有足够的产品时,这是可能的,
- 确认购物车非空(我们可以确认两次,安全地处理幂等性),
- 取消打开的购物车(我们可以取消两次,安全地处理幂等性)。
业务逻辑可能如下所示:
添加产品项目:
const addProductItem = ( command: { clientId: string; shoppingCartId: string; productItem: PricedProductItem; now: Date; }, state: ShoppingCart | null, ): ShoppingCart => { if (state && state.status === 'Confirmed') throw new Error('Shopping Cart already closed');
const { shoppingCartId, clientId, productItem, now } = command;
const shoppingCart: ShoppingCart = state ?? { _id: shoppingCartId, clientId, openedAt: now, status: 'Opened', productItems: [], totalAmount: 0, productItemsCount: 0, };
const currentProductItem = shoppingCart.productItems.find( (pi) => pi.productId === productItem.productId && pi.unitPrice === productItem.unitPrice, );
if (currentProductItem !== undefined) { currentProductItem.quantity += productItem.quantity; } else { shoppingCart.productItems.push(productItem); }
shoppingCart.totalAmount += productItem.unitPrice * productItem.quantity; shoppingCart.productItemsCount += productItem.quantity;
return shoppingCart; };
|
删除产品项目:
const removeProductItem = ( command: { productItem: PricedProductItem; now: Date; }, state: ShoppingCart | null, ): ShoppingCart => { if (state === null || state.status !== 'Opened') throw new Error('Shopping Cart is not opened');
const { productItem } = command;
const currentProductItem = state.productItems.find( (pi) => pi.productId === productItem.productId && pi.unitPrice === productItem.unitPrice, );
if ( currentProductItem === undefined || currentProductItem.quantity < productItem.quantity ) { throw new Error('Not enough products in shopping carts'); }
state.totalAmount -= productItem.unitPrice * productItem.quantity; state.productItemsCount -= productItem.quantity;
return state; };
|
确认
const confirm = ( command: { now: Date; }, state: ShoppingCart | null, ): ShoppingCart => { if (state === null) throw new Error('Shopping Cart is not opened');
if (state.status === 'Confirmed') return state;
if (state.productItems.length === 0) throw new Error('Shopping Cart is empty');
const { now } = command;
state.status = 'Confirmed'; state.confirmedAt = now;
return state; };
|
取消:
const cancel = (state: ShoppingCart | null): ShoppingCart | null => { if (state != null && state.status === 'Confirmed') throw new Error('Cannot cancel confirmed Shopping Cart'); return null; };
|
然后我们可以进行基本的 Pongo 设置:
import { pongoClient } from "@event-driven-io/pongo";
const connectionString = "postgresql://dbuser:secretpassword@database.server.com:3211/mydb";
const pongo = pongoClient(connectionString); const pongoDb = pongo.db();
const shoppingCarts = pongoDb.collection<ShoppingCart>("shoppingCarts");
|
并将我们的代码插入到一些请求处理管道(例如 Web API)中:
type AddProductItemRequest = Request< { clientId: string; shoppingCartId: string }, unknown, { productId: string; quantity: number } >;
router.post( '/clients/:clientId/shopping-carts/current/product-items', on(async (request: AddProductItemRequest) => { const command = { clientId: request.params.clientId, productItem: { productId: request.body.productId, quantity: request.body.quantity), unitPrice: request.body.unitPrice, }, }; const result = await shoppingCarts.handle((state) => addProductItem(command, state), );
return Ok(result.document); }) );
|
对于所有其他端点,代码将看起来相应。这不是很好吗?
我认为这是启动新的 CRUD 系统或引导新的概念证明的简单快捷的方法。