EFCore通过Include关联清单不存在时返回值为默认值的方式

背景

  最近在使用EFCore2.1写代码的时候遇到一个问题,在最终的查询结果中有一个SinglePrice字段查询的结果总是不符合预期,按照自己的设想这个字段是主单的一个TotalFeeAfter字段和主单关联的清单其中的CouponFee合计这两者之间的差值,当然主单关联的清单可能不存在,所以当清单不存在时清单的CouponFee合计值为0,可是在使用EFCore写的代码中结果返回的总是null,显然这个不符合预期,为了交代好这一个过程,我们来看一下这个主清单BO,从而便于自己有一个主观的认知。

  1.1 主单保养套餐销售订单(MaintenancePackageOrder)

/// <summary>
    /// 保养套餐销售订单
    /// </summary>
    [Table("MaintenancePackageOrder")]
    public class MaintenancePackageOrder : RowVersionAuditEntity, IMustHaveCode {
        public MaintenancePackageOrder() {
            Coupons = new List<MaintenancePackageOrderCoupon>();
        }
        /// <summary>
        ///订单编号
        /// </summary>
        [Required]
        [MaxLength(EntityDefault.FieldLength_50)]
        public string Code { get; set; }
        /// <summary>
        ///营销分公司编号
        /// </summary>
        [Required]
        [MaxLength(EntityDefault.FieldLength_50)]
        public string BranchCode { get; set; }
        /// <summary>
        ///品牌编号
        /// </summary>
        [Required]
        [MaxLength(EntityDefault.FieldLength_50)]
        public string BrandCode { get; set; }
        /// <summary>
        ///仓库编号
        /// </summary>
        [Required]
        [MaxLength(EntityDefault.FieldLength_50)]
        public string WarehouseCode { get; set; }
        /// <summary>
        ///仓库名称
        /// </summary>
        [Required]
        [MaxLength(EntityDefault.FieldLength_100)]
        public string WarehouseName { get; set; }
        /// <summary>
        ///车辆售后档案
        /// </summary>
        public Guid VehicleSoldId { get; set; }
        /// <summary>
        ///VIN
        /// </summary>
        [Required]
        [MaxLength(EntityDefault.FieldLength_50)]
        public string Vin { get; set; }
        /// <summary>
        ///产品编号
        /// </summary>
        [MaxLength(EntityDefault.FieldLength_50)]
        public string ProductCode { get; set; }
        /// <summary>
        ///车牌号
        /// </summary>
        [MaxLength(EntityDefault.FieldLength_50)]
        public string LicensePlate { get; set; }
        /// <summary>
        ///行驶里程
        /// </summary>
        public int? Mileage { get; set; }
        /// <summary>
        ///优惠后总金额
        /// </summary>
        public decimal TotalFeeAfter { get; set; }
        /// <summary>
        ///状态
        /// </summary>
        public MaintenancePackageOrderStatus Status { get; set; }
        /// <summary>
        ///备注
        /// </summary>
        [MaxLength(EntityDefault.FieldLength_200)]
        public string Remark { get; set; }
        /// <summary>
        /// 产品分类id
        /// </summary>
        [MaxLength(EntityDefault.FieldLength_50)]
        public string ProductCategoryCode { get; set; }
        /// <summary>
        /// 销售顾问
        /// </summary>
        [MaxLength(EntityDefault.FieldLength_100)]
        public string SaleAdviserName { get; set; }
        /// <summary>
        /// 保养套餐销售订单抵用券清单(DMSPart)
        /// </summary>
        public List<MaintenancePackageOrderCoupon> Coupons { get; set; }
    }

  1.2 清单保养套餐抵用券清单(MaintenancePackageOrderCoupon)

