背景
領域驅動設計(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
集合結果中包含:
- 聚合根
RawMaterial
的SerialNumber
、Name
- 聚合根
Order
的SerialNumber
- 聚合根
FinishProduct
的SerialNumber
有問題查詢實現
先看下面的查詢實現:
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
, 那爲了獲取聚合根 Order
、FinishProduct
中的屬性,
就需要額外執行 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
);
}
......
}
代碼說明:
-
使用
xxxRepository.GetQueryableAsync()
返回聚合根的IQueryable<T>
-
關於
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()
, 否則運行發生異常 。