项目

集成模块:关联产品与订单数据

在这一部分,您将学习如何在多个模块的数据位于同一个物理数据库时,对它们的数据执行数据库级别的 JOIN 操作。

问题概述

模块化的一个核心目的是创建对其它模块隐藏(封装)其内部数据和实现细节的模块。这些模块通过定义良好的 集成服务事件 进行通信。这样,只要您不在这些模块集成点上引入破坏性更改,就可以独立地开发和更改模块实现(甚至模块的数据库结构)。

在非模块化的应用程序中,访问相关数据很容易。您可以编写一个连接 OrdersProducts 数据库表的 LINQ 表达式,通过单个数据库查询获取数据。这更容易实现,并且具有良好的执行性能。

另一方面,在模块化系统中,执行需要访问多个模块内部数据的操作或获取报告会变得更加困难。回想一下 实现集成服务 部分;我们无法在 Ordering 模块内部访问产品数据(IOrderingDbContext 只定义了一个 DbSet<Order>),因此我们需要创建一个集成服务,仅仅为了根据 ID 列表获取产品名称。这种方法实现起来更困难,性能也较差(但如果您不在 UI 上显示太多订单,或者正确实现了缓存层,这仍然是可接受的)。不过,它给了 Catalog 模块在其内部数据库或应用逻辑更改方面的自由。例如,您可以决定将产品数据移动到另一个物理数据库,甚至另一个数据库管理系统(DBMS),而不会影响其他模块。

一种解决方案

如果您希望在模块化系统中执行跨多个模块数据库表的单一数据库查询,您仍然有一些选择。一种选择是创建一个可以访问所有实体(或数据库表)的报告模块。然而,当您这样做时,您需要接受以下限制:

  • 当您更改一个模块的数据库结构时,您也必须更新您的报告代码。这是合理的,但所有模块开发者在这种情况下都应通知您。
  • 您不能轻易更改一个模块的 DBMS。例如,如果您决定为 Catalog 模块使用 MongoDB,而 Ordering 模块仍然使用 SQL Server,那么执行这样的 JOIN 操作将是不可能的。将 Catalog 模块移动到另一个物理服务器上的另一个 SQL Server 数据库也可能会破坏您的报告逻辑。

如果这些对您来说不是问题,或者您能在问题出现时处理它们,那么您可以创建使用多个模块数据的报告模块或聚合模块。

在下一节中,为了保持教程简短,我们将使用主应用程序的代码库来实现这样的 JOIN 操作。但是,您已经学习了如何创建新模块,因此如果需要,您可以创建一个新的报告模块并在该新模块中开发您的 JOIN 逻辑。

实现

在本节中,我们将在主应用程序的 .NET 解决方案中创建一个应用服务。该应用服务将对 ProductOrder 实体执行 LINQ 操作。

定义 IOrderReportingAppService 接口

在您的 IDE 中打开主 ModularCrm .NET 解决方案,在 Services 文件夹下创建一个 Orders 子文件夹,并添加一个 IOrderReportingAppService 接口。以下是该接口的定义:

using ModularCrm.Services.Dtos.Orders;
using Volo.Abp.Application.Services;

namespace ModularCrm.Services.Orders;

public interface IOrderReportingAppService : IApplicationService
{
    Task<List<OrderReportDto>> GetLatestOrders();
}

我们有一个单一的方法 GetLatestOrders,它将返回最新订单的列表。我们还应该定义该方法返回的 OrderReportDto 类。在 Services/Dtos 文件夹下创建 Orders 子文件夹,并创建一个名为 OrderReportDto 的类。

using ModularCrm.Ordering;

namespace ModularCrm.Services.Dtos.Orders;

public class OrderReportDto
{
    // 订单数据
    public Guid OrderId { get; set; }
    public string CustomerName { get; set; } = null!;
    public OrderState State { get; set; }

    // 产品数据
    public Guid ProductId { get; set; }
    public string ProductName { get; set; } = null!;
}

OrderReportDto 包含来自 OrderProduct 两个实体的数据。

添加这些文件后,最终的文件夹结构应如下所示:

visual-studio-order-reporting-app-service

实现 OrderReportingAppService

Services/Orders 文件夹下创建一个名为 OrderReportingAppService 的类。

打开 OrderReportingAppService.cs 文件,并将其内容更改为以下代码块:

using ModularCrm.Catalog;
using ModularCrm.Ordering;
using ModularCrm.Services.Dtos.Orders;
using Volo.Abp.Domain.Repositories;

namespace ModularCrm.Services.Orders;