/// <summary>
    /// 保养套餐销售订单抵用券清单(DMSPart)
    /// </summary>
    public class MaintenancePackageOrderCoupon : Entity<Guid> {
        /// <summary>
        /// 保养套餐销售订单
        /// </summary>
        [ForeignKeyReference(DeleteBehavior = DeleteBehavior.Cascade)]
        public Guid MaintenancePackageOrderId { get; set; }
        /// <summary>
        /// 代金券
        /// </summary>
        public Guid? CouponId { get; set; }
        /// <summary>
        /// 抵用券编号
        /// </summary>
        [MaxLength(EntityDefault.FieldLength_100)]
        public string CouponCode { get; set; }
        /// <summary>
        /// 抵用券面值
        /// </summary>
        public decimal? CouponPrice { get; set; }
        /// <summary>
        /// 抵用金额
        /// </summary>
        public decimal? CouponFee { get; set; }
    }

  这里只是为了解释主单MaintenancePackageOrder和清单MaintenancePackageOrderCoupon一对多的关系,所以这里只截取了部分的字段名称,所以可能会和后面的SQL中的部分字段对不上,这里做一个说明。

  1.3 EFCore查询数据

        public (bool, IQueryable<GetPackageTicketUsageDetailedOutput>) QueryDetailed(GetPackageTicketUsageInput input) {
            var isDealer = _companyService.IsDealerUser();
            var maintenancePackageOrders = _maintenancePackageOrderRepository.GetAll()
                 .Include(p => p.Coupons)
                 .WhereIf(isDealer, p => p.DealerId == SdtSession.TenantId)
                 .WhereIf(input.BrandId.HasValue, p => p.BrandId == input.BrandId)
                 .WhereIf(input.BeginSettlementTime.HasValue, p => p.SettlementTime >= input.BeginSettlementTime)
                 .WhereIf(input.EndSettlementTime.HasValue, p => p.SettlementTime <= input.EndSettlementTime);
            var partPrices = _partPriceRepository.GetAll()
                .WhereIf(input.MinWholeSalePrice.HasValue, p => p.WholeSalePrice >= input.MinWholeSalePrice)
                .WhereIf(input.MaxWholeSalePrice.HasValue, p => p.WholeSalePrice <= input.MaxWholeSalePrice);
            var marketingDepartmentDetails = _marketingDepartmentDetailRepository.GetAll()
                .WhereIf(input.MarketingDepartmentIds.Any(), m => input.MarketingDepartmentIds.Contains(m.MarketingDepartmentId));
            var details = _maintenancePackageOrderTicketDetailRepository.GetAll()
                .WhereIf(!string.IsNullOrEmpty(input.PackageTicketCode), p => p.PackageTicketCode.Contains(input.PackageTicketCode))
                .WhereIf(input.BeginUsedTime.HasValue, p => p.PackageTicketUsedTime >= input.BeginUsedTime)
                .WhereIf(input.EndUsedTime.HasValue, p => p.PackageTicketUsedTime <= input.EndUsedTime);
            var result = from maintenancePackageOrder in maintenancePackageOrders
                         join partPrice in partPrices on maintenancePackageOrder.MaintenancePackageOrderDetail.PartId equals partPrice.PartId
                         join marketingDepartmentDetail in marketingDepartmentDetails on maintenancePackageOrder.DealerId equals marketingDepartmentDetail.DealerId
                         join marketingDepartment in _marketingDepartmentRepository.GetAll() on marketingDepartmentDetail.MarketingDepartmentId equals marketingDepartment.Id
                         join detail in details on maintenancePackageOrder.Id equals detail.MaintenancePackageOrderId
                         join partPackage in _partPackageRepository.GetAll() on maintenancePackageOrder.MaintenancePackageOrderDetail.PartId equals partPackage.PackagePartId into pp
                         from partPackage in pp.DefaultIfEmpty()
                         join maintenancePackageReturnOrder in _maintenancePackageReturnOrderRepository.GetAll()
                                 .Where(p => p.Status == MaintenancePackageReturnOrderStatus.审核通过 || p.Status == MaintenancePackageReturnOrderStatus.已结算)
                             on maintenancePackageOrder.Id equals maintenancePackageReturnOrder.MaintenancePackageOrderId into mpro
                         from maintenancePackageReturnOrder in mpro.DefaultIfEmpty()
                         select new GetPackageTicketUsageDetailedOutput {
                             BrandCode = maintenancePackageOrder.BrandCode,
                             BrandName = maintenancePackageOrder.BrandName,
                             MarketingDepartmentName = marketingDepartment.Name,
                             LicensePlate = maintenancePackageOrder.LicensePlate,
                             RetailGuidePrice = partPrice.RetailGuidePrice,
                             WholeSalePrice = partPrice.WholeSalePrice,
                             OrderCode = maintenancePackageOrder.Code,
                             SettlementTime = maintenancePackageOrder.SettlementTime,
                             CustomerName = maintenancePackageOrder.CustomerName,
                             CellPhoneNumber = maintenancePackageOrder.CellPhoneNumber,
                             Vin = maintenancePackageOrder.Vin,
                             EffectiveTime = maintenancePackageOrder.EffectiveTime,
                             ExpirationTime = maintenancePackageOrder.ExpirationTime,
                             //(优惠后金额-代金券金额)÷次数
                             SinglePrice = (maintenancePackageOrder.TotalFeeAfter - maintenancePackageOrder.Coupons.Sum(d => d.CouponFee ?? 0))
                                           / (partPackage == null ? 1 : partPackage.PackageTimes),
                             PackageTicketCode = detail.PackageTicketCode,
                             PackageTicketStatus = maintenancePackageReturnOrder != null ? PackageTicketStatus.已作废 : detail.UsedFlag ? PackageTicketStatus.已使用 : PackageTicketStatus.未使用
                         };
            result = result.WhereIf(input.PackageTicketStatus.Any(), m => input.PackageTicketStatus.Contains(m.PackageTicketStatus));
            return (isDealer, result);
        }

  在这段EFCore代码中,我们通过前端输入Dto名称为GetPackageTicketUsageInput 来获取最终的输出Dto名称为GetPackageTicketUsageDetailedOutput,中间部分是LINQ语法,这里面重点分析的是SinglePrice的赋值(优惠后金额-代金券金额)/次数 ,按照我们的理解这样的写法是没有问题的,但是结果所有的SinglePrice的值都是null,这里面可以确定的是TotalFeeAfter 是有值的,那么问题到底出在哪里呢?

  我们来看看EFCore生成的SQL长什么样子?

