【问题标题】:EFCore 2.1 Stored Procedure Loading Related DataEFCore 2.1 存储过程加载相关数据
【发布时间】:2018-07-25 20:06:52
【问题描述】:

我遇到了一个本应在 EFCore 2.1 中更正的问题。我知道这在 2.0 中是没有的。我的项目是 MVC Core 2.1 应用程序,EFCore 也是 2.1。

我在网上搜索了几种不同的表达问题的方式,但我得到的信息没有完成使代码正常工作所需的整个代码过程。

我有一个模型母亲,而母亲有一个邮寄地址和实际地址的子模型。我正在使用带有 FromSql 的存储过程,以便让我能够在我的过程中进行多个连接。存储过程在 Linq 中几乎是不可能写出来的。

我将包含我编写的符合我已经阅读的示例的所有内容。 请注意:我在代码中使用区域是因为我正在编写一个具有多个功能区域的系统。

模型 - Mother.cs

namespace Birth.Models
{
    public class Mother
    {

        [Key]
        public int MomId { get; set; }
        public string MomFirst { get; set; }
        public string MomLast { get; set; }

        //[ForeignKey("MomId")]
        public MotherAddress Physical { get; set; }

        //      public MotherAddress Mailing { get; set; }
    }

    //[Owned]
    public class MotherAddress
    {
        public string pType { get; set; }
        public string Street { get; set; }
        public string PreDir { get; set; }

        [Key]
        public int MomId { get; set; }

        public Mother Mother { get; set; }
    }
}

接口 - IbirthRepository.cs

public interface IBirthRepository
{
    IQueryable<Mother> Mothers { get; }
    IQueryable<MotherAddress> MotherAddresses { get; }
}

存储库 - BirthRepository.cs

public class BirthRepository : IBirthRepository
{
    private BirthDbContext context;
    public BirthRepository(BirthDbContext ctx)
    {
        context = ctx;
    }
    public IQueryable<Mother> Mothers => context.Mothers;
    public IQueryable<MotherAddress> MotherAddresses => context.MotherAddresses;
}

控制器 - GetMotherController.cs

namespace VSMaintenance.Areas.Birth.Controllers
{
    [Area("Birth")]
    [Route("Birth/GetMother")]
    public class GetMotherController : Controller
    {
        private BirthDbContext context;
        private IBirthRepository repository;
        public GetMotherController(BirthDbContext ctx, IBirthRepository repo)
        {
            repository = repo;
            context = ctx;
        }

        public IActionResult Load()
        {
            return View("Index");
        }

        [HttpPost]
        public async Task<ActionResult> Retrieve(Mother mother)
        {
            var gbd = new GetBirthData(repository, context);
            var mom = new Mother();
            mom = await gbd.GetMotherData(mother.MomId);
            return View("Mother", mom);
        }
    }
}

主视图 - Mother.cshtml

@model Birth.Models.Mother
@{
    ViewData["Title"] = "Mother";
}

<h2>Mother</h2>

@Html.DisplayNameFor(model => model.MomId)
@Html.DisplayFor(model => model.MomId)
<br />
@Html.DisplayNameFor(model => model.MomFirst)
@Html.DisplayFor(model => model.MomFirst)
<br />
@Html.DisplayNameFor(model => model.MomLast)
@Html.DisplayFor(model => model.MomLast)

<br /><br /><br />

@Html.RenderPartialAsync("Birth/_MotherAddress",  Model.Physical)
@*@Html.RenderPartialAsync("Birth/_MotherAddress", Model.Mailing)*@

部分视图 - MotherAddress.cshtml

@model Birth.Models.MotherAddress
<div class="container">
    <div class="row">
        <div class="col-sm-3">
            @Html.DisplayNameFor(model => model.pType)
        </div>
        <div class="col-sm-3">
            @Html.DisplayNameFor(model => model.Street)
        </div>
        <div class="col-sm-4">
            @Html.DisplayNameFor(model => model.PreDir)
        </div>
        <div class="col-sm-2">&nbsp;</div>
    </div>

    <div class="row">
        <div class="col-sm-3">
            @Html.TextBoxFor(model => model.pType, new { Style = "width:100%" })
        </div>
        <div class="col-sm-3">
            @Html.TextBoxFor(model => model.Street, new { Style = "width:100%" })
        </div>
        <div class="col-sm-4">
            @Html.TextBoxFor(model => model.PreDir, new { Style = "width:80%" })
        </div>
        <div class="col-sm-2">&nbsp;</div>
    </div>
</div>

Models.Data - GetBirthData.cs

namespace Birth.Models.Data
{
    public class GetBirthData
    {
        private IBirthRepository repo;
        pivate BirthDbContext _ctx;
        public GetBirthData(IBirthRepository repository, BirthDbContext context)
        {
            repo = repository;
            _ctx = context;
        }

