存储过程为列返回值,但 FromSqlRaw 为类属性返回 null

Posted

技术标签:

【中文标题】存储过程为列返回值,但 FromSqlRaw 为类属性返回 null【英文标题】:Stored Procedure returns value for a column but FromSqlRaw returns null for the class property 【发布时间】:2021-12-23 07:25:14 【问题描述】:

我有一个由多种方法调用的类。这个类是:

public class Policy : EntityObject

    public Guid PolicyId  get; set; 
    public Guid CustomerId  get; set; 
    public string PolicyNumber  get; set; 
    [NotMapped]
    public string TrimmedPolicyNumber  get; set; 
    public DateTime? PolicyEffectiveDate  get; set; 
    public DateTime? PolicyExpirationDate  get; set; 
    [NotMapped]
    public string PolicyType  get; set; 
    public string InsuranceCompany  get; set; 
    public string WritingCompany  get; set; 
    [NotMapped]
    public string BillMethod_PaymentPlan  get; set; 
    [NotMapped]
    public decimal? FullTermPremium  get; set; 
    [NotMapped]
    public string AccountExecutive  get; set; 
    [NotMapped]
    public string AccountRepresentative  get; set; 

    [NotMapped]
    public string PolicyLineOfBusiness  get; set; 

    [NotMapped]
    public string Status  get; set; 

现在我有以下存储过程:

Create PROCEDURE [GetActivePoliciesByCustomer]
@CustomerId UNIQUEIDENTIFIER
AS
    SET NOCOUNT ON;
    Select
        c.CustId as CustomerId
        , p.PolId as PolicyId
        , p.PolNo as PolicyNumber
        , p.PolTypeLOB as [PolicyLineOfBusiness]
        , p.PolEffDate as PolicyEffectiveDate
        , p.PolExpDate as PolicyExpirationDate
        , cmp.Name as InsuranceCompany
        , wcmp.Name as WritingCompany
        , pr.Description as [Status]
    FROM
        Policies p
        (Further details have been omitted as it is not important)
GO

此存储过程返回上面列出的所有字段的数据。但是,当我拨打以下电话时:

public async Task<List<Policy>> GetActivePoliciesByCustomerId(Guide customerId)

    var activePolicies = await _context.Policy
        .FromSqlRaw<Policy>("EXEC [GetActivePoliciesByCustomer] @customerId=0", customerId)
        .ToListAsync();

    return activepolicies;

在调试会话期间,我看到 Status 和 PolicyLineOfBusiness 设置为 null。我怀疑[NotMapped] 属性会阻止这些字段被映射,我已经验证了这一点。如果我删除 [NotMapped] 属性,我会看到填充了 Status 和 PolicyLineOfBusiness 字段。但是,这个类(Policy)被另一个调用使用:

public async Task<Policy> GetPolicyByPolicyId(Guid id)

    var policyDetails = await _context.Policy
        .FromSqlRaw<Policy>("EXEC [GetPolicyDetailsByPolicyId] @policyId=0", id)
        .ToListAsync();

    return policyDetails.FirstOrDefault();

它正在调用的存储过程:

CREATE PROCEDURE [GetPolicyDetailsByPolicyId]
@PolicyId UNIQUEIDENTIFIER
AS
    SET NOCOUNT ON;

    SELECT TOP 1
        p.PolId as PolicyId
        , p.CustId as CustomerId
        , p.PolNo as PolicyNumber
        , p.ShortPolNo as TrimmedPolicyNumber
        , p.PolEffDate as PolicyEffectiveDate
        , p.PolExpDate as PolicyExpirationDate
        , p.PolTypeLOB as PolicyType
        , c.[Name] as InsuranceCompany
        , wc.[Name] as WritingCompany
        , p.BillMethod_PaymentPlan
        , p.FullTermPremium
        , e1.[LastName] as AccountExecutive
        , CONCAT(e.FirstName, ' ', e.LastName) as AccountRepresentative 
    From
        Policies p
        (Further details have been omitted as it is not important)

