Abp vNext : Appliction 層 ApplicationService 中聚合根之間級聯查詢

背景

領域驅動設計(DDD)最佳實踐,聚合根是其內部實體數據訪問的總入口,故聚合根內最好不要有其它聚合根實例對象(屬性導航、集合導航)的引用,僅保留其它聚合根的Id。
這樣就需要考聚合根之間級聯查詢。

下面的查詢示例,涉及 4 個聚合根(數據庫表)的級聯查詢,包括左連接。

聚合根關係

聚合根 RawMaterialOutwarehouseRecord 的定義:


public class RawMaterialOutwarehouseRecord : AuditedAggregateRoot<Guid>
{
    ......

    [NotNull]
    /// <summary>
    /// 原料Id
    /// </summary>
    public Guid RawMaterialId { get; set; } // 其它聚合根Id

    /// <summary>
    /// 訂單Id
    /// </summary>
    public Guid? OrderId { get; set; }  // 其它聚合根Id, 可null

    /// <summary>
    /// 成品Id
    /// </summary>
    public Guid? FinishProductId { get; set; } // 其它聚合根Id,可null

    ......

    // 其它屬性
}

現在需求是,在查詢聚合根 RawMaterialOutwarehouseRecord 集合結果中包含:

  • 聚合根 RawMaterialSerialNumberName
  • 聚合根 OrderSerialNumber
  • 聚合根 FinishProductSerialNumber

有問題查詢實現

先看下面的查詢實現:

    public virtual async Task<PagedResultDto<RawMaterialOutwarehouseRecordWithDetialsDto>> GetRawMaterialOutwarehouseRecordList2Async(GetRawMaterialOutwarehouseRecodsInput input)
    {
        var query =
            from outwarehouseRecord in (await _rawMaterialOutwarehouseRecordRepository.GetQueryableAsync())
            join rawMaterial in (await _rawMaterialRepository.GetQueryableAsync())
                on outwarehouseRecord.RawMaterialId equals rawMaterial.Id
            select new
            {
                outwarehouseRecord,
                rawMaterialSerialNumber = rawMaterial.SerialNumber,
                rawMaterialName = rawMaterial.Name,
            };

        query = query
            .WhereIf(
                !input.Filter.IsNullOrWhiteSpace(),
                x => x.rawMaterialSerialNumber.Contains(input.Filter) ||
                    (x.rawMaterialName != null && x.rawMaterialName.Contains(input.Filter))
            )
            .WhereIf(!string.IsNullOrWhiteSpace(input.RawMaterialSerialNumber), x => x.rawMaterialSerialNumber == input.RawMaterialSerialNumber)
            .WhereIf(!string.IsNullOrWhiteSpace(input.RawMaterialName), x => x.rawMaterialName == input.RawMaterialName)
            .WhereIf(input.MaxOutwarehouseTime != null, x => x.outwarehouseRecord.OutwarehouseTime <= input.MaxOutwarehouseTime)
            .WhereIf(input.MinOutwarehouseTime != null, x => x.outwarehouseRecord.OutwarehouseTime >= input.MinOutwarehouseTime)
            .WhereIf(input.MaxCreationTime != null, x => x.outwarehouseRecord.CreationTime <= input.MaxCreationTime)
            .WhereIf(input.MinCreationTime != null, x => x.outwarehouseRecord.CreationTime >= input.MinCreationTime)
            .WhereIf(input.MaxModifitionTime != null, x => x.outwarehouseRecord.LastModificationTime <= input.MaxModifitionTime)
            .WhereIf(input.MinModifitionTime != null, x => x.outwarehouseRecord.LastModificationTime >= input.MinModifitionTime)
            .PageBy(input.SkipCount, input.MaxResultCount);

        var queryResult = await AsyncExecuter.ToListAsync(query);

        var outwarehouseRecordDtos =  queryResult.Select(x =>
        {
            var outwarehouseRecordDto = ObjectMapper.Map<RawMaterialOutwarehouseRecord, RawMaterialOutwarehouseRecordWithDetialsDto>(x.outwarehouseRecord);

            outwarehouseRecordDto.RawMaterialSerialNumber = x.rawMaterialSerialNumber;
            outwarehouseRecordDto.RawMaterialName = x.rawMaterialName;

            return outwarehouseRecordDto;
        }).ToList();

        foreach (var dto in outwarehouseRecordDtos)
        {
            if (dto.FinishProductId != null)
            {
                dto.FinishProductSerialNumber =
                    (await _finishProductRepository.GetAsync(x.outwarehouseRecord.FinishProductId.Value))
                    .SerialNumber;

            }

            if (dto.OrderId != null)
            {
                dto.FinishProductSerialNumber =
                    (await _orderRepository.GetAsync(x.outwarehouseRecord.OrderId.Value))
                    .SerialNumber;
            }
        }

        var totalCount = await AsyncExecuter.CountAsync(query);

        return new PagedResultDto<RawMaterialOutwarehouseRecordWithDetialsDto>(
           totalCount,
           outwarehouseRecordDtos
       );
    }

