【问题标题】:Trying to use Entity Framework method for multiple adds to database尝试使用实体框架方法多次添加到数据库
【发布时间】:2023-04-10 14:24:17
【问题描述】:

我有一个使用 SQL Server 后端的实体框架的 ASP.NET MVC 应用程序。

目标是通过 foreach 循环创建约 18 个 WPackage 条目:

foreach (var dbitem in dbCList)

该代码适用于单个 WPackage 条目,但我们有客户要求创建 300 多个 WPackage,因此尝试使用实体框架代码进行单个“添加”并循环创建 300 多个添加。

T-SQL 将非常具有挑战性,因为在创建时/在行创建时创建了许多键,因此对于活动 >> 资源,我们必须插入活动,抓取或记住活动键,然后添加具有新创建的活动密钥的资源。

每个WPackage(这是主父表)可以有以下一个或多个子表条目:

  • 1+ 活动
  • 每个活动都有 1 个以上的资源
  • 1+ 预算
  • 1+ 签名
  • 1+ 成本代码

我们的架构或模型图将是:

WPackage
--Activities
-----Resources (child of Activities)
--CostCodes
--Budgets
--Signatures

以下代码失败:

dbContextTransaction.Commit();

出现错误:

事务操作无法执行,因为此事务有待处理的请求。

[HttpPost]
public ActionResult Copy([Bind(Include = "ID,WBSID,...***fields excluded for brevity")] Package model)
{
    if (ModelState.IsValid)
    {
        try
        {
            using (var dbContextTransaction = db.Database.BeginTransaction())
            {
                var dbCList = db.Packages.Join(db.WBS,
   *expression omitted for brevity*)
                // this dbClist will build about 18 items in the collection for below loop

                foreach (var dbitem in dbCList)
                {
                    int testWPID = dbitem;

                    WPackage prvWP = db.WPackages.Find(dbitem);
    
                    int previousWPID = dbitem;

                    WPackage previousWP = db.WPackages.Find(dbitem);
                    model.ID = dbitem;
    
                    db.WPackages.Add(model);
                    db.SaveChanges();
                                
                    var budgets = db.Budgets.Where(i => i.WPID == previousWPID);

                    foreach (Budget budget in budgets)
                    {
                        budget.WPID = model.ID;
                        db.Budgets.Add(budget);
                    }
    
                    var costCodes = db.CostCodes.Where(i => i.WPID == previousWPID);

                    foreach (CostCode costCode in costCodes)
                    {
                        costCode.WPID = model.ID;                                
                        db.CostCodes.Add(costCode);
                    }
    
                    var activities = db.Activities.Where(i => i.WPID == previousWPID);
                    // *code excluded for brevity*
    
                    var previousActivityID = activity.ID;
    
                    db.Activities.Add(activity);
                    db.SaveChanges();
                                    
                    var resources = db.Resources.Where(i => i.ActivityID == previousActivityID);

                    foreach (Resource resource in resources)
                    {
                        resource.WPID = model.ID;                                    
                        resource.ActivityID = activity.ID;
                        resource.ActivityNumber = activity.ActivityNumber;
    
                        db.Resources.Add(resource);
                        db.SaveChanges();
                    }
                }

                var signatures = db.RolesAndSigs
                                   .Where(i => i.KeyId == previousWPID && i.Type == "WPL")
                                   .OrderBy(i => i.Role)
                                   .OrderBy(i => i.Person);

                foreach (RolesAndSig signature in signatures)
                {
                    db.RolesAndSigss.Add(signature);
                }
    
                db.SaveChanges();
                dbContextTransaction.Commit();
            }
        }
    }
}

我还尝试让Commit()foreach dbitem 循环之外运行,例如:

     db.SaveChanges();
     //dbContextTransaction.Commit();
 }
 dbContextTransaction.Commit();

...但这会返回以下错误:

[例外] 属性“ID”是对象的关键信息的一部分,不能修改。