select top (20)
  [p].[BrandCode],
  [p].[BrandName],
  [marketingDepartment].[Name] as [MarketingDepartmentName],
  [p].[DealerCode],
  [p].[DealerName],
  [p].[LicensePlate],
  [p.MaintenancePackageOrderDetail].[PartCode],
  [p.MaintenancePackageOrderDetail].[PartName],
  [partPrice].[RetailGuidePrice],
  [partPrice].[WholeSalePrice],
  [p].[Code]                   as [OrderCode],
  [p].[SettlementTime],
  [p].[CustomerName],
  [p].[CellPhoneNumber],
  [p].[Vin],
  [p].[EffectiveTime],
  [p].[ExpirationTime],
  ([p].[TotalFeeAfter] - (
    select SUM(COALESCE([d].[CouponFee], 0.0))
    from [MaintenancePackageOrderCoupon] as [d]
    where [p].[Id] = [d].[MaintenancePackageOrderId]
  )) / case
when [partPackage].[Id] is null
then 1 else [partPackage].[PackageTimes]
end as [SinglePrice], [detail].[PackageTicketCode], case
when [t].[Id] is not null
then 3 else CASE
    WHEN [detail].[UsedFlag] = 1
then 2 else 1
end
end as [PackageTicketStatus], [detail].[PackageTicketUsedTime], [detail].[SourceBillCode], [detail].[UseDealerCode], [detail].[UseDealerName]
from [MaintenancePackageOrder] as [p]
left join [MaintenancePackageOrderDetail] as [p.MaintenancePackageOrderDetail] on [p].[Id] = [p.MaintenancePackageOrderDetail].[MaintenancePackageOrderId]
inner join [PartPrice] as [partPrice] on [p.MaintenancePackageOrderDetail].[PartId] = [partPrice].[PartId]
inner join [MarketingDepartmentDetail] as [marketingDepartmentDetail] on [p].[DealerId] = [marketingDepartmentDetail].[DealerId]
inner join [MarketingDepartment] as [marketingDepartment] on [marketingDepartmentDetail].[MarketingDepartmentId] = [marketingDepartment].[Id]
inner join [MaintenancePackageOrderTicketDetail] as [detail] on [p].[Id] = [detail].[MaintenancePackageOrderId]
left join [PartPackage] as [partPackage] on [p.MaintenancePackageOrderDetail].[PartId] = [partPackage].[PackagePartId]
left join (
select [p0].*
from [MaintenancePackageReturnOrder] as [p0]
where ([p0].[Status] = 2) or ([p0].[Status] = 3)
) as [t] on [p].[Id] = [t].[MaintenancePackageOrderId]
where [p].[DealerId] =‘457EF8B6-C935-4EAC-18F2-08D70733BCF8‘

  这里面最重要的一段就是下面的部分