        public async Task<Mother> GetMotherData(int MomId)
        {
            Mother list = new Mother();
            try
            {
                string query = "exec MAINTGetMotherAddresses {0}";
                list = await repo.Mothers
                    .FromSql(query, MomId)
                    .AsNoTracking()
                    .FirstOrDefaultAsync();
            }

            catch (Exception ex)
            {
                var msg = ex.Message;
            }
            return list;
        }
    }
}

DbContext - BirthDbContext.cs

namespace Birth.Models.Data
{
    public class BirthDbContext : DbContext
    {
        public BirthDbContext(DbContextOptions<BirthDbContext> options)
            :base(options) {
        }
            protected override void OnModelCreating(ModelBuilder modelBuilder)
            {
                modelBuilder.Entity<Mother>(b =>
                {
                    b.HasKey(e => e.MomId);
                    b.Property(e => e.MomId).ValueGeneratedNever();
                    //b.OwnsOne<MotherAddress>(e => e.Physical);
                    b.HasOne<MotherAddress>(e => e.Physical)
                        .WithOne(e => e.Mother)
                        .HasForeignKey<MotherAddress>(e => e.MomId);
                    b.ToTable("Table1");

                });
            }

        public DbSet<Mother> Mothers { get; set; }
        public DbSet<MotherAddress> MotherAddresses { get; set; }
    }
}

SQLServer 存储过程 - MAINTGetMotherAddresses

ALTER PROCEDURE MAINTGetMotherAddresses
    @MotherId NUMERIC(18,0)
AS
BEGIN

SELECT
    CAST(md.MOTHER_DETAIL_ID AS INT) AS MomId,
    md.FIRST_NAME AS MomFirst,
    md.LAST_NAME AS MomLast,

    'Physical' AS pType,
    maP.STREET_ADDRESS AS Street,--PhysicalStreet,
    maP.PRE_DIRECTION AS PreDir --PhysicalPreDir--,

    --'Mailing' AS MailingType,
    --maM.STREET_ADDRESS AS MailingStreet,
    --maM.PRE_DIRECTION AS MailingPreDir


    ,CAST(@MotherId AS INT) AS PhysicalMomId
    --,CAST(@MotherId AS INT) AS MailingMomId
    --,CAST(@MotherId AS INT) AS MotherAddressMomId

FROM dbo.MOTHER_DETAIL md
JOIN dbo.MOTHER_ADDRESS maP
    ON maP.MOTHER_DETAIL_ID = md.MOTHER_DETAIL_ID
JOIN dbo.MOTHER_ADDRESS maM
    ON maM.MOTHER_DETAIL_ID = md.MOTHER_DETAIL_ID

WHERE md.MOTHER_DETAIL_ID = @MotherId
    AND maP.ADDRESS_TYPE in (133, 176)
    AND maM.ADDRESS_TYPE IN (132, 175)


END

Mother_Detail 和 Mother_Address 表的字段比我在这里列出的要多得多,所以我不打算展示完整的表结构。如果您创建表并将地址添加到 Mother_Address 表,Address_Type 为 176,那么您将能够看到我当前尝试使用的结果集。我的愿望是再次加入该地址,如存储过程中所见,并在一个结果集中返回邮件和物理地址,并让系统使用这两个地址适当地填充 MotherAddress 模型。

请帮忙!这很令人沮丧。

兰迪

【问题讨论】:

标签: c# stored-procedures ef-core-2.1