如果我从上述属性(PolicyLineOfBusiness 和 Status)中删除 [NotMapped] 属性并使用以前的方法(GetPolicyByPolicyId)调用上述存储过程,我会得到一个异常:

System.InvalidOperationException: The required column 'PolicyLineOfBusiness' was not present in the results of a 'FromSql' operation.

那么我该如何解决使属性可选的问题,同时它们应该能够映射到两个不同的存储过程返回的字段?如果有更好的方法,我愿意接受建议。提前致谢。

【问题讨论】:

这是关于这里回答的内容:Assigning NotMapped property value after "regular" (mapped) property value has been assigned 吗? 也许您可以拥有一个具有公共字段的类,并为第二个存储过程创建另一个具有继承性的类?或者它不是一种选择? 不相关,但为什么名为 'GetPolicyDetailsByPolicyId' 的 proc 需要 TOP 1 ? 您使用的是哪个 dbms? (上面的 SQL 是特定于产品的。) 请将sample code 设为最小且完整。大多数Policy 属性和数据库列都可以删除,并添加了_context 的声明。 【参考方案1】:

这个解决方案可以帮助你。

首先,我们需要使用继承。

其次,我们必须创建一个鉴别器来处理 SP 和类。

最后,我们在存储过程中设置判别器。

让我们看一些代码。

    使用您的课程:
public class Policy : EntityObject

    public Guid PolicyId  get; set; 
    public Guid CustomerId  get; set; 
    public string PolicyNumber  get; set;     
    public DateTime? PolicyEffectiveDate  get; set; 
    public DateTime? PolicyExpirationDate  get; set; 
    public string InsuranceCompany  get; set; 
    public string WritingCompany  get; set; 


public class CustomerPolicy : Policy

    public string Status  get; set; 
    public string PolicyLineOfBusiness  get; set;         


public class DetailedPolicy : Policy
    
    public string TrimmedPolicyNumber  get; set; 
    public string PolicyType  get; set; 
    public string BillMethod_PaymentPlan  get; set;     
    public decimal? FullTermPremium  get; set;     
    public string AccountExecutive  get; set; 
    public string AccountRepresentative  get; set; 

    设置鉴别器:
public class Policy : EntityObject

    public Guid PolicyId  get; set; 
    // ...
    [NotMapped]  
    public string discriminator  get; set;  


然后,流畅的 API 设置

// ...
public DbSet<Policy> Policy  get; set; 

protected override void OnModelCreating(ModelBuilder modelBuilder)

  // here you can use enum, byte, int, etc. instead of string.
  modelBuilder.Entity<Policy>()
    .HasDiscriminator<string>("discriminator")
    .HasValue<Policy>(nameof(Policy)) 
    .HasValue<CustomerPolicy>(nameof(CustomerPolicy))
    .HasValue<DetailedPolicy>(nameof(DetailedPolicy));
  // ...

    在这一步中,我们必须将鉴别器(类名)传递给存储过程。
CREATE PROCEDURE [GetPolicyDetailsByPolicyId]
@PolicyId UNIQUEIDENTIFIER
AS
    SET NOCOUNT ON;

    SELECT TOP 1
        p.PolId as PolicyId
        , p.CustId as CustomerId
        , p.PolNo as PolicyNumber
        , p.ShortPolNo as TrimmedPolicyNumber
        , p.PolEffDate as PolicyEffectiveDate
        , p.PolExpDate as PolicyExpirationDate
        , p.PolTypeLOB as PolicyType
        , c.[Name] as InsuranceCompany
        , wc.[Name] as WritingCompany
        , p.BillMethod_PaymentPlan
        , p.FullTermPremium
        , e1.[LastName] as AccountExecutive
        , CONCAT(e.FirstName, ' ', e.LastName) as AccountRepresentative 

        // Add the discriminator here
        ,'DetailedPolicy' discriminator

    From
        Policies p
        (Further details have been omitted as it is not important)

在另一个 sp 中重复。