出現性能問題的代碼如下:

        foreach (var dto in outwarehouseRecordDtos)
        {
            if (dto.FinishProductId != null)
            {
                dto.FinishProductSerialNumber =
                    (await _finishProductRepository.GetAsync(dto.FinishProductId.Value))
                    .SerialNumber;

            }

            if (dto.OrderId != null)
            {
                dto.FinishProductSerialNumber =
                    (await _orderRepository.GetAsync(dto.OrderId.Value))
                    .SerialNumber;
            }
        }

如果查詢結果需要 100 條 outwarehouseRecordDtos, 那爲了獲取聚合根 OrderFinishProduct 中的屬性,
就需要額外執行 100 * (1 + 1) = 200 次數據庫查詢,嚴重影響查詢效率。

改進查詢實現


namespace WarehouseMs.WarehouseService.RawMaterials;

public class RawMaterialAppService : WarehouseServiceAppServiceBase, IRawMaterialAppService
{
    private readonly IRawMaterialRepository _rawMaterialRepository;
    private readonly IRawMaterialOutwarehouseRecordRepository _rawMaterialOutwarehouseRecordRepository;
    private readonly IFinishProductRepository _finishProductRepository;
    private readonly IOrderRepository _orderRepository;

    public RawMaterialAppService(
        IRawMaterialRepository rawMaterialRepository,
        IRawMaterialOutwarehouseRecordRepository rawMaterialOutwarehouseRecord,
        IFinishProductRepository finishProductRepository,
        IOrderRepository orderRepository)
    {
        _rawMaterialRepository = rawMaterialRepository;
        _rawMaterialOutwarehouseRecordRepository = rawMaterialOutwarehouseRecord;
        _finishProductRepository = finishProductRepository;
        _orderRepository = orderRepository;
    }  