([p].[TotalFeeAfter] - (
    select SUM(COALESCE([d].[CouponFee], 0.0))
    from [MaintenancePackageOrderCoupon] as [d]
    where [p].[Id] = [d].[MaintenancePackageOrderId]
  )) / case
when [partPackage].[Id] is null
then 1 else [partPackage].[PackageTimes]
end as [SinglePrice]

  后面模拟了一下一条记录不存在的情况,如果确实不存在清单MaintenancePackageOrderCoupon,那么CouponFee确实返回的是null

    select SUM(COALESCE([d].[CouponFee], 0.0)) as CouponFee
    from [MaintenancePackageOrderCoupon] as [d]
    where [d].[MaintenancePackageOrderId]=‘BC594786-B9AD-41AA-8424-00001EB90E37‘

  那么问题就非常好理解了当当前主单并没有关联清单的话,后面一段返回的是null,所以TotalFeeAfter-null=null,所以无论前面TotalFeeAfter部分是否有值,所以上面出现的问题就可以理解了,显然这个是不符合我们的预期的,我们的预期是如果当前主单并没有关联上清单那么后面一部分返回的值为0而不是返回null,那么实际上就是我们的代码的问题,下面我们就来分析怎么解决这个问题。

解决方案

  我们试着来改一下EFCore的写法,既然问题出现在主单不存在清单的情况,那么我们就仅仅来改一下这个部分,这里为了避免重复贴代码,下面只改了核心部分的代码