Create PROCEDURE [GetActivePoliciesByCustomer]
@CustomerId UNIQUEIDENTIFIER
AS
    SET NOCOUNT ON;
    Select
        c.CustId as CustomerId
        , p.PolId as PolicyId
        , p.PolNo as PolicyNumber
        , p.PolTypeLOB as [PolicyLineOfBusiness]
        , p.PolEffDate as PolicyEffectiveDate
        , p.PolExpDate as PolicyExpirationDate
        , cmp.Name as InsuranceCompany
        , wcmp.Name as WritingCompany
        , pr.Description as [Status]

        // Add the discriminator here
        'CustomerPolicy' discriminator
         
    FROM
        Policies p
        (Further details have been omitted as it is not important)
GO

EF 上下文调用:

using Microsoft.EntityFrameworkCore;
using Microsoft.Data.SqlClient;

// ...

var activePolicies = await context.Policy
    .FromSqlRaw("exec [GetActivePoliciesByCustomer] @customerId", 
        new SqlParameter("customerId", customerId))
    .AsAsyncEnumerable();

var policyDetails = await context.Policy
    .FromSqlRaw("exec [GetPolicyDetailsByPolicyId] @policyId", 
        new SqlParameter("policyId", id))
    .AsAsyncEnumerable();

注意:您可能需要使用 AsAsyncEnumerable 或 IQueryable 来正确转换您的类。

更新:

注意2:如果编译器出现问题,你必须用其他派生类的null填充所有字段。

例如:

CREATE PROCEDURE [GetPolicyDetailsByPolicyId]
// ...
        ,'DetailedPolicy' discriminator
        ,null Status
        ,null PolicyLineOfBusiness 
// ...

以下是 .NET Core 5 和 EF Core 5 的示例:

public class Policy

  public int field1  get; set;     
  [NotMapped]
  public string discriminator  get; set;  


public class Policy2 : Policy

  public int? field2  get; set; 


public class Policy3 : Policy

  public int? field3  get; set; 

流畅的 API:

// ...
public DbSet<Policy> Policies  get; set; 

protected override void OnModelCreating(ModelBuilder modelBuilder)

  modelBuilder.Entity<Policy>().HasNoKey();
  
  modelBuilder.Entity<Policy>()
    .HasDiscriminator<string>("discriminator")
    .HasValue<Policy>(nameof(Policy))
    .HasValue<Policy2>(nameof(Policy2))
    .HasValue<Policy3>(nameof(Policy3));

  //...

自定义存储过程:

create procedure GenPolicyQuery
    @id int,
    @source varchar(10)
as

declare @field1 int, @field2 int, @field3 int

if @source =  'Policy'
    set @field1 = @id
else if @source = 'Policy2'
    select @field1 = 10, @field2 = @id
else if @source = 'Policy3'
    select @field1 = 10, @field3 = @id

select @field1 field1, @field2 field2, @field3 field3, @source discriminator

go

结果:

using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.Data.SqlClient;
using Newtonsoft.Json;
// ...

var policy1 = db.Policies
  .FromSqlRaw("exec GenPolicyQuery @id, @source", 
    new SqlParameter("id", 10), 
    new SqlParameter("source", "Policy"))        
  .AsEnumerable();

var policy2 = db.Policies
  .FromSqlRaw("exec GenPolicyQuery @id, @source", 
    new SqlParameter("id", 20), 
    new SqlParameter("source", "Policy2"))
  .AsEnumerable();

var policy3 = db.Policies
  .FromSqlRaw("exec GenPolicyQuery @id, @source", 
    new SqlParameter("id", 30), 
    new SqlParameter("source", "Policy3"))
  .AsEnumerable();
    
Console.WriteLine(JsonConvert.SerializeObject(policy1));
Console.WriteLine(JsonConvert.SerializeObject(policy2));
Console.WriteLine(JsonConvert.SerializeObject(policy3))

// Result
/*
  ["field1":10,"discriminator":"Policy"]
  ["field2":20,"field1":10,"discriminator":"Policy2"]
  ["field3":30,"field1":10,"discriminator":"Policy3"]
*/

【讨论】:

【参考方案2】:

您的Policies 类同时具有PolicyTypePolicyLineOfBusiness 属性。