    ......
    public virtual async Task<PagedResultDto<RawMaterialOutwarehouseRecordWithDetialsDto>> GetRawMaterialOutwarehouseRecordListAsync(GetRawMaterialOutwarehouseRecodsInput input)
    {
        var query =
            from outwarehouseRecord in (await _rawMaterialOutwarehouseRecordRepository.GetQueryableAsync())
            join rawMaterial in (await _rawMaterialRepository.GetQueryableAsync())
                on outwarehouseRecord.RawMaterialId equals rawMaterial.Id 

            join leftFinishProduct in (await _finishProductRepository.GetQueryableAsync())
                on outwarehouseRecord.FinishProductId equals leftFinishProduct.Id into LeftJionFinishProduct
            from finishProduct in LeftJionFinishProduct.DefaultIfEmpty() // 左連接

            join leftOrder in (await _orderRepository.GetQueryableAsync())
                 on outwarehouseRecord.OrderId equals leftOrder.Id into LeftJionOrder
            from order in LeftJionOrder.DefaultIfEmpty() // 左連接
            select new 
            {
                outwarehouseRecord,
                rawMaterialSerialNumber = rawMaterial.SerialNumber,
                rawMaterialName = rawMaterial.Name,
                finishProductSerialNumber = finishProduct != null ? finishProduct.SerialNumber : null,
                orderSerialNumber = order != null ? order.SerialNumber : null,
            };

        query = query
            .WhereIf(
                !input.Filter.IsNullOrWhiteSpace(),
                x => x.rawMaterialSerialNumber.Contains(input.Filter) ||
                    (x.rawMaterialName != null && x.rawMaterialName.Contains(input.Filter))
            )
            .WhereIf(!string.IsNullOrWhiteSpace(input.RawMaterialSerialNumber), x => x.rawMaterialSerialNumber == input.RawMaterialSerialNumber)
            .WhereIf(!string.IsNullOrWhiteSpace(input.RawMaterialName), x => x.rawMaterialName == input.RawMaterialName)
            .WhereIf(!string.IsNullOrWhiteSpace(input.FinishProductSerialNumber), x => x.finishProductSerialNumber == input.FinishProductSerialNumber)
            .WhereIf(!string.IsNullOrWhiteSpace(input.OrderSerialNumber), x => x.orderSerialNumber == input.OrderSerialNumber)
            .WhereIf(input.MaxOutwarehouseTime != null, x => x.outwarehouseRecord.OutwarehouseTime <= input.MaxOutwarehouseTime)
            .WhereIf(input.MinOutwarehouseTime != null, x => x.outwarehouseRecord.OutwarehouseTime >= input.MinOutwarehouseTime)
            .WhereIf(input.MaxCreationTime != null, x => x.outwarehouseRecord.CreationTime <= input.MaxCreationTime)
            .WhereIf(input.MinCreationTime != null, x => x.outwarehouseRecord.CreationTime >= input.MinCreationTime)
            .WhereIf(input.MaxModifitionTime != null, x => x.outwarehouseRecord.LastModificationTime <= input.MaxModifitionTime)
            .WhereIf(input.MinModifitionTime != null, x => x.outwarehouseRecord.LastModificationTime >= input.MinModifitionTime);

        // 獲總數(不需要排序)
        var totalCount = await AsyncExecuter.CountAsync(query);

        // 排序
        query =
            from x in query
            orderby (input.Sorting.IsNullOrWhiteSpace() 
                        ? $"{nameof(x.outwarehouseRecord.OutwarehouseTime)} desc" 
                        : input.Sorting
                     )
            select x;

        // 分頁
        query = query.PageBy(input.SkipCount, input.MaxResultCount);

        var queryResult = await AsyncExecuter.ToListAsync(query);

        var outwarehouseRecordDtos = queryResult.Select(x =>
        {
            var outwarehouseRecordDto = ObjectMapper.Map<RawMaterialOutwarehouseRecord, RawMaterialOutwarehouseRecordWithDetialsDto>(x.outwarehouseRecord);

            outwarehouseRecordDto.RawMaterialSerialNumber = x.rawMaterialSerialNumber;
            outwarehouseRecordDto.RawMaterialName = x.rawMaterialName;
            outwarehouseRecordDto.FinishProductSerialNumber = x.finishProductSerialNumber;
            outwarehouseRecordDto.OrderSerialNumber = x.orderSerialNumber;

            return outwarehouseRecordDto;
        }).ToList();

        return new PagedResultDto<RawMaterialOutwarehouseRecordWithDetialsDto>(
           totalCount,
           outwarehouseRecordDtos
       );
    }
   

    ......
}

代碼說明:

  1. 使用 xxxRepository.GetQueryableAsync() 返回聚合根的IQueryable<T>

  2. 關於 ToList 異步方法:
    注意到, 這裏的 ToList異步方法使用了 AsyncExecuter.ToListAsync(), 而不是使用程序集 Microsoft.EntityFrameworkCore 中的 ToListAsync(),
    如果使用程序集 Microsoft.EntityFrameworkCore 中的 ToListAsync(), 在 Appliction 層,就必須添加對 程序集 Microsoft.EntityFrameworkCore 引用,
    導致整個架構鎖定了 EF Core倉儲EfCoreRawMaterialRepository),如果切換其它倉儲實現 ,比如:MongoDB倉儲 (MongoDBRawMaterialRepository),
    代碼有可能就不通用了, 因爲 MongoDB 不支持異步 ToListAsync() 方法。

