详解EFCore中的导航属性
使用了这么久的EntityFrameworkCore框架,今天想来就其中的一个部分来做一个知识的梳理,从而使自己对于整个知识有一个更加深入的理解,如果你对EFCore中的实体关系不熟悉你需要有一个知识的预热,这样你才能够更好的去理解整个知识,在建立好了这些实体之间的关系以后,我们可以通过使用InClude、ThenInclude这些方法来进行快速获得对应关联实体数据,用起来确实十分的方便,这里我们将通过一系列的例子来进行说明。
1单独使用Include
在介绍这个方法之前,我来先贴出实体之间的关联关系,假设这里有三个相互关联的实体VehicleWarranty、WarrantyWarningLevel、VehicleWarrantyRepairHistory这三个实体后面两个都是第一个的子级并且并且VehicleWarranty、WarrantyWarningLevel之间的关系是1对1的关系,VehicleWarranty和VehicleWarrantyRepairHistory之间是1:N的关系,即1对多的关系,我们这里贴出具体的Model,从而方便后面分析具体的代码。
///
///车辆三包信息(DCSService)
///
publicclassVehicleWarranty:Entity{
publicVehicleWarranty(){
Details=newList();
}
//车辆售后档案
publicGuidVehicleSoldId{get;set;}
//VIN
[Required]
[MaxLength(EntityDefault.FieldLength_50)]
publicstringVin{get;set;}
//产品分类
publicGuid?ProductCategoryId{get;set;}
//产品分类编号
[MaxLength(EntityDefault.FieldLength_50)]
publicstringProductCategoryCode{get;set;}
//产品分类名称
[MaxLength(EntityDefault.FieldLength_100)]
publicstringProductCategoryName{get;set;}
//车牌号
[MaxLength(EntityDefault.FieldLength_50)]
publicstringLicensePlate{get;set;}
//发动机号
[MaxLength(EntityDefault.FieldLength_50)]
publicstringEngineCode{get;set;}
//变速箱号
[MaxLength(EntityDefault.FieldLength_50)]
publicstringTransmissionSn{get;set;}
//开发票日期
publicDateTimeInvoiceDate{get;set;}
//行驶里程
publicintMileage{get;set;}
//是否三包期内
publicbool?IsInWarranty{get;set;}
//预警等级
publicGuid?WarningLevelId{get;set;}
publicWarrantyWarningLevelWarningLevel{get;set;}
//等级编号
[MaxLength(EntityDefault.FieldLength_50)]
publicstringLevelCode{get;set;}
//等级名称
[MaxLength(EntityDefault.FieldLength_100)]
publicstringLevelName{get;set;}
//预警内容
[MaxLength(EntityDefault.FieldLength_800)]
publicstringWarningComment{get;set;}
//累计维修天数
publicintTotoalRepairDays{get;set;}
//售出后60天/3000KM内严重故障次数
publicint?FNum{get;set;}
//严重安全性能故障累计次数
publicint?GNum{get;set;}
//发动机总成累计更换次数
publicint?HNum{get;set;}
//变速箱总成累计更换次数
publicint?INum{get;set;}
//发动机主要零件最大更换次数
publicint?JNum{get;set;}
//变速箱主要零件最大更换次数
publicint?KNum{get;set;}
//同一主要零件最大更换次数
publicint?LNum{get;set;}
//同一产品质量问题最大累计次数(部件+故障+方位)
publicint?MNum{get;set;}
//同一产品质量问题最大累计次数
publicint?NNum{get;set;}
publicListDetails{get;set;}
}
///
///三包预警等级(DCS)
///
publicclassWarrantyWarningLevel:Entity{
//等级编号
[Required]
[MaxLength(EntityDefault.FieldLength_50)]
publicstringCode{get;set;}
//等级名称
[Required]
[MaxLength(EntityDefault.FieldLength_100)]
publicstringName{get;set;}
//颜色
[Required]
[MaxLength(EntityDefault.FieldLength_50)]
publicstringColor{get;set;}
//备注
[MaxLength(EntityDefault.FieldLength_200)]
publicstringRemark{get;set;}
}
///
///车辆三包信息维修履历(DCSService)
///
publicclassVehicleWarrantyRepairHistory:Entity{
//车辆三包信息
[Required]
publicGuidVehicleWarrantyId{get;set;}
publicVehicleWarrantyVehicleWarranty{get;set;}
//VIN
[Required]
[MaxLength(EntityDefault.FieldLength_50)]
publicstringVin{get;set;}
//维修合同
publicGuidRepairContractId{get;set;}
//维修合同编号
[Required]
[MaxLength(EntityDefault.FieldLength_50)]
publicstringRepairContractCode{get;set;}
//处理时间
publicDateTime?DealTime{get;set;}
//经销商
publicGuidDealerId{get;set;}
//经销商编号
[Required]
[MaxLength(EntityDefault.FieldLength_50)]
publicstringDealerCode{get;set;}
//经销商名称
[Required]
[MaxLength(EntityDefault.FieldLength_100)]
publicstringDealerName{get;set;}
//履历来源
publicVehicleWarrantyRepairHistorySourceSource{get;set;}
//累计维修天数
publicint?TotoalRepairDays{get;set;}
//售出后60天/3000KM内严重故障次数
publicint?FNum{get;set;}
//严重安全性能故障累计次数
publicint?GNum{get;set;}
//发动机总成累计更换次数
publicint?HNum{get;set;}
//变速箱总成累计更换次数
publicint?INum{get;set;}
//发动机主要零件最大更换次数
publicint?JNum{get;set;}
//变速箱主要零件最大更换次数
publicint?KNum{get;set;}
//同一主要零件最大更换次数
publicint?LNum{get;set;}
//同一产品质量问题最大累计次数(部件+故障+方位)
publicint?MNum{get;set;}
//同一产品质量问题最大累计次数
publicint?NNum{get;set;}
}
这里我们贴出第一个简单的查询示例,通过Include方法来一下子查询出关联的三包预警等级这个实体,在我们的例子中我们返回的结果是带分页的,而且会根据前端传递的Dto来进行过滤,这里我们来看这段代码怎么实体。
//////查询车辆三包信息 /// ///查询输入 /// 分页请求 /// 带分页的三包预警车辆信息 publicasyncTask>GetVehicleWarrantiesAsync(GetVehicleWarrantiesInputinput,PageRequestpageRequest){ varqueryResults=_vehicleWarrantyRepository.GetAll() .Include(v=>v.WarningLevel) .Where(v=>_vehicleSoldRepository.GetAll().Any(vs=>vs.Status==VehicleStatus.实销完成&&v.Vin==vs.Vin)); vartotalCount=awaitqueryResults.CountAsync(); varpagedResults=awaitqueryResults.ProjectTo (_autoMapper.ConfigurationProvider).PageAndOrderBy(pageRequest).ToListAsync(); returnnewPage (pageRequest,totalCount,pagedResults); }
在这里我们看到了通过一个Include就能够查询出关联的实体,为什么能够实现,那是因为在VehicleWarranty实体中存在WarrantyWarningLevel实体的外键,并且这里还增加了外键关联的实体,这样才能够正确使用InClude方法,并且这个InClude方法只能够以实体作为参数,不能以外键作为参数,到了这里我想提出一个问题,这里最终生成的SQL(SqlServer数据库)是leftjoin还是innerjoin呢?在看完后面的分析之前需要思考一下。
selecttop(20)
[v].[EngineCode],
[v].[GNum],
[v].[Id],
[v.WarningLevel].[Color]as[LevelColor],
[v].[LevelName],
[v].[LicensePlate],
[v].[ProductCategoryName],
[v].[TotoalRepairDays],
[v].[Vin]
from[VehicleWarranty]as[v]
leftjoin[WarrantyWarningLevel]as[v.WarningLevel]on[v].[WarningLevelId]=[v.WarningLevel].[Id]
whereEXISTS(
select1
from[VehicleSold]as[vs]
where([vs].[Status]=7)and([v].[Vin]=[vs].[Vin]))
orderby[v].[Vin]
这里我们看到生成的SQL语句是leftjoin,那么这里为什么不是innerjoin呢?这里先给你看具体的答案吧?这里你看懂了吗?问题就处在我这里建立的外键是可为空的 public Guid? WarningLevelId { get; set; }、如果是不可为空的外键那么生成的SQL就是innerjoin这个你可以亲自尝试。另外有一个需要提醒的就是,如果你像上面的实体中建立了VehicleWarranty、WarrantyWarningLeve之间的关系的话,迁移到数据库会默认生成外键约束,这个在使用的时候需要特别注意,但是如果你只是添加了外键而没有添加对应的外键同名的实体是不会生成外键约束关系的,这个暂时不理解里面的实现机制。
2主清单使用Include
刚才介绍的是1对1的关联关系,那么像VehicleWarranty、VehicleWarrantyRepairHistory之间有明显的主清单关系,即一个VehicleWarranty对应多个VehicleWarrantyRepairHistory的时候使用InClude方法会生成什么样的SQL语句呢?这里我也贴出代码,然后再来分析生成的SQL语句。
//////查询特定的三包预警车辆信息 /// ///特定Id /// 特定的三包预警车辆信息 publicasyncTaskGetVehicleWarrantyWithDetailsAsync(Guidid){ varquery=await_vehicleWarrantyRepository.GetAll() .Include(v=>v.WarningLevel) .Include(v=>v.Details) .SingleOrDefaultAsync(v=>v.Id==id); if(null==query){ thrownewValidationException("找不到当前特定的三包预警车辆信息"); } varretResult=ObjectMapper.Map (query); returnretResult; }
这里使用了两个InClude方法,那么EFCore会怎么生成这个SQL呢?通过查询最终的SQL我们发现EFCore在处理这类问题的时候是分开进行查询,然后再合并到查询的实体中去的,所以在这个查询的过程中生成的SQL如下:
selecttop(2)
[v].[Id],
[v].[EngineCode],
[v].[FNum],
[v].[GNum],
[v].[HNum],
[v].[INum],
[v].[InvoiceDate],
[v].[IsInWarranty],
[v].[JNum],
[v].[KNum],
[v].[LNum],
[v].[LevelCode],
[v].[LevelName],
[v].[LicensePlate],
[v].[MNum],
[v].[Mileage],
[v].[NNum],
[v].[ProductCategoryCode],
[v].[ProductCategoryId],
[v].[ProductCategoryName],
[v].[TotoalRepairDays],
[v].[TransmissionSn],
[v].[VehicleSoldId],
[v].[Vin],
[v].[WarningComment],
[v].[WarningLevelId],
[v.WarningLevel].[Id],
[v.WarningLevel].[Code],
[v.WarningLevel].[Color],
[v.WarningLevel].[Name],
[v.WarningLevel].[Remark]
from[VehicleWarranty]as[v]
leftjoin[WarrantyWarningLevel]as[v.WarningLevel]on[v].[WarningLevelId]=[v.WarningLevel].[Id]
where[v].[Id]=@__id_0
orderby[v].[Id]
select
[v.Details].[Id],
[v.Details].[DealTime],
[v.Details].[DealerCode],
[v.Details].[DealerId],
[v.Details].[DealerName],
[v.Details].[FNum],
[v.Details].[GNum],
[v.Details].[HNum],
[v.Details].[INum],
[v.Details].[JNum],
[v.Details].[KNum],
[v.Details].[LNum],
[v.Details].[MNum],
[v.Details].[NNum],
[v.Details].[RepairContractCode],
[v.Details].[RepairContractId],
[v.Details].[Source],
[v.Details].[TotoalRepairDays],
[v.Details].[VehicleWarrantyId],
[v.Details].[Vin]
from[VehicleWarrantyRepairHistory]as[v.Details]
innerjoin(
selectdistinct[t].*
from(
selecttop(1)[v0].[Id]
from[VehicleWarranty]as[v0]
leftjoin[WarrantyWarningLevel]as[v.WarningLevel0]
on[v0].[WarningLevelId]=[v.WarningLevel0].[Id]
where[v0].[Id]=@__id_0
orderby[v0].[Id]
)as[t]
)as[t0]on[v.Details].[VehicleWarrantyId]=[t0].[Id]
orderby[t0].[Id]
这个在查询的过程中会分作几个SQL查询并且会将前面查询的结果作为后面查询的部分条件来进行了,待整个查询完毕后再在内存中将这些结果组合到一个query对象中。
3ThenInclude用法
上面的介绍完了之后,你应该能够明白这个Include的具体含义和用法了,接着上面的例子,如果WarrantyWarningLevel里面还有通过外键Id去关联别的实体,这个时候ThenInclude就派上了用场了,理论上只要彼此之间建立了这种外键关系就可以一直ThenInClude下去,但是一般情况下不会用到这么复杂的情况,当然这里面每一个Include也都是作为一个单独的查询来进行的,这个也可以找具体的例子进行试验,这里也贴出一个具体的例子吧。
publicasyncTaskGetById(Guidid){ varrepairContract=await_repairContractRepository.GetAll() .Include(d=>d.RepairContractWorkItems) .ThenInclude(w=>w.Materials) .FirstOrDefaultAsync(r=>r.Id==id); if(repairContract==null) thrownewValidationException(_localizer["当前维修合同不存在"]); varvehicleSold=_vehicleSoldRepository.Get(repairContract.VehicleId); varisTrafficSubsidy=_repairContractManager.IsTrafficSubsidy(repairContract.Id); var(nextMaintenanceMileage,nextMaintenanceTime)=_repairContractManager.GetNextMaintainInfo(repairContract,vehicleSold); varresult=newGetRepairContractDetailForSettlementOutput(){ Id=repairContract.Id, Code=repairContract.Code, CustomerName=repairContract.CustomerName, CellPhoneNumber=repairContract.CellPhoneNumber, Vin=repairContract.Vin, LicensePlate=repairContract.LicensePlate, NextMaintenanceTime=nextMaintenanceTime, NextMaintenanceMileage=nextMaintenanceMileage, LaborFee=repairContract.LaborFee, LaborFeeAfter=repairContract.LaborFeeAfter, MaterialFee=repairContract.MaterialFee, MaterialFeeAfter=repairContract.MaterialFeeAfter, OutFee=repairContract.OutFee, OtherFee=repairContract.OtherFee, TotalFeeAfter=repairContract.TotalFeeAfter, ShowIsTrafficSubsidy=isTrafficSubsidy, LastMaintenanceTime=vehicleSold.LastMaintenanceTime, LastMaintenanceMileage=vehicleSold.LastMaintenanceMileage, WorkItems=_mapper.Map >(repairContract.RepairContractWorkItems) }; returnresult; }
最后还要介绍一种极特殊的情况,由于ThenInclude方法只能一层层向下进行,如果我想对同一个实体里面的两个关联实体做ThenInclude操作这个怎么处理,这里就直接给出代码吧。
//////维修合同完成的事件 /// ///publicvoidFinished(GuidrepairContractId){ varrepairContract=_repairContractRepository.GetAll() .Include(c=>c.RepairContractWorkItems).ThenInclude(wi=>wi.Materials) .Include(c=>c.RepairContractWorkItems).ThenInclude(wi=>wi.Fault) .SingleOrDefault(c=>c.Id==repairContractId); varrepairContractAdjusts=_repairContractAdjustRepository.GetAll() .Include(a=>a.WorkItems).ThenInclude(w=>w.Materials) .Where(a=>a.RepairContractId==repairContractId).ToList(); var@event=newAddRepairContractEvent{ Key=repairContract?.Code, RepairContract=repairContract, RepairContractAdjusts=repairContractAdjusts }; _producer.Produce(@event); }
这里需要Include同一个实体两次,然后分别调用ThenInclude方法,这个属于比较特殊的情况,在使用的时候需要注意。
温馨提示:
这里读者在看代码的时候可能不太理解类似这种 _repairContractRepository的具体由来,这里贴出一份完整的代码。
internalclassAddRepairContractEventManager:DomainService,IAddRepairContractEventManager{
privatereadonlyKafkaProducer_producer;
privatereadonlyIRepository_repairContractRepository;
privatereadonlyIRepository_repairContractAdjustRepository;
publicAddRepairContractEventManager(KafkaProducerproducer,
IRepositoryrepairContractRepository,
IRepositoryrepairContractAdjustRepository){
_producer=producer;
_repairContractRepository=repairContractRepository;
_repairContractAdjustRepository=repairContractAdjustRepository;
}
///
///维修合同完成的事件
///
///
publicvoidFinished(GuidrepairContractId){
varrepairContract=_repairContractRepository.GetAll()
.Include(c=>c.RepairContractWorkItems).ThenInclude(wi=>wi.Materials)
.Include(c=>c.RepairContractWorkItems).ThenInclude(wi=>wi.Fault)
.SingleOrDefault(c=>c.Id==repairContractId);
varrepairContractAdjusts=_repairContractAdjustRepository.GetAll()
.Include(a=>a.WorkItems).ThenInclude(w=>w.Materials)
.Where(a=>a.RepairContractId==repairContractId).ToList();
var@event=newAddRepairContractEvent{
Key=repairContract?.Code,
RepairContract=repairContract,
RepairContractAdjusts=repairContractAdjusts
};
_producer.Produce(@event);
}
}
4IncludeFilter用法
在有些场景中我们可能需要带出清单的时候并且过滤清单,这个功能算是对Include方法的一个提升,可以将两个操作合并到一起来进行,这个在使用的时候需要注意这个并不是Asp.NetCore自带的功能,这个需要通过引入包Z.EntityFramework.Plus.EFCore.dll的包来实现的,如果你们的系统中使用的是ABP作为项目主框架,那么你只需要引用Abp.EntityFrameworkCore.EFPlus这个包就可以了,因为这个包中就包含和Z.EntityFramework相关的子包,这个在使用的时候需要注意。
下面我们来看一看我们的代码中是怎么使用的。
private(Companycompany,IEnumerablepartSaleOrderTypes)GetCompanyDetailForFactory(Guidid){ varcurrentPartSaleOrderTypes=GetCurrentPartSaleOrderTypes(); varcurrentPartSaleOrderTypeIds=currentPartSaleOrderTypes.Select(t=>t.Id); varcompany=_companyRepository.GetAll() .IncludeFilter(c=>c.CustomerPartInformations.Where(i=>i.BranchId==SdtSession.TenantId.GetValueOrDefault())) .IncludeFilter(c=>c.CustomerOrderWarehouses.Where(w=>currentPartSaleOrderTypeIds.Contains(w.PartSaleOrderTypeId))) .IncludeFilter(c=>c.CustomerMarkupRates.Where(w=>currentPartSaleOrderTypeIds.Contains(w.PartSaleOrderTypeId))) .IncludeFilter(c=>c.OrderShippingSequences.Where(w=>currentPartSaleOrderTypeIds.Contains(w.PartSaleOrderTypeId))) .IncludeFilter(c=>c.OrderingCalendars.Where(w=>currentPartSaleOrderTypeIds.Contains(w.PartSaleOrderTypeId))) .FirstOrDefault(d=>d.Id==id&&d.Status==BaseDataStatus.生效); if(company==null){ thrownewEntityNotFoundException(SharedLocalizer["未能找到对应的企业"]); } return(company,currentPartSaleOrderTypes); }
这个提供了一种新的清单过滤方式,不仅提高了效率而且使代码更加优化简练。
5特殊情况
这里还介绍一种不通过Include方法来获取清单中的方式,就像下面这种写法,Company对象和OrderingCalendars之间建立了一对多的导航属性,我们在使用 company.OrderingCalendars之前先将内部的清查查询出来(不需要定义变量接收,使用_即可),这样也是能通过导航属性自动映射到company的OrderingCalendars中去的,这个在使用的时候需要特别注意,查询的时候要提前ToList,将数据查询到内存里面
_=_orderingCalendarRepository.GetAll().Where(t=>t.OrderingCompanyId==company.Id).ToList();
vartempCalendars=company.OrderingCalendars.OrderBy(d=>d.PartSaleOrderType.Level).Select(d=>new{
d.PartSaleOrderType.BrandId,
d.PartSaleOrderType.BrandCode,
d.PartSaleOrderType.BrandName,
d.PartSaleOrderTypeId,
d.PartSaleOrderTypeCode,
d.PartSaleOrderTypeName,
d.Year,
d.Month,
d.Day
});
以上就是详解EFCore中的导航属性的详细内容,更多关于EFCore导航属性的资料请关注毛票票其它相关文章!
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:czq8825#qq.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。