GetActivePoliciesByCustomer 过程中,列p.PolTypeLOB 别名为PolicyLineOfBusiness,但在GetPolicyDetailsByPolicyId 过程中,列p.PolTypeLOB 别名为PolicyType

所以,两个属性的 origin 列是相同的,我不明白原因, 但为避免第二个过程出现异常,您可以对其进行修改,同时添加PolicyLineOfBusiness 所需的输出:

CREATE PROCEDURE [GetPolicyDetailsByPolicyId]
@PolicyId UNIQUEIDENTIFIER
AS
    SET NOCOUNT ON;

    SELECT TOP 1
        p.PolId as PolicyId
        , p.CustId as CustomerId
        , p.PolNo as PolicyNumber
        , p.ShortPolNo as TrimmedPolicyNumber
        , p.PolEffDate as PolicyEffectiveDate
        , p.PolExpDate as PolicyExpirationDate
        , p.PolTypeLOB as PolicyType
        , p.PolTypeLOB as PolicyLineOfBusiness /* *** ADD THIS LINE  *** */
        , c.[Name] as InsuranceCompany
        , wc.[Name] as WritingCompany
        , p.BillMethod_PaymentPlan
        , p.FullTermPremium
        , e1.[LastName] as AccountExecutive
        , CONCAT(e.FirstName, ' ', e.LastName) as AccountRepresentative 
    From
        Policies p
        (Further details have been omitted as it is not important)

【讨论】:

【参考方案3】:

由于 PolicyLineOfBusiness 属性存在于您的域模型中。因此,如果您删除属性(未映射),两个 SP 都必须返回该属性值。但是通过删除该属性,您的第二个 SP 将给出异常,第一个 SP 将获取数据。所以这意味着第二个 SP 不想返回 PolicyLineOfBusiness 属性。

不幸的是,您无法通过删除域模型属性中的属性 (NotMapped) 来强制编写 PolicyLineOfBusiness 属性。因此,如果要保持域模型相同,则必须从第二个 SP 返回属性。

CREATE PROCEDURE [GetPolicyDetailsByPolicyId]
@PolicyId UNIQUEIDENTIFIER
AS
    SET NOCOUNT ON;

    SELECT TOP 1
        p.PolId as PolicyId
        , p.CustId as CustomerId
        , p.PolNo as PolicyNumber
        , p.ShortPolNo as TrimmedPolicyNumber
        , p.PolEffDate as PolicyEffectiveDate
        , p.PolExpDate as PolicyExpirationDate
        , p.PolTypeLOB as PolicyType
        , c.[Name] as InsuranceCompany
        , wc.[Name] as WritingCompany
        , p.BillMethod_PaymentPlan
        , p.FullTermPremium
        , "" As PolicyLineOfBusiness --Have to add this as the domain model contain this property
        , e1.[LastName] as AccountExecutive
        , CONCAT(e.FirstName, ' ', e.LastName) as AccountRepresentative 
    From
        Policies p
        (Further details have been omitted as it is not important)

如果您不想这样做,请为 2 个 SP 创建 2 个不同的模型。

【讨论】:

【参考方案4】:

对于单独的存储过程,您需要有单独的模型,因为两个存储过程中的列不同。

【讨论】:

以上是关于存储过程为列返回值,但 FromSqlRaw 为类属性返回 null的主要内容,如果未能解决你的问题,请参考以下文章

如何通过 FromSqlRaw 在 EF Core 3.0 中调用存储过程

如何在 c# 实体框架核心中使用 FromSqlRaw() 将 OUTPUT 参数发送到 MySql 存储过程

包含在FromSqlRaw和EF Core 3.1中的存储过程中

使用存储过程时出现 ASP.Net Core 3.1 MVC 错误

使用 EF 生成的 MySQL 在 LinQ 中为 FromSqlRaw 在从系统中选择变量时返回“SQL 语法错误”

实体框架“FromSqlRaw 或 FromSqlInterpolated 是用不可组合的 SQL 调用的,并且在它上面有一个查询。”返回对象的错误