【问题讨论】:

    标签: asp.net-mvc entity-framework


    【解决方案1】:

    您发布的代码存在一些没有意义的问题,并且可能没有按照您认为的那样做。您面临的问题的症结在于 Entity Framework 跟踪对其加载和关联的实体的所有引用:

    首先是这段代码:

    int testWPID = dbitem;
    WPackage prvWP = db.WPackages.Find(dbitem);
        
    int previousWPID = dbitem;
    WPackage previousWP = db.WPackages.Find(dbitem);
    

    prvWP 和previousWP 将指向完全相同的引用,而不是同一实体的两个副本。在更新检索到或与同一 ID 关联的任何其他引用时要小心。它们都指向同一个实例。如果您确实想要一个独立的快照参考,您可以使用AsNoTracking()

    接下来,当你在循环中做这样的事情时:

    model.ID = dbitem;
    db.WPackages.Add(model);
    

    在第一次迭代中,“模型”不是实体。它是具有包实体类型的反序列化数据块。只要您调用.Add(model),该引用现在将指向一个新跟踪的实体引用。在下一个循环中,您将告诉 EF 将该跟踪实体引用的 ID 更改为新值,这是非法的。

    您想要做的是为 18 次预期迭代中的每一次创建此模型的副本。为此,您想要做的更像是:

    foreach (var dbitem in dbCList)
    {
        var newModel = new WPackage 
        {
             ID = dbItem,
             WBSID = model.WBSID,
              /// copy across all relevant fields from the passed in model.
        };
    
    
        db.WPackages.Add(newModel);
    
    
       // ...
    }
    

    利用相关实体的导航属性而不是使用显式连接并尝试在具有多个 SaveChanges() 调用的显式事务中限定所有内容是非常值得的。 EF 可以自动管理所有 FK,而不是本质上将其用作单个 ADO CRUD 操作的包装器。

    您需要在“克隆”对象引用和“复制”引用之间明确。例如,如果我有一个客户有一个地址,而地址有一个国家参考,当我克隆一个客户时,我会想为那个客户克隆一个新的地址记录,但是确保复制国家参考。如果我在英格兰伦敦苹果街 123 号有 Jack 的记录,然后去克隆 Jack 在同一地址为 Jill 制作记录,他们现在可能在同一位置,但并非总是如此,所以我想要它们指向不同的地址记录,以防吉尔搬出去。尽管如此,“英格兰”应该只有一个记录。 (Jill 可能会搬到不同的国家,但她的地址记录只会指向不同的 Country Id)

    错误:

    var jill = context.Customers.Single(c => c.Name == "Jack");
    jill.Name = "Jill";
    context.Customers.Add(jill);
    

    这会尝试将 Jack 重命名为 Jill,然后“添加”已跟踪的实例,从而导致异常。

    会工作,但仍然错误:

    var jack = context.Customers.AsNoTracking().Single(c => c.Name == "Jack");
    var jill = jack;
    jill.Name = "Jill";
    context.Customers.Add(jill);
    

    这在技术上可以通过将 Jack 加载为未跟踪的实体来工作,并将 Jill 保存为具有新 ID 的新记录。然而,这可能非常令人困惑。根据 AddressId/Address 的引用方式,我们最终可能会看到 Jack 和 Jill 引用相同的单个地址记录。如果您希望 Jack 和 Jill 拥有不同的地址,那就不好了。

    对:

    var jack = context.Customers
        .Include(c => c.Address)
            .ThenInclude(a => a.Country)
        .Single(c => c.Name == "Jack");
    var jill = new Customer
    {
        Name = "Jill",
        // copy other fields...
        Address = new Address
        {
           StreetNumber = jack.Address.StreetNumber,
           StreetName = jack.Address.StreetName,
           Country = jack.Address.Country
        }
    };
    context.Customers.Add(jill);
    

    第一个细节是确保当我们加载 Jack 时,我们渴望加载所有我们想要克隆或复制引用的相关细节。然后我们为 Jill 创建一个新实例,复制 Jack 的值,包括设置一个新的 Address 记录。复制国家/地区参考,因为“英格兰”应该只有一条记录。

    编辑:如果您按年有一个包,那么对于类似翻转场景的情况,让我们使用下面的包类示例:

    public class Package
    {
        [Key] 
        public int PackageId { get; set; }
        [ForeignKey("PackageType")]
        public int PackageTypeId { get; set; }
        public int Year { get; set; }
    
        // .. More package related details and relationships...
    
        public virtual PackageType PackageType { get; set; }
    }
    

    目标可能是根据 2021 年的数据为 2022 年制作新的包和相关数据,并应用传入的视图模型中的任何更改。

    Find 是一个糟糕的选择,因为Find 想通过 PK 定位数据。如果您的方法只是传递要从中复制的实体(即 2021 年的数据),那么这可以工作,但是如果您已修改 2021 年的数据以表示您想要的 2022 年的值,这可能在代码中是危险或误导的. (我们不想更新 2021 年的数据,我们想为 2022 年创建一个新记录集)要为 2022 年创建一个新包,我们只需要更新的数据来构成该新项目,以及一种识别来源的方法使用什么作为模板。该标识可以是要从 (ProductId) 复制的行的 PK,也可以是从传入的数据派生的。(ProductTypeId 和 Year-1)在这两种情况下,如果我们要考虑与“复制自”产品相关的数据,那么谨慎的做法是在一次查询中急切加载相关数据,而不是重复返回数据库。 Find 无法适应。

    例如,如果我想传递数据来制作新产品,我会传递 ProductTypeId 和 Year 以及用于新结构的任何值。我可以尝试通过以下方式获取现有年份的副本以用作模板:

    var existingProduct = context.Products
        .Include(x => x.Activities) // Eager load related data.
        .Include(x => x.CostCodes)
        // ...
        .Single(x => x.ProductTypeId == productTypeId && x.Year = year - 1);
    

    或者如果我传递了 ProductId:(例如,如果我可以选择复制选定年份(如 2020 年)的数据)

    var existingProduct = context.Products
        .Include(x => x.Activities)
        .Include(x => x.CostCodes)
        // ...
        .Single(x => x.ProductId == copyFromProductId);
    

    这两个示例都希望找到一种,并且只有一种现有产品。如果请求带有无法找到行的值,则会出现应处理的异常。这将获取我们可以从中复制的所有现有产品信息,以及传递到方法中以创建新产品的任何数据。

    【讨论】:

    • 史蒂夫·派,非常感谢您的意见。可以忽略这些行,就像在初始测试期间一样: int testWPID = dbitem; WPackage prvWP = db.WPackages.Find(dbitem);我将在本周末或下周初查看您的代码并报告。再次感谢您的帮助!
    • OriginalValues 听起来像是 DbContext 本身中的某种代码逻辑,或者暴露了更改跟踪,即假设仅针对更新的实体调用所讨论的方法。关于翻转类型的场景,我将很快在上面的答案中添加一些内容,以涵盖对此的典型考虑。
    • 看起来使用新的模型方法有效。 OriginalValues 是由于需要提供非空字段值。异常错误并没有太大帮助,您只需要深入调试器并沿着层次结构向下查找最终导致问题的字段。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-08-14
    • 1970-01-01
    • 2022-10-14
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多