//(优惠后金额-代金券金额)÷次数
SinglePrice = (maintenancePackageOrder.TotalFeeAfter - (maintenancePackageOrder.Coupons.Any() ? maintenancePackageOrder.Coupons.Sum(d => d.CouponFee ?? 0) : 0))
               / (partPackage == null ? 1 : partPackage.PackageTimes)

  在这段代码中只是加了这么一段判断(maintenancePackageOrder.Coupons.Any() ? maintenancePackageOrder.Coupons.Sum(d => d.CouponFee ?? 0) : 0),这么一改以后我们首先来看看这次生成的SQL到底长成什么样子?

  2.1 新生成SQL

    select top (20) [p].[BrandCode], [p].[BrandName], [marketingDepartment].[Name] as [MarketingDepartmentName], [p].[DealerCode], [p].[DealerName], [p].[LicensePlate], [p.MaintenancePackageOrderDetail].[PartCode], [p.MaintenancePackageOrderDetail].[PartName], [partPrice].[RetailGuidePrice], [partPrice].[WholeSalePrice], [p].[Code] as [OrderCode], [p].[SettlementTime], [p].[CustomerName], [p].[CellPhoneNumber], [p].[Vin], [p].[EffectiveTime], [p].[ExpirationTime],
    ([p].[TotalFeeAfter] - case
    when (
      select case
        when exists (
        select 1
        from [MaintenancePackageOrderCoupon] as [m]
        where [p].[Id] = [m].[MaintenancePackageOrderId])
        then CAST(1 as bit ) else CAST(0 as bit )
      end
      ) = 1
    then (
      select sum ( coalesce ([d].[CouponFee], 0.0))
      from [MaintenancePackageOrderCoupon] as [d]
      where [p].[Id] = [d].[MaintenancePackageOrderId]
    ) else 0.0
    end ) / case
    when [partPackage].[Id] is null
    then 1 else [partPackage].[PackageTimes]
    end as [SinglePrice], [detail].[PackageTicketCode], case
    when [t].[Id] is not null
    then 3 else case
    when [detail].[UsedFlag] = 1
    then 2 else 1
    end
    end as [PackageTicketStatus], [detail].[PackageTicketUsedTime], [detail].[SourceBillCode], [detail].[UseDealerCode], [detail].[UseDealerName]
    from [MaintenancePackageOrder] as [p]
    left join [MaintenancePackageOrderDetail] as [p.MaintenancePackageOrderDetail] on [p].[Id] = [p.MaintenancePackageOrderDetail].[MaintenancePackageOrderId]
    inner join [PartPrice] as [partPrice] on [p.MaintenancePackageOrderDetail].[PartId] = [partPrice].[PartId]
    inner join [MarketingDepartmentDetail] as [marketingDepartmentDetail] on [p].[DealerId] = [marketingDepartmentDetail].[DealerId]
    inner join [MarketingDepartment] as [marketingDepartment] on [marketingDepartmentDetail].[MarketingDepartmentId] = [marketingDepartment].[Id]
    inner join [MaintenancePackageOrderTicketDetail] as [detail] on [p].[Id] = [detail].[MaintenancePackageOrderId]
    left join [PartPackage] as [partPackage] on [p.MaintenancePackageOrderDetail].[PartId] = [partPackage].[PackagePartId]
    left join (
    select [p0].*
    from [MaintenancePackageReturnOrder] as [p0]
    where ([p0].[Status] = 2) or ([p0].[Status] = 3)
    ) as [t] on [p].[Id] = [t].[MaintenancePackageOrderId]
    where [p].[DealerId] = ‘457EF8B6-C935-4EAC-18F2-08D70733BCF8‘

  这次改写后看到差别了吗?这次TotalFeeAfter后面相减的部分不再是简单的去关联一下,而是通过case when操作来分情况进行讨论,其中也和上面一样我把最核心的一部分也拿出来分析。

    select case
      when (
      select case
        when exists (
          select 1
          from [MaintenancePackageOrderCoupon] as [m]
          where [m].[MaintenancePackageOrderId]=‘D2565056-D069-49C6-A8D8-08D7C558D94D‘)
        then CAST(1 as bit ) else CAST(0 as bit )
      end
      )= 1
      then (
        select sum (coalesce ([d].[CouponFee], 0.0))
        from [MaintenancePackageOrderCoupon] as [d]
        where [d].[MaintenancePackageOrderId]=‘D2565056-D069-49C6-A8D8-08D7C558D94D‘
      ) else 0.0
    end as CouponFee;

  这次我们也选取了一个主单并没有清单的例子,但是这段SQL最终返回的并不是null,而是返回的默认值0.0,通过这个例子我们就能够完整理解EFCore生成SQL语句之后的一些规律。

总结

  最后我们来看看这个问题的关键点:

  1 明白在SQL SERVER数据库中任何数据和null进行运算操作最终都将得到null。

  2 明白如何通过Include关联清单不存在时返回值为默认值的方式。

  3 EFCore代码中分析问题最核心的就是分析最终生成的SQL,通过最终SQL来分析代码是否符合预期,并反过来启发如何修改EFCore代码。

原文地址:https://www.cnblogs.com/seekdream/p/12527019.html

时间: 2024-09-28 10:56:31

EFCore通过Include关联清单不存在时返回值为默认值的方式的相关文章

C#把对象类型转化为指定类型,转化失败时返回该类型默认值