這裏有 3 種方案:

  • 使用通用異步方法(推薦):

        var queryResult = await AsyncExecuter.ToListAsync(query);
    
  • 不使用異步方法

        var queryResult = query.ToList();
    
  • Appliction 層添加對 程序集 Microsoft.EntityFrameworkCore 引用

        var queryResult = query.ToListAsync();
    

至於使用哪種方案,根據實際需求做取捨。

查詢 GetInput:

    public class GetRawMaterialOutwarehouseRecodsInput : PagedAndSortedResultRequestDto
    {
        public string Filter { get; set; } = string.Empty;
        public string RawMaterialSerialNumber { get; set; } = string.Empty;
        public string RawMaterialName { get; set; } = string.Empty;
        public string OrderSerialNumber { get; set; } = string.Empty;
        public string FinishProductSerialNumber { get; set; } = string.Empty;
        public DateTime? MaxOutwarehouseTime { get; set; }
        public DateTime? MinOutwarehouseTime { get; set; }
        public DateTime? MaxCreationTime { get; set; }
        public DateTime? MinCreationTime { get; set; }
        public DateTime? MaxModifitionTime { get; set; }
        public DateTime? MinModifitionTime { get; set; }
    }

聚合根:
RawMaterialOutwarehouseRecord.CS


public class RawMaterialOutwarehouseRecord : AuditedAggregateRoot<Guid>
{
    [NotNull]
    /// <summary>
    /// 原料Id
    /// </summary>
    public Guid RawMaterialId { get; set; }

    [NotNull]
    /// <summary>
    /// 出庫類型
    /// </summary>
    public RawMaterialOutwarehouseType OutwarehouseType { get; set; }

    [NotNull]
    /// <summary>
    /// 原料出庫數量
    /// </summary>
    public int OutwarehouseCount { get; private set; }

    /// <summary>
    /// 訂單Id
    /// </summary>
    public Guid? OrderId { get; set; }  // 其它聚合根Id, 可null

    /// <summary>
    /// 成品Id
    /// </summary>
    public Guid? FinishProductId { get; set; } // 其它聚合根Id,可null

    /// <summary>
    /// 成品數量
    /// </summary>
    public int? FinishProductCount { get; set; }

    [NotNull]
    /// <summary>
    /// 出庫時間
    /// </summary> 
    public DateTime OutwarehouseTime { get; set; }

    /// <summary>
    /// 備註
    /// </summary>
    public string Comment { get; private set; }

繼承: AuditedAggregateRoot<Guid>

Dto:

  • RawMaterialOutwarehouseRecordDto .CS
    public class RawMaterialOutwarehouseRecordDto : AuditedEntityDto<Guid>
    {
        public Guid RawMaterialId { get; set; }

        public RawMaterialOutwarehouseType OutwarehouseType { get; set; }

        public int OutwarehouseCount { get;  set; }

        public Guid? OrderId { get; set; }
        public Guid? FinishProductId { get; set; }

        public int? FinishProductCount { get; set; }

        public DateTime OutwarehouseTime { get; set; }
      
        public string Comment { get;  set; }
    }

繼承:AuditedEntityDto<Guid>

  • RawMaterialOutwarehouseRecordWithDetialsDto
    public class RawMaterialOutwarehouseRecordWithDetialsDto : RawMaterialOutwarehouseRecordDto 
    {
        public string RawMaterialSerialNumber { get; set; }
        public string RawMaterialName { get; set; }
        public string OrderSerialNumber { get; set; }
        public string FinishProductSerialNumber { get; set; }
    }

繼承:RawMaterialOutwarehouseRecordDto, 然後新增其它聚合根的屬性。

Dto 配置:

        CreateMap<RawMaterialOutwarehouseRecord, RawMaterialOutwarehouseRecordWithDetialsDto>()
            .Ignore(p => p.RawMaterialSerialNumber)
            .Ignore(p => p.RawMaterialName)
            .Ignore(p => p.FinishProductSerialNumber)
            .Ignore(p => p.OrderSerialNumber);

注意:把 Dto: RawMaterialOutwarehouseRecordWithDetialsDto 中聚合根 RawMaterialOutwarehouseRecord 不存在的屬性需要 Ignore(), 否則運行發生異常 。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章