【解决方案1】:
  1. 您的存储过程将获取 1 位妈妈的所有交叉连接数据,这是正确的 sql:

    SELECT  m.Whatever, physical.Whatever, mailing.Whatever
    FROM    Mother m
    CROSS APPLY
        (
        SELECT  TOP 1 a.Name, a.Whatever
        FROM    Address a
        WHERE   a.momId= m.Id and a.pType in (1,2,3)
        ) physical
    CROSS APPLY
        (
        SELECT  TOP 1 b.Name, b.Whatever
        FROM    Address b
        WHERE   b.momId= m.Id and b.pType in (4,5,6)
        ) mailing
    
  2. 您的 SP 无法将结果映射到您的模型的原因是:您的 SP 返回的视图在您的上下文中不存在。当前的 EF & Core 没有提供执行 Query SP 的好方法,只能用于插入、更新、删除。

    //call extension like this:
    _context.QueryStoredProcedureAsync<YourViewModel>("YourProcedureName", ("a", 1), ("b", 2), ("c", 3));
    
    //here is extension
    public static class ContextExtensions
    {
    public static async Task<List<T>> QueryStoredProcedureAsync<T>(
        this DbContext context, 
        string storedProcName, 
        params (string Key, object Value)[] args)//<==c# 7 new feature
    {
        using (var command = context.Database.GetDbConnection().CreateCommand())
        {
    
            command.CommandText = storedProcName;
            command.CommandType = System.Data.CommandType.StoredProcedure;
    
            foreach (var arg in args)
            {
                var param = command.CreateParameter();
                param.ParameterName = arg.Key;
                param.Value = arg.Value;
                command.Parameters.Add(param);
            }
            using (var reader = await command.ExecuteReaderAsync())
            {
                return reader.MapToList<T>();
            }
        }
    }
    
    private static List<T> MapToList<T>(this DbDataReader dr)
    {
        var result = new List<T>();
        var props = typeof(T).GetRuntimeProperties();
    
        var colMapping = dr.GetColumnSchema()
            .Where(x => props.Any(y => y.Name.ToLower() == x.ColumnName.ToLower()))
            .ToDictionary(key => key.ColumnName.ToLower());
    
        if (dr.HasRows)
        {
            while (dr.Read())
            {
                T obj = Activator.CreateInstance<T>();
                foreach (var prop in props)
                {
                    var val =
                        dr.GetValue(colMapping[prop.Name.ToLower()].ColumnOrdinal.Value);
                    prop.SetValue(obj, val == DBNull.Value ? null : val);
                }
                result.Add(obj);
            }
        }
        return result;
    }
    }
    
  3. 你的 SP 只是一个查询,我为你提供了 2 个替换它的选项:

    • 使用编译查询:https://docs.microsoft.com/en-us/dotnet/framework/data/adonet/ef/language-reference/compiled-queries-linq-to-entities
    • 用我的简单sultion去掉SP,SP不是为查询数据设计的,而是为处理数据设计的。

      public partial class Mother
      {
          public Mother()
          {
              MotherAddresses = new HashSet<MotherAddress>();
          }
      
          [Key, Column("ID")]
          public int Id { get; set; }
          [StringLength(50)]
          public string Name { get; set; }
      
          [InverseProperty("Mother")]
          public ICollection<MotherAddress> MotherAddresses { get; set; }
      }
      public partial class MotherAddress
      {
          [Key, Column(Order = 0)]
          public int MotherId { get; set; }
          [Key, Column(Order = 1)]
          public int AddressType { get; set; }
          [StringLength(50)]
          public string Address { get; set; }
      
          [ForeignKey("MotherId")]
          [InverseProperty("MotherAddress")]
          public Mother Mother { get; set; }
      }
      
      public enum AddressType
      {
          Physical,
          Mailing,
      }
      
      
      public static class MotherExtension
      {
          public static MotherAddress MailingAddress(this Mother mom)
          {
              return mom.Address(AddressType.Mailing);
          }
          public static MotherAddress PhysicalAddress(this Mother mom)
          {
              return mom.Address(AddressType.Physical);
          }
      
          public static MotherAddress Address(this Mother mom, AddressType addressType)
          {
              return mom.MotherAddresses.FirstOrDefault(x => x.AddressType == addressType);
          }
      }
      
      // here is in your DbContext
      protected override void OnModelCreating(ModelBuilder modelBuilder)
      {
          modelBuilder.Entity<MotherAddress>(entity =>
          {
              entity.HasKey(e => new { e.MotherId, e.AddressType });
      
              entity.HasOne(d => d.Mother)
                  .WithMany(p => p.MotherAddress)
                  .HasForeignKey(d => d.MotherId)
                  .OnDelete(DeleteBehavior.ClientSetNull)
                  .HasConstraintName("FK_MotherAddress_Mother");
          });
      
      
      }
      

      然后在你的 html 中:

      @Html.RenderPartialAsync("Birth/_MotherAddress",  Model.PhysicalAddress())
      @*@Html.RenderPartialAsync("Birth/_MotherAddress", Model.WhateverAddress())*@
      

【讨论】:

  • 我必须使用存储过程。这是我必须使用的真实查询的精简查询,它连接了 10 多个表。
  • 使用这些模型定义时,我收到以下错误:“使用 InversePropertyAttribute 和 ForeignKeyAttribute 指定了无效关系。实体类型“MotherAddress”中的导航“Mother”和实体中的导航“Addresses”类型 'Mother' 与 InversePropertyAttribute 相关,但为两个导航指定的 ForeignKeyAttribute 具有不同的值。”另外,我需要调用存储过程而不是使用 LINQ。
  • @RandyC 答案已更新。无论加入多少表,Linq 都能立即告诉您问题出在哪里。一个改进你的sql的好方法是:写linq,看看调试时翻译了什么sql。我的另一个建议是:尝试用 CompiledQuery 代替 SP,你会喜欢的。
  • 再一次,我不能使用 LINQ。问题是我们正在将现有系统升级到 MVC。有数百个现有存储过程,我们没有时间将其重写为 LINQ。使用存储过程的 EFCore 2.1 应该可以做到这一点,这就是我需要做的。
猜你喜欢
  • 2017-09-01
  • 2014-11-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-05-16
  • 1970-01-01
相关资源
最近更新 更多