最近在開發的過程中遇到了一個問題,在使用EFCore2.2 版本進行開發的時候,調試的時候一直報Data is Null. This method or property cannot be called on Null values這個錯誤,憑自己的直覺又是哪個空類型的轉換出了錯誤,但是問題到底該怎么找呢?而且整個代碼的邏輯還不算簡單,怎么一步步找到問題並解決問題呢?本文主要從問題的發現到問題的解決來一步步歸納到底該如何順利找到這類問題的解決方案。
一 背景
在使用EFCore進行開發的時候有時候考慮到一些性能的問題,我們不得不使用一些能夠直接執行純SQL的框架,比如Dapper,我們的整個項目都是使用ABP框架作為主框架,所以ABP內部會集成Dapper並提供對外的接口供我們來使用,在理解本篇文章之前建議你先看一下這篇文章從而對后面的代碼有一個整體的認知。
先看一下我們代碼調用出錯的位置及具體內容。
public PageWithTotal<GetPartStockOutput, GetPartStockTotalOutput> GetPartStocks(GetPartStockInput input, PageRequest pageRequest) { var queryPartStock = QueryPartStock(input); //服務站服務信息 var dealerBusinessPermit = _dealerBusinessPermitManager.GetDefaultBranDealerBusinessPermit(SdtSession.TenantId.GetValueOrDefault()); var result = _partStockRepository.GetPartStockWithPrice(queryPartStock, SdtSession.TenantId.GetValueOrDefault(), dealerBusinessPermit.UserDefinedPriceBaseType, dealerBusinessPermit.RetailGuidePriceFactor, dealerBusinessPermit.UserDefinedPriceFactor) .WhereIf(!string.IsNullOrWhiteSpace(input.SupplierCode), r => r.SupplierCode.Contains(input.SupplierCode)) .WhereIf(!string.IsNullOrWhiteSpace(input.SupplierName), r => r.SupplierName.Contains(input.SupplierName)); var count = result.Count(s => _part.GetAll().Any(p => p.Id == s.PartId)); var pageResults = result.PageAndOrderBy(pageRequest).ToList(); var varietySum = result.Select(q => q.PartId).Distinct().Count(); var total = new GetPartStockTotalOutput(); if (count > 0) { total = result.GroupBy(g => 1).Select(g => new GetPartStockTotalOutput { StockQuantity = g.Sum(r => r.StockQuantity), TaxFreePrice = g.Sum(r => r.TaxFreeFeeSum), StockPrice = g.Sum(r => r.StockCost), BorrowAndNotReturnPrice = g.Sum(r => r.BorrowAndNotReturnPrice), BorrowAndNotReturn = g.Sum(r => r.BorrowAndNotReturn) }).First(); total.VarietySum = varietySum; total.CostPrice = total.StockPrice + (total.BorrowAndNotReturnPrice ?? 0); total.Tax = total.CostPrice - total.TaxFreePrice; total.TotalQuantity = total.StockQuantity + (total.BorrowAndNotReturn ?? 0); } var queryResults = ObjectMapper.Map<List<GetPartStockOutput>>(pageResults); return new PageWithTotal<GetPartStockOutput, GetPartStockTotalOutput>(pageRequest, count, queryResults, total); }
這里面最核心的一個方法就是GetPartStockWithPrice這個,這個部分是通過ABP中集成的Dapper來實現的,這個里面調用了一個重要的子函數QueryPartStock,這個函數主要是將前端傳入查詢條件的參數轉換成后端對應的EFCore代碼,這里我也貼出具體點的代碼。
private IQueryable<PartStock> QueryPartStock(GetPartStockInput input) { var company = _companyManager.GetValidCompany(SdtSession.TenantId); return _partStockRepository.GetAll() .WhereIf(company.Type == CompanyCategory.服務站, r => r.DealerId == SdtSession.TenantId) .WhereIf(input.WarehouseId.HasValue, r => r.WarehouseId == input.WarehouseId) .WhereIf(!string.IsNullOrWhiteSpace(input.BinCode), r => r.BinCode.Contains(input.BinCode)) .WhereIf(input.PartCodes?.Count == 1, r => r.PartCode.Contains(input.PartCodes[0])) .WhereIf(input.PartCodes?.Count > 1, r => input.PartCodes.Contains(r.PartCode)) .WhereIf(!string.IsNullOrWhiteSpace(input.PartCode), r => r.PartCode.Contains(input.PartCode)) .WhereIf(!string.IsNullOrWhiteSpace(input.PartName), r => r.PartName.Contains(input.PartName)) .WhereIf(input.ZeroStock.HasValue, r => input.ZeroStock.Value ? r.StockQuantity == 0 : r.StockQuantity != 0) .WhereIf(input.Unassigned, r => r.BinId == null) .WhereIf(input.BrandId.HasValue, r => _partPrice.GetAll().Any(ps => ps.PartId == r.PartId && ps.BrandId == input.BrandId)) .WhereIf(input.IsExternalPart.HasValue, r => _part.GetAll().Any(p => p.Id == r.PartId && p.IsExternalPart == input.IsExternalPart)) .WhereIf(input.PartProperty?.Length > 0, s => _partSalesProperty.GetAll() .Where(p => p.PartProperty != PartProperty.套餐 && input.PartProperty.Contains(p.PartProperty.Value)).Any(p => p.PartId == s.PartId)) .WhereIf(input.BeginLastInTime.HasValue, ps => ps.LastInTime >= input.BeginLastInTime) .WhereIf(input.EndLastInTime.HasValue, ps => ps.LastInTime <= input.EndLastInTime) .WhereIf(input.BeginLastOutTime.HasValue, ps => ps.LastOutTime >= input.BeginLastOutTime) .WhereIf(input.EndLastOutTime.HasValue, ps => ps.LastOutTime <= input.EndLastOutTime); }
這段函數最終返回一個IQueryable<PartStock>的對象信息,其中PartStock是一個Entity和數據庫中唯一的實體對應,_partStockRepository對應的是PartStock具體的倉儲,在ABP框架中我們一般是通過構造函數注入IRepository<PartStock, Guid> 來實現對整個倉儲類實現的,這里我們着重看一下第一個函數內部GetPartStockWithPrice的具體實現,我們先來看一下這個函數的定義。
using System; using System.Linq; using Abp.Domain.Repositories; using Sunlight.Dms.Parts.Domain.PartStocks.Models; namespace Sunlight.Dms.Parts.Domain.PartStocks { public interface IPartStockRepository : IRepository<PartStock, Guid> { /// <summary> /// 查詢備件庫存信息 /// </summary> /// <typeparam name="T"></typeam name="query"></param> /// <param name="dealerId">當前登錄服務站信息</param> /// <param name="userDefinedPriceBaseType">備件自定義價格基准價類型</param> /// <param name="retailGuidePriceFactor">備件零售價格系數</param> /// <param name="userDefinedPriceFactor">自定義價格系數</param> /// <returns></returns> IQueryable<PartStockWithPriceModel> GetPartStockWithPrice<T>(IQueryable<T> query, Guid dealerId, UserDefinedPriceBaseType userDefinedPriceBaseType, decimal retailGuidePriceFactor, decimal userDefinedPriceFactor); } }
這個方法是定義在一個叫做繼承自IRepository<PartStock, Guid>的倉儲類中的,返回的結構是一個IQueryable<PartStockWithPriceModel>類型的對象,這里我們看一下PartStockWithPriceModel這個對象,這里為了分析方便只截取了這個實體中一部分代碼,我們也貼出這部分的內容,另外PartStockWithPriceModel需要定義在當前的DbContext下面,並且定義成下面的形式 public DbQuery<PartStockWithPriceModel> PartStockWithPriceModels { get; set; },這里DbQuery會映射為數據庫視圖類型。
using System; using Sunlight.Abstractions; namespace Sunlight.Dms.Parts.Domain.PartStocks.Models { public class PartStockWithPriceModel : IRowVersion<byte[]> { public decimal StockQuantity { get; set; } /// <summary> /// 批發價 /// </summary> public decimal? WholeSalePrice { get; set; } /// <summary> /// 零售指導價 /// </summary> public decimal? RetailGuidePrice { get; set; } /// <summary> /// 是否外采件 /// </summary> public bool IsExternalPart { get; set; } /// <summary> /// 庫存金額 /// </summary> public decimal? StockCost { get; set; } /// <summary> /// 成本單價 /// </summary> public decimal? CostPrice { get; set; } public bool? IsDecimalAllowed { get; set; } /// <summary> /// 借出未還數量(取值:備件借用單.狀態 = 生效/部分歸還,取其清單.借用數量-已歸還數量) /// </summary> public decimal? BorrowAndNotReturn { get; set; } /// <summary> /// 備件自定義名稱 /// </summary> public string UserDefinedPartName { get; set; } /// <summary> /// 本店零售價 /// </summary> public decimal? UserDefinedRetailGuidePrice { get; set; } /// <summary> /// 出庫價 /// </summary> public decimal UserDefinedPartPrice { get; set; } public decimal? TaxFreePrice { get; set; } /// <summary> /// 不含稅成本金額 /// </summary> public decimal? TaxFreeFeeSum { get; set; } /// <summary> /// 最近采購供應商編號 /// </summary> public string SupplierCode { get; set; } /// <summary> /// 最近采購供應商名稱 /// </summary> public string SupplierName { get; set; } /// <summary> /// 借出未還金額 /// </summary> public decimal? BorrowAndNotReturnPrice { get; set; } /// <summary> /// 備件庫存RowVersion /// </summary> public byte[] RowVersion { get; set; } } }
然后我們來看看這個方法的具體實現,后面我們將會通過后面的具體代碼來進行分析。
using System; using System.Linq; using Abp.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; using Sunlight.Dms.Parts.Domain; using Sunlight.Dms.Parts.Domain.PartStocks; using Sunlight.Dms.Parts.Domain.PartStocks.Models; using Sunlight.EFCore.Extensions; namespace Sunlight.Dms.Parts.Data.EntityFrameworkCore.Repository { public class PartStockRepository : PartsRepositoryBase<PartStock, Guid>, IPartStockRepository { public PartStockRepository(IDbContextProvider<PartsDbContext> dbContextProvider) : base(dbContextProvider) { } /// <summary> /// 查詢備件庫存信息 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="query"></param> /// <param name="dealerId">當前登錄服務站信息</param> /// <param name="userDefinedPriceBaseType">備件自定義價格基准價類型</param> /// <param name="retailGuidePriceFactor">備件零售價格系數</param> /// <param name="userDefinedPriceFactor">自定義價格系數</param> /// <returns></returns> public IQueryable<PartStockWithPriceModel> GetPartStockWithPrice<T>(IQueryable<T> query, Guid dealerId, UserDefinedPriceBaseType userDefinedPriceBaseType, decimal retailGuidePriceFactor, decimal userDefinedPriceFactor) { var stockSql = query.ToSql(Context); var querySql = $@"SELECT [borrow].[BorrowAndNotReturn], [borrow].[BorrowAndNotReturn] * [psc].[CostPrice] [BorrowAndNotReturnPrice], [partStock].*, ISNULL([psc].[CostPrice], 0) * [partStock].[StockQuantity] AS [StockCost], [psc].[CostPrice], [partPrice].[WholeSalePrice], [pcp].[UserDefinedRetailGuidePrice], case when [part].[IsExternalPart] = 1 then COALESCE([partPrice].[RetailGuidePrice], 0.0) else ROUND(COALESCE([partPrice].[RetailGuidePrice], 0.0) * {retailGuidePriceFactor} ,0) end AS [RetailGuidePrice], --定義出庫價 case when [part].[IsExternalPart] = 1 then COALESCE([partPrice].[RetailGuidePrice], 0.0) else case when [pcp].[UserDefinedRetailGuidePrice] is not null then [pcp].[UserDefinedRetailGuidePrice] else case when {(int)userDefinedPriceBaseType} =1 then ROUND(COALESCE([partPrice].[WholeSalePrice], 0.0) * {retailGuidePriceFactor} * {userDefinedPriceFactor},0) when {(int)userDefinedPriceBaseType} =2 then ROUND(COALESCE([partPrice].[RetailGuidePrice], 0.0) * {retailGuidePriceFactor} * {userDefinedPriceFactor},0) end end end AS [UserDefinedPartPrice], --定義出庫價來源 case when [part].[IsExternalPart] = 1 then 3 else case when [pcp].[UserDefinedRetailGuidePrice] is not null then 1 else 2 end end AS [OutPriceSourceType], [psc].[TaxFreePrice], ISNULL([psc].[TaxFreePrice], 0) * [partStock].[StockQuantity] + ISNULL([outSum].[Fee], 0.0) - ISNULL([inSum].[Fee], 0.0) AS [TaxFreeFeeSum], [psc].[Id] AS [PartStockCostId], [psc].[SupplierCode], [psc].[SupplierName], [part].[IsDecimalAllowed], [part].[IsExternalPart] FROM ({stockSql}) [partStock] LEFT JOIN ( SELECT [p].* FROM [PartStockCost] AS [p] WHERE [p].[DealerId] = '{dealerId}' ) AS [psc] ON [partStock].[PartId] = [psc].[PartId] LEFT JOIN [PartPrice] AS [partPrice] ON [partStock].[PartId] = [partPrice].[PartId] LEFT JOIN [PartCustomerProperty] AS [pcp] ON [partStock].[PartId] = [pcp].[PartId] AND [partStock].DealerId = [pcp].[DealerId] INNER JOIN [Part] AS [part] ON [partStock].[PartId] = [part].[Id] LEFT JOIN ( SELECT [partBorrowDetail].[PartId], [t0].[BorrowWarehouseId] AS [WarehouseId], SUM([partBorrowDetail].[BorrowQuantity] - COALESCE([partBorrowDetail].[ReturnedQuantity], 0.0)) AS [BorrowAndNotReturn] FROM [PartBorrowDetail] AS [partBorrowDetail] INNER JOIN ( SELECT [b].* FROM [PartBorrowOrder] AS [b] WHERE [b].[Status] IN ({(int)PartBorrowStatus.生效}, {(int)PartBorrowStatus.部分歸還}) AND ([b].[DealerId] = '{dealerId}') ) AS [t0] ON [partBorrowDetail].[PartBorrowOrderId] = [t0].[Id] GROUP BY [partBorrowDetail].[PartId], [t0].[BorrowWarehouseId] ) AS [borrow] ON ([partStock].[WarehouseId] = [borrow].[WarehouseId]) AND ([partStock].[PartId] = [borrow].[PartId]) LEFT JOIN (SELECT WarehouseId, PartId, sum(TaxFreeFeeSum) Fee FROM PartOut INNER JOIN PartOutDetail ON PartOut.Id = PartOutDetail.PartOutId WHERE OutType = {(int)PartOutType.借用出庫} GROUP BY WarehouseId, PartId ) outSum ON ([partStock].[WarehouseId] = [outSum].[WarehouseId]) AND ([partStock].[PartId] = [outSum].[PartId]) LEFT JOIN (SELECT WarehouseId, PartId, sum(TaxFreeFeeSum) Fee FROM PartIn INNER JOIN PartInDetail ON PartIn.Id = PartInDetail.PartInId WHERE InType = {(int)PartInType.借用歸還入庫} GROUP BY WarehouseId, PartId ) inSum ON ([partStock].[WarehouseId] = [inSum].[WarehouseId]) AND ([partStock].[PartId] = [inSum].[PartId]) WHERE [partStock].[DealerId] = '{dealerId}' "; #pragma warning disable EF1000 // Possible SQL injection vulnerability. return Context.PartStockWithPriceModels.FromSql(querySql); #pragma warning restore EF1000 // Possible SQL injection vulnerability. } } }
寫到這里我們要重點講述一下 var stockSql = query.ToSql(Context);這部分代碼,在我們使用EFCore的時候不可避免使用混合方式進行編程,有了這個ToSql方法我們就很容易將EFCore中的IQueryable類型查詢代碼轉換為SQL語句最終拼接到最終的查詢語句中,這樣能夠在一定程度上簡化代碼,那么這個ToSql的方法到底是怎么實現的,我們來簡單看看。
using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; using System.Data.Common; using System.Linq; using System.Reflection; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Query; using Sunlight.Domain.Models; using Sunlight.EFCore.Repositories; #if NETCOREAPP2_2 using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Query.Internal; using Microsoft.EntityFrameworkCore.Storage; #else using Microsoft.EntityFrameworkCore.Query.SqlExpressions; #endif namespace Sunlight.EFCore.Extensions { /// <summary> /// IQueryable類型的擴展方法 /// </summary> public static class QueryableExtensions { #if NETCOREAPP2_2 /// <summary> /// 將查詢語句轉換成Sql, 便於進一步的Sql拼接 /// <seealso href="https://github.com/yangzhongke/ZackData.Net/blob/master/Tests.NetCore/IQueryableExtensions.cs" /> /// </summary> /// <param name="query"></param> /// <param name="dbCtx"></param> /// <typeparam name="TEntity"></typeparam> /// <returns></returns> public static string ToSql<TEntity>(this IQueryable<TEntity> query, DbContext dbCtx) { var modelGenerator = dbCtx.GetService<IQueryModelGenerator>(); var queryModel = modelGenerator.ParseQuery(query.Expression); var databaseDependencies = dbCtx.GetService<DatabaseDependencies>(); var queryCompilationContext = databaseDependencies.QueryCompilationContextFactory.Create(false); var modelVisitor = (RelationalQueryModelVisitor)queryCompilationContext.CreateQueryModelVisitor(); modelVisitor.CreateQueryExecutor<TEntity>(queryModel); var sql = modelVisitor.Queries.First().ToString(); return sql; } #else /// <summary> /// 將查詢語句轉換成Sql, 便於進一步的Sql拼接 /// <seealso href="https://gist.github.com/rionmonster/2c59f449e67edf8cd6164e9fe66c545a#gistcomment-3109335" /> /// </summary> /// <param name="query"></param> /// <param name="dbCtx">數據庫上下文</param> /// <typeparam name="TEntity"></typeparam> /// <returns></returns> public static string ToSql<TEntity>(this IQueryable<TEntity> query, DbContext dbCtx = null) where TEntity : class { return ToSql(query); } /// <summary> /// 將查詢語句轉換成Sql, 便於進一步的Sql拼接 /// <seealso href="https://gist.github.com/rionmonster/2c59f449e67edf8cd6164e9fe66c545a#gistcomment-3109335" /> /// </summary> /// <param name="query"></param> /// <typeparam name="TEntity"></typeparam> /// <returns></returns> private static string ToSql<TEntity>(this IQueryable<TEntity> query) where TEntity : class { using var enumerator = query.Provider.Execute<IEnumerable<TEntity>>(query.Expression).GetEnumerator(); var relationalCommandCache = enumerator.Private("_relationalCommandCache"); var selectExpression = relationalCommandCache.Private<SelectExpression>("_selectExpression"); var factory = relationalCommandCache.Private<IQuerySqlGeneratorFactory>("_querySqlGeneratorFactory"); var sqlGenerator = factory.Create(); var command = sqlGenerator.GetCommand(selectExpression); var sql = command.CommandText; return sql; } private static object Private(this object obj, string privateField) => obj?.GetType().GetField(privateField, BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(obj); private static T Private<T>(this object obj, string privateField) => (T)obj?.GetType().GetField(privateField, BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(obj); /// <summary> /// 增加 /// </summary> /// <param name="query"></param> /// <typeparam name="TEntity"></typeparam> /// <returns></returns> public static (string, IReadOnlyDictionary<string, object>) ToSqlWithParams<TEntity>(this IQueryable<TEntity> query) { using var enumerator = query.Provider.Execute<IEnumerable<TEntity>>(query.Expression).GetEnumerator(); var relationalCommandCache = enumerator.Private("_relationalCommandCache"); var selectExpression = relationalCommandCache.Private<SelectExpression>("_selectExpression"); var factory = relationalCommandCache.Private<IQuerySqlGeneratorFactory>("_querySqlGeneratorFactory"); var queryContext = enumerator.Private<RelationalQueryContext>("_relationalQueryContext"); var sqlGenerator = factory.Create(); var command = sqlGenerator.GetCommand(selectExpression); var parametersDict = queryContext.ParameterValues; var sql = command.CommandText; return (sql, parametersDict); } #endif } }
到了這里整個過程的背景全部交代完了,通過上面的講述你也可以加深對EFCore的理解,同時對整個混合方式查詢訪問數據庫有了一個更加清晰的認識,這個里面有很多的知識點需要自己去好好消化。
二 解決過程
有了上面的代碼,下面就是講我們遇到的問題並且解決問題的過程,在這里我們在本地Swagger傳入參數並點擊查詢的時候結果一直報錯Data is Null. This method or property cannot be called on Null values,這里還看不出什么我們來看看具體的堆棧調用信息。
System.Data.SqlTypes.SqlNullValueException: Data is Null. This method or property cannot be called on Null values. at System.Data.SqlClient.SqlBuffer.get_Decimal() at System.Data.SqlClient.SqlDataReader.GetDecimal(Int32 i) at lambda_method(Closure , DbDataReader , DbContext ) at Microsoft.EntityFrameworkCore.Query.QueryMethodProvider._FastQuery[TEntity](RelationalQueryContext relationalQueryContext, ShaperCommandContext shaperCommandContext, Func`3 materializer, Type contextType, IDiagnosticsLogger`1 logger)+MoveNext() at System.Collections.Generic.List`1.AddEnumerable(IEnumerable`1 enumerable) at System.Linq.Enumerable.ToList[TSource](IEnumerable`1 source) at Sunlight.Dms.Parts.Application.PartStocks.PartStockService.GetPartStocks(GetPartStockInput input, PageRequest pageRequest) in D:\項目代碼\Chery\Dms\parts-service\src\Sunlight.Dms.Parts.Application\PartStocks\PartStockService.cs:line 142 at Castle.Proxies.Invocations.IPartStockService_GetPartStocks.InvokeMethodOnTarget() at Castle.DynamicProxy.AbstractInvocation.Proceed() at Abp.Domain.Uow.UnitOfWorkInterceptor.PerformSyncUow(IInvocation invocation, UnitOfWorkOptions options) in D:\Github\aspnetboilerplate\src\Abp\Domain\Uow\UnitOfWorkInterceptor.cs:line 68 at Castle.DynamicProxy.AbstractInvocation.Proceed() at Abp.Auditing.AuditingInterceptor.PerformSyncAuditing(IInvocation invocation, AuditInfo auditInfo) in D:\Github\aspnetboilerplate\src\Abp\Auditing\AuditingInterceptor.cs:line 59 at Castle.DynamicProxy.AbstractInvocation.Proceed() at Castle.DynamicProxy.AbstractInvocation.Proceed() at Castle.Proxies.PartStockServiceProxy.GetPartStocks(GetPartStockInput input, PageRequest pageRequest) at Sunlight.Dms.Parts.WebHost.Controllers.PartStockController.GetPartStock(GetPartStockInput input, PageRequest pageRequest) in D:\項目代碼\Chery\Dms\parts-service\src\Sunlight.Dms.Parts.WebHost\Controllers\PartStockController.cs:line 42 at lambda_method(Closure , Object , Object[] ) at Microsoft.Extensions.Internal.ObjectMethodExecutor.Execute(Object target, Object[] parameters) at Microsoft.AspNetCore.Mvc.Internal.ActionMethodExecutor.SyncActionResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments) at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.InvokeActionMethodAsync() at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.InvokeNextActionFilterAsync() at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.Rethrow(ActionExecutedContext context) at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted) at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.InvokeInnerFilterAsync() at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeNextExceptionFilterAsync()
通過上面的堆棧調用信息我們發現內部是這個SqlDataReader.GetDecimal(Int32 i) 一直報錯,看這個報錯提示這個方法只能接受Int32類型的參數,可能在實際轉換中出現了null值,當這個null值傳入到這個方法內部的時候就會報這個錯誤,那么我們來看哪個decimal參數會導致這個問題呢?我們知道最終查詢的所有數據都會映射成一個IQueryable<PartStockWithPriceModel>類型的對象,只有可能是這個過程中出現了問題,我們來把PartStockWithPriceModel這個實體里面所有的decimal類型的都找出來看看有什么收獲,我們發現整個實體中只有StockQuantity和UserDefinedPartPrice兩個屬性是定義成了decimal類型的,其它的所有類型都是decimal?可為空的decimal類型,到了這里我們發現如果這兩個字段最終從數據庫查詢到的結果是null,那么將其轉換為decimal類型的時候肯定是有問題的,這個轉換過程在內部應該就是調用的SqlDataReader.GetDecimal(Int32 i)這個方法,當值為null的時候就會報上面的錯誤,那么到底是不是這個原因呢?我們來驗證一下。
我們把最終生成的SQL拿到數據庫去查詢發現了定義成decimal類型的兩個字段,UsedDefinedPartPrice果然有很多的數據為null,這個最終轉換成decimal類型的時候肯定是有問題的,那么我們的猜測到底是不是正確的呢?
圖一 查詢結果
三 最后驗證
帶着我們猜測的結果,我們把PartStockWithPriceModel里面的UsedDefinedPartPrice定義的類型修改為可為空的decimal類型,再次查詢結果一切正確,至此所有問題完美解決,由上面的整個過程你應該對整個過程怎么分析問題最終找到問題的過程有一個深入的理解了,所以這里面我們可以吸取的教訓就是數據庫中查不到數據並返回null是一個大概率事件,所以很多時候我們才需要使用ISNULL或者是COALESCE函數來規避各種為空的問題,另外我們在實體中定義值類型的時候如果這個值要和數據庫中的字段進行映射,如果可以的話最好定義成可為空的類型,這些也算是從上面的例子中得到的一點點經驗吧。