public class OrderReportingAppService :
    ModularCrmAppService,
    IOrderReportingAppService
{
    private readonly IRepository<Order, Guid> _orderRepository;
    private readonly IRepository<Product, Guid> _productRepository;

    public OrderReportingAppService(
        IRepository<Order, Guid> orderRepository,
        IRepository<Product, Guid> productRepository)
    {
        _orderRepository = orderRepository;
        _productRepository = productRepository;
    }

    public async Task<List<OrderReportDto>> GetLatestOrders()
    {
        var orders = await _orderRepository.GetQueryableAsync();
        var products = await _productRepository.GetQueryableAsync();

        var latestOrders = (from order in orders
                join product in products on order.ProductId equals product.Id
                orderby order.CreationTime descending
                select new OrderReportDto
                {
                    OrderId = order.Id,
                    CustomerName = order.CustomerName,
                    State = order.State,
                    ProductId = product.Id,
                    ProductName = product.Name
                })
            .Take(10)
            .ToList();

        return latestOrders;
    }
}

让我们解释一下这个类:

  • 它注入了 OrderProduct 实体的 仓储 服务。我们可以从主应用程序代码库访问所有模块的所有实体。
  • GetLatestOrders 方法中,我们获取实体的 IQueryable 对象,以便可以创建 LINQ 表达式。
  • 然后,我们使用 join 关键字执行一个 LINQ 表达式,这使我们能够执行使用多个表的单一查询。

就这样。通过这种方式,您可以执行使用来自多个模块数据的 JOIN 查询。但是,如果您将大部分代码写入主应用程序并对多个模块执行操作,您的系统可能就不那么模块化了。这里我们展示这在技术上是可行的。使用时请注意风险。

测试报告服务

我们还没有创建一个使用 OrderReportingAppService 来显示最新订单列表的 UI。但是,我们可以再次使用 Swagger UI 来测试它。

打开 ABP Studio UI,如果应用程序正在运行请停止它,然后重新构建并运行。应用程序启动后,浏览它,然后在 URL 末尾添加 /swagger 以打开 Swagger UI。在这里,找到 OrderReporting API 并执行它,如下所示:

abp-studio-swagger-list-orders

您应该会收到带有产品名称的订单对象。

或者,您可以访问 /api/app/order-reporting/latest-orders URL,直接在浏览器上执行 HTTP API(您需要写入完整的 URL,例如 https://localhost:44303/api/app/order-reporting/latest-orders — 端口号在您的情况下可能不同)。

总结

在本教程的最后三部分中,您学习了三种集成应用程序模块的方法:

  1. 您可以使用集成服务在模块之间进行请求/响应式的通信。
  2. 您可以从一个模块发布事件,并从其他模块订阅这些事件。
  3. 您可以将代码写入主应用程序,这样您就可以访问所有模块的所有实体(及相关数据)。您也可以创建一些可以访问多个模块实体的聚合或报告模块,而不是将其写入主应用程序代码中。

现在,您已经了解了使用 ABP 构建复杂的模块化单体应用程序的基本原理和机制。

下载源代码

您可以 在此处 下载完整的示例解决方案。

另请参阅

请参阅以下部分以获取更多资源。

书店教程

在本教程中,我们有意将应用程序逻辑保持得非常简单,没有为模块构建可用的用户界面。同时,也没有为模块实现授权和本地化。这是为了让您专注于模块化概念。如果您想学习如何使用 ABP 构建真实世界的用户界面,可以查阅 书店教程。其中解释的所有原则和方法在模块化系统中也同样适用。

ABP 可重用应用模块

ABP 从一开始就被设计为模块化的。ABP 团队已经创建了数十个可用于生产环境的 可重用应用模块。您可以研究这些模块(其中一些已经是 免费和开源的)以查看真实世界的模块示例。

当您 创建一个新的 ABP 解决方案 时,其中一些模块已经作为已安装的包(NuGet 和 NPM 包)包含在您的应用程序中。因此,您的初始 ABP 应用程序从第一天起就已经是一个模块化应用程序了。

指南:模块开发最佳实践与规范

ABP 团队基于一些严格的原则和规则创建了 可重用应用模块,以使它们能够在不同场景(包括模块化单体应用程序和微服务系统)中尽可能地被重用。

这些应用模块被设计为通用的、可扩展的、彼此独立的、可在任何 Web 应用程序中重用的、提供多种 UI 和数据库选项等。在单体模块化应用程序中,您通常没有这样的要求。但是,您仍然可以查阅由 ABP 团队准备并在实现这些应用模块时遵循的 最佳实践指南


在本文档中