/// <summary> ///通用类型扩展方法类 /// </summary> public static class ObjectExtensions { /// <summary> ///把对象类型转化为指定类型,转化失败时返回该类型默认值 /// </summary> /// <typeparam name="T"> 动态类型 </typeparam> /// <param name="v

使用navicat构建数据库时,varchar的默认值保存报1064错误

原因在网上查了一下,有说默认值使用了mysql关键字问题的,也有说字符集问题的,但都不是. 我的问题很简单,设置默认值时一定要用单引号或者双引号把默认值包起来..口可口可,真他妈弱智的错误啊.

&lt;input type=&quot;text&quot;/&gt;未输入时属性value的默认值--js学习之路

在百度ife刷题是自己的一个错误引发了我对<input type="text"/>的学习. 先贴代码: <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>example</title> </head> <body> <label for="weather_input"

mysql关联两张表时的编码问题

Mysql关联两张表时,产生错误提示Illegal mix of collations 1.先用工具把数据库.两张表的编码方式改变 2.这步很重要,需要改变字段的编码方式. ALTER TABLE `表名` CHANGE `dev_chancode` `字段` VARCHAR(32) CHARACTER SET gbk NOT NULL; 总结:在建表时一定注意统一的编码方式,后续搞来搞去超级麻烦. 原文地址:https://www.cnblogs.com/Anders888/p/1144183

程序猿之---C语言细节27(函数无参数时细节、函数默认返回int型证明、return默认还回值、void指针++操作)

主要内容:函数无参数时细节.函数默认返回int型证明.return默认还回值.void指针++操作 一.函数无参数时细节 函数无参数时应该加上void 在c语言中一个函数 void f(); 在使用时传递参数f(2);没有报错,而在c++中则会报错 最好加上void来明确函数是无参数的 二.函数默认返回类型为int型 见下面程序 三.return默认返回1 细节:return不可返回执行栈内存中的指针,因为该内存在函数体结束时自动销毁 四.void 指针++操作 void *p; p++; //

定义结构体时的初始化默认值

结构体变量定义时的初始化问题: 默认值同变量一样,int型的为0,指针型的为"NULL"------------并不是整个结构体为NULL,其中的int型还是有值0的,只是指针型的为NULL 验证一下: #include "stdafx.h" struct stu { int data; char *name; }boy1,girl={102,"xiaom"}; int _tmain(int argc, _TCHAR* argv[]) { pri

.NET DateTime类型变量作为参数时设置默认值

一个小的 Tips. .NET 中函数参数的默认值需要是编译时常量.如果参数是引用类型,可以设置Null,如果是值类型,可以设置相应的编译时常量,如整型可以用整数,但对于DateTime(结构体,值类型)想要设置默认类型时要如何处理? 通常情况下会给 DateTime 结构体默认当时时间,即 DateTime.Now,但 DateTime.Now 不是编译时常量,因此无法通过编译. 如以下代码无法通过编译: public const DateTime defaultDateTime = Date

关于有默认值的字段在用EF做插入操作时的思考

今天在用EF做插入操作的时候发现数据库中一个datetime类型的字段(CreateDate)的值居然全部为null.于是赶紧看表结构发现CreateDate字段居然是允许为空的. 虽然为空,但是设置了默认值getdate(),按说不应该为null的.于是开始测试. 字段允许Null值的情况 Users表结构如下: 假如一个字段有了默认值,并且又允许为Null,在做插入操作时会发生什么? 如上图中的表结构,CreateDate是允许为null的,而又有默认值getdate().这样在用传统SQL

pycharm 修改新建文件时的头部模板(默认为__author__=&#39;...&#39;)

pycharm 修改新建文件时的头部模板 默认为__author__='...'    [省略号是默认你的计算机名] 修改这个作者名的步骤: 依次点击:File->Settings->Editor->File and Code Templete 点击右侧Templates选项卡,会有一些格式文件新建时的模板 在这里可以修改这些默认模板 以修改Python Script为例: 默认为: __author__ = '$USER' 把 '$USER' 换成你想要的作者名,也可以直接将这一句删掉