EFCore中的导航属性
使用了这么久的EntityFrameworkCore框架,今天想来就其中的一个部分来做一个知识的梳理,从而使自己对于整个知识有一个更加深入的理解,如果你对EFCore中的实体关系不熟悉你需要有一个知识的预热,这样你才能够更好的去理解整个知识,在建立好了这些实体之间的关系以后,我们可以通过使用InClude、ThenInclude这些方法来进行快速获得对应关联实体数据,用起来确实十分的方便,这里我们将通过一系列的例子来进行说明。
1 单独使用Include
在介绍这个方法之前,我来先贴出实体之间的关联关系,假设这里有三个相互关联的实体VehicleWarranty、WarrantyWarningLevel、VehicleWarrantyRepairHistory这三个实体后面两个都是第一个的子级并且并且VehicleWarranty、WarrantyWarningLevel之间的关系是1对1的关系,VehicleWarranty和VehicleWarrantyRepairHistory之间是1:N的关系,即1对多的关系,我们这里贴出具体的Model,从而方便后面分析具体的代码。
/// <summary>
/// 车辆三包信息(DCSService)
/// </summary>
public class VehicleWarranty : Entity<Guid> {
public VehicleWarranty() {
Details = new List<VehicleWarrantyRepairHistory>();
} //车辆售后档案
public Guid VehicleSoldId { get; set; } //VIN
[Required]
[MaxLength(EntityDefault.FieldLength_50)]
public string Vin { get; set; } //产品分类
public Guid? ProductCategoryId { get; set; } //产品分类编号
[MaxLength(EntityDefault.FieldLength_50)]
public string ProductCategoryCode { get; set; } //产品分类名称
[MaxLength(EntityDefault.FieldLength_100)]
public string ProductCategoryName { get; set; } //车牌号
[MaxLength(EntityDefault.FieldLength_50)]
public string LicensePlate { get; set; } //发动机号
[MaxLength(EntityDefault.FieldLength_50)]
public string EngineCode { get; set; } //变速箱号
[MaxLength(EntityDefault.FieldLength_50)]
public string TransmissionSn { get; set; } //开发票日期
public DateTime InvoiceDate { get; set; } //行驶里程
public int Mileage { get; set; } //是否三包期内
public bool? IsInWarranty { get; set; } //预警等级
public Guid? WarningLevelId { get; set; } public WarrantyWarningLevel WarningLevel { get; set; } //等级编号
[MaxLength(EntityDefault.FieldLength_50)]
public string LevelCode { get; set; } //等级名称
[MaxLength(EntityDefault.FieldLength_100)]
public string LevelName { get; set; } //预警内容
[MaxLength(EntityDefault.FieldLength_800)]
public string WarningComment { get; set; } //累计维修天数
public int TotoalRepairDays { get; set; } //售出后60天/3000KM内严重故障次数
public int? FNum { get; set; } //严重安全性能故障累计次数
public int? GNum { get; set; } //发动机总成累计更换次数
public int? HNum { get; set; } //变速箱总成累计更换次数
public int? INum { get; set; } //发动机主要零件最大更换次数
public int? JNum { get; set; } //变速箱主要零件最大更换次数
public int? KNum { get; set; } //同一主要零件最大更换次数
public int? LNum { get; set; } //同一产品质量问题最大累计次数(部件+故障+方位)
public int? MNum { get; set; } //同一产品质量问题最大累计次数
public int? NNum { get; set; } public List<VehicleWarrantyRepairHistory> Details { get; set; }
} /// <summary>
/// 三包预警等级(DCS)
/// </summary>
public class WarrantyWarningLevel : Entity<Guid> {
//等级编号
[Required]
[MaxLength(EntityDefault.FieldLength_50)]
public string Code { get; set; } //等级名称
[Required]
[MaxLength(EntityDefault.FieldLength_100)]
public string Name { get; set; } //颜色
[Required]
[MaxLength(EntityDefault.FieldLength_50)]
public string Color { get; set; } //备注
[MaxLength(EntityDefault.FieldLength_200)]
public string Remark { get; set; }
} /// <summary>
/// 车辆三包信息维修履历(DCSService)
/// </summary>
public class VehicleWarrantyRepairHistory : Entity<Guid> {
//车辆三包信息
[Required]
public Guid VehicleWarrantyId { get; set; } public VehicleWarranty VehicleWarranty { get; set; } //VIN
[Required]
[MaxLength(EntityDefault.FieldLength_50)]
public string Vin { get; set; } //维修合同
public Guid RepairContractId { get; set; } //维修合同编号
[Required]
[MaxLength(EntityDefault.FieldLength_50)]
public string RepairContractCode { get; set; } //处理时间
public DateTime? DealTime { get; set; } //经销商
public Guid DealerId { get; set; } //经销商编号
[Required]
[MaxLength(EntityDefault.FieldLength_50)]
public string DealerCode { get; set; } //经销商名称
[Required]
[MaxLength(EntityDefault.FieldLength_100)]
public string DealerName { get; set; } //履历来源
public VehicleWarrantyRepairHistorySource Source { get; set; } //累计维修天数
public int? TotoalRepairDays { get; set; } //售出后60天/3000KM内严重故障次数
public int? FNum { get; set; } //严重安全性能故障累计次数
public int? GNum { get; set; } //发动机总成累计更换次数
public int? HNum { get; set; } //变速箱总成累计更换次数
public int? INum { get; set; } //发动机主要零件最大更换次数
public int? JNum { get; set; } //变速箱主要零件最大更换次数
public int? KNum { get; set; } //同一主要零件最大更换次数
public int? LNum { get; set; } //同一产品质量问题最大累计次数(部件+故障+方位)
public int? MNum { get; set; } //同一产品质量问题最大累计次数
public int? NNum { get; set; }
}
这里我们贴出第一个简单的查询示例,通过Include方法来一下子查询出关联的三包预警等级这个实体,在我们的例子中我们返回的结果是带分页的,而且会根据前端传递的Dto来进行过滤,这里我们来看这段代码怎么实体。
/// <summary>
/// 查询车辆三包信息
/// </summary>
/// <param name="input">查询输入</param>
/// <param name="pageRequest">分页请求</param>
/// <returns>带分页的三包预警车辆信息</returns>
public async Task<Page<GetVehicleWarrantiesOutput>> GetVehicleWarrantiesAsync(GetVehicleWarrantiesInput input, PageRequest pageRequest) {
var queryResults = _vehicleWarrantyRepository.GetAll()
.Include(v => v.WarningLevel)
.Where(v => _vehicleSoldRepository.GetAll().Any(vs => vs.Status == VehicleStatus.实销完成 && v.Vin == vs.Vin));
var totalCount = await queryResults.CountAsync();
var pagedResults = await queryResults.ProjectTo<GetVehicleWarrantiesOutput>(_autoMapper.ConfigurationProvider).PageAndOrderBy(pageRequest).ToListAsync();
return new Page<GetVehicleWarrantiesOutput>(pageRequest, totalCount, pagedResults);
}
在这里我们看到了通过一个Include就能够查询出关联的实体,为什么能够实现,那是因为在VehicleWarranty实体中存在WarrantyWarningLevel实体的外键,并且这里还增加了外键关联的实体,这样才能够正确使用InClude方法,并且这个InClude方法只能够以实体作为参数,不能以外键作为参数,到了这里我想提出一个问题,这里最终生成的SQL(SqlServer数据库)是left join 还是inner join呢?在看完后面的分析之前需要思考一下。
select top (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]
left join [WarrantyWarningLevel] as [v.WarningLevel] on [v].[WarningLevelId] = [v.WarningLevel].[Id]
where EXISTS(
select 1
from [VehicleSold] as [vs]
where ([vs].[Status] = 7) and ([v].[Vin] = [vs].[Vin]))
order by [v].[Vin]
这里我们看到生成的SQL语句是left join ,那么这里为什么不是inner join呢?这里先给你看具体的答案吧?这里你看懂了吗?问题就处在我这里建立的外键是可为空的 public Guid? WarningLevelId { get; set; }、如果是不可为空的外键那么生成的SQL就是inner join这个你可以亲自尝试。另外有一个需要提醒的就是,如果你像上面的实体中建立了VehicleWarranty、WarrantyWarningLeve之间的关系的话,迁移到数据库会默认生成外键约束,这个在使用的时候需要特别注意,但是如果你只是添加了外键而没有添加对应的外键同名的实体是不会生成外键约束关系的,这个暂时不理解里面的实现机制。
2 主清单使用Include
刚才介绍的是1对1的关联关系,那么像VehicleWarranty、VehicleWarrantyRepairHistory之间有明显的主清单关系,即一个VehicleWarranty对应多个VehicleWarrantyRepairHistory的时候使用InClude方法会生成什么样的SQL语句呢?这里我也贴出代码,然后再来分析生成的SQL语句。
/// <summary>
/// 查询特定的三包预警车辆信息
/// </summary>
/// <param name="id">特定Id</param>
/// <returns>特定的三包预警车辆信息</returns>
public async Task<GetVehicleWarrantyWithDetailsOutput> GetVehicleWarrantyWithDetailsAsync(Guid id) {
var query = await _vehicleWarrantyRepository.GetAll()
.Include(v => v.WarningLevel)
.Include(v => v.Details)
.SingleOrDefaultAsync(v => v.Id == id);
if (null == query) {
throw new ValidationException("找不到当前特定的三包预警车辆信息");
} var retResult = ObjectMapper.Map<GetVehicleWarrantyWithDetailsOutput>(query);
return retResult;
}
这里使用了两个InClude方法,那么EFCore会怎么生成这个SQL呢?通过查询最终的SQL我们发现EFCore在处理这类问题的时候是分开进行查询,然后再合并到查询的实体中去的,所以在这个查询的过程中生成的SQL如下:
select top (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]
left join [WarrantyWarningLevel] as [v.WarningLevel] on [v].[WarningLevelId] = [v.WarningLevel].[Id]
where [v].[Id] = @__id_0
order by [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]
inner join (
select distinct [t].*
from (
select top (1) [v0].[Id]
from [VehicleWarranty] as [v0]
left join [WarrantyWarningLevel] as [v.WarningLevel0]
on [v0].[WarningLevelId] = [v.WarningLevel0].[Id]
where [v0].[Id] = @__id_0
order by [v0].[Id]
) as [t]
) as [t0] on [v.Details].[VehicleWarrantyId] = [t0].[Id]
order by [t0].[Id]
这个在查询的过程中会分作几个SQL查询并且会将前面查询的结果作为后面查询的部分条件来进行了,待整个查询完毕后再在内存中将这些结果组合到一个query对象中。
3 ThenInclude用法
上面的介绍完了之后,你应该能够明白这个Include的具体含义和用法了,接着上面的例子,如果WarrantyWarningLevel里面还有通过外键Id去关联别的实体,这个时候ThenInclude就派上了用场了,理论上只要彼此之间建立了这种外键关系就可以一直ThenInClude下去,但是一般情况下不会用到这么复杂的情况,当然这里面每一个Include也都是作为一个单独的查询来进行的,这个也可以找具体的例子进行试验,这里也贴出一个具体的例子吧。
public async Task<GetRepairContractDetailForSettlementOutput> GetById(Guid id) {
var repairContract = await _repairContractRepository.GetAll()
.Include(d => d.RepairContractWorkItems)
.ThenInclude(w => w.Materials)
.FirstOrDefaultAsync(r => r.Id == id);
if (repairContract == null)
throw new ValidationException(_localizer["当前维修合同不存在"]);
var vehicleSold = _vehicleSoldRepository.Get(repairContract.VehicleId);
var isTrafficSubsidy = _repairContractManager.IsTrafficSubsidy(repairContract.Id);
var (nextMaintenanceMileage, nextMaintenanceTime) = _repairContractManager.GetNextMaintainInfo(repairContract, vehicleSold);
var result = new GetRepairContractDetailForSettlementOutput() {
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<IList<GetRepairContractWorkItemForSettlementOutput>>(repairContract.RepairContractWorkItems)
};
return result;
}
最后还要介绍一种极特殊的情况,由于ThenInclude方法只能一层层向下进行,如果我想对同一个实体里面的两个关联实体做ThenInclude操作这个怎么处理,这里就直接给出代码吧。
/// <summary>
///维修合同完成的事件
/// </summary>
/// <param name="repairContractId"></param>
public void Finished(Guid repairContractId) {
var repairContract = _repairContractRepository.GetAll()
.Include(c => c.RepairContractWorkItems).ThenInclude(wi => wi.Materials)
.Include(c => c.RepairContractWorkItems).ThenInclude(wi => wi.Fault)
.SingleOrDefault(c => c.Id == repairContractId);
var repairContractAdjusts = _repairContractAdjustRepository.GetAll()
.Include(a => a.WorkItems).ThenInclude(w => w.Materials)
.Where(a => a.RepairContractId == repairContractId).ToList(); var @event = new AddRepairContractEvent {
Key = repairContract?.Code,
RepairContract = repairContract,
RepairContractAdjusts = repairContractAdjusts
};
_producer.Produce(@event);
}
这里需要Include同一个实体两次,然后分别调用ThenInclude方法,这个属于比较特殊的情况,在使用的时候需要注意。
温馨提示:
这里读者在看代码的时候可能不太理解类似这种 _repairContractRepository的具体由来,这里贴出一份完整的代码。
internal class AddRepairContractEventManager : DomainService, IAddRepairContractEventManager {
private readonly KafkaProducer _producer;
private readonly IRepository<RepairContract, Guid> _repairContractRepository;
private readonly IRepository<RepairContractAdjust, Guid> _repairContractAdjustRepository;
public AddRepairContractEventManager(KafkaProducer producer,
IRepository<RepairContract, Guid> repairContractRepository,
IRepository<RepairContractAdjust, Guid> repairContractAdjustRepository) {
_producer = producer;
_repairContractRepository = repairContractRepository;
_repairContractAdjustRepository = repairContractAdjustRepository;
}
/// <summary>
///维修合同完成的事件
/// </summary>
/// <param name="repairContractId"></param>
public void Finished(Guid repairContractId) {
var repairContract = _repairContractRepository.GetAll()
.Include(c => c.RepairContractWorkItems).ThenInclude(wi => wi.Materials)
.Include(c => c.RepairContractWorkItems).ThenInclude(wi => wi.Fault)
.SingleOrDefault(c => c.Id == repairContractId);
var repairContractAdjusts = _repairContractAdjustRepository.GetAll()
.Include(a => a.WorkItems).ThenInclude(w => w.Materials)
.Where(a => a.RepairContractId == repairContractId).ToList();
var @event = new AddRepairContractEvent {
Key = repairContract?.Code,
RepairContract = repairContract,
RepairContractAdjusts = repairContractAdjusts
};
_producer.Produce(@event);
}
}
EFCore中的导航属性的更多相关文章
- 在实体对象中访问导航属性里的属性值出现异常“There is already an open DataReader associated with this Command which must be closed first”
在实体对象中访问导航属性里的属性值出现异常“There is already an open DataReader associated with this Command which must be ...
- EntityFramework 6.x和EntityFramework Core关系映射中导航属性必须是public?
前言 不知我们是否思考过一个问题,在关系映射中对于导航属性的访问修饰符是否一定必须为public呢?如果从未想过这个问题,那么我们接下来来探讨这个问题. EF 6.x和EF Core 何种情况下必须配 ...
- 你所不知道的库存超限做法 服务器一般达到多少qps比较好[转] JAVA格物致知基础篇:你所不知道的返回码 深入了解EntityFramework Core 2.1延迟加载(Lazy Loading) EntityFramework 6.x和EntityFramework Core关系映射中导航属性必须是public? 藏在正则表达式里的陷阱 两道面试题,带你解析Java类加载机制
你所不知道的库存超限做法 在互联网企业中,限购的做法,多种多样,有的别出心裁,有的因循守旧,但是种种做法皆想达到的目的,无外乎几种,商品卖的完,系统抗的住,库存不超限.虽然短短数语,却有着说不完,道不 ...
- .NET ORM 导航属性【到底】可以解决什么问题?
写在开头 从最早期入门时的单表操作, 到后来接触了 left join.right join.inner join 查询, 因为经费有限,需要不断在多表查询中折腾解决实际需求,不知道是否有过这样的经历 ...
- 《Entity Framework 6 Recipes》中文翻译系列 (22) -----第五章 加载实体和导航属性之延迟加载
翻译的初衷以及为什么选择<Entity Framework 6 Recipes>来学习,请看本系列开篇 第五章 加载实体和导航属性 实体框架提供了非常棒的建模环境,它允许开发人员可视化地使 ...
- 《Entity Framework 6 Recipes》中文翻译系列 (23) -----第五章 加载实体和导航属性之预先加载与Find()方法
翻译的初衷以及为什么选择<Entity Framework 6 Recipes>来学习,请看本系列开篇 5-2 预先加载关联实体 问题 你想在一次数据交互中加载一个实体和与它相关联实体. ...
- 《Entity Framework 6 Recipes》中文翻译系列 (26) ------ 第五章 加载实体和导航属性之延缓加载关联实体和在别的LINQ查询操作中使用Include()方法
翻译的初衷以及为什么选择<Entity Framework 6 Recipes>来学习,请看本系列开篇 5-7 在别的LINQ查询操作中使用Include()方法 问题 你有一个LINQ ...
- EFCode First 导航属性
首先谈谈自己对EF的接触的过程吧,最先接触EF只是因为EF支持从数据库把关系扒下来,可以省掉自己写Select.Update.Insert这些SQL语句,而且修改非常方便,后来在使用的过程中发现导航属 ...
- EF Code First 导航属性 与外键
一对多关系 项目中最常用到的就是一对多关系了.Code First对一对多关系也有着很好的支持.很多情况下我们都不需要特意的去配置,Code First就能通过一些引用属性.导航属性等检测到模型之间的 ...
随机推荐
- Python安装第三方库常用方法
在学习Python过程中,经常要用到很多第三方库,面对各种不同情况,Python为我们提供了多种安装方法: 一.pip安装: pip安装相信大家都不陌生了,在安装第三方库中,pip是最常使用的一种方法 ...
- 如何在团队中做好Code Review
一.Code Review的好处 想要做好Code Review,必须让参与的工程师充分认识到Code Review的好处 1.互相学习,彼此成就 无论是高手云集的架构师团队,还是以CURD为主的业务 ...
- C# ffmpeg 视频处理格式转换和添加水印
通过C#调用ffmpeg 将flv格式转换为mp4格式,并添加水印 C#调用ffmpeg的方法封装如下: /// <summary>/// 视频处理器ffmpeg.exe的位置/// &l ...
- zookeeper/kafka的部署
Ubuntu中安装zookeeper及kafka并配置环境变量 首先安装zookeeper zookeeper需要jdk环境,请在jdk安装完成的情况下安装zookeeper1.从官网下载zook ...
- JS如何判断文字是全角还是半角
载自:http://www.php.cn/js-tutorial-362638.html 全角:是一种电脑字符,是指一个全角字符占用两个标准字符(或两个半角字符)的位置.全角占两个字节.半角:是指一个 ...
- Could not attach to pid : "xx"最近启动Xcode运行项目都会出现这个问题,再次启动或者多启动几次,就可以正常运行工程了。
最近启动Xcode运行项目都会出现这个问题,再次启动或者多启动几次,就可以正常运行工程了. 普及一下:PID(进程控制符)英文全称为Process Identifier,它也属于电工电子类技术术语. ...
- npm配置淘宝镜像
npm直接安装包太慢,采用淘宝npm镜像安装 在linux和Mac上可以添加环境变量的形式修改bashrc文件,但是在windows上可以直接采取如下方式,以绝后患. 永久采用 npm config ...
- 【转载】 【Tensorflow】卷积神经网络中strides的参数
原文地址: https://blog.csdn.net/TwT520Ly/article/details/79540251 http://blog.csdn.net/TwT520Ly -------- ...
- 基于ZooKeeper实现简单的服务注册于发现
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明.本文链接:https://blog.csdn.net/Shaun_luotao/article/ ...
- 【问题解决】Flasgger mapping values are not allowed here?
参考来源:https://stackoverflow.com/questions/9055371/python-and-pyaml-yaml-scanner-scannererror-mapping- ...