C# 实体框架添加具有相关子项的新项

C# 实体框架添加具有相关子项的新项,c#,entity-framework,C#,Entity Framework,我尝试添加包含子项的项时出错 当identity\u insert设置为OFF时,无法在表“Category”中为identity列插入显式值 我的班级: public class Product { public long Id { get; set; } public string Name { get; set; } public string Description { get; set; } public Category Category { get;

我尝试添加包含子项的项时出错

当identity\u insert设置为OFF时,无法在表“Category”中为identity列插入显式值

我的班级:

public class Product
{
    public long Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public Category Category { get; set; }
}

public class Category
{
    public long Id { get; set; }
    public string CatName { get; set; }
}
当我添加新的
产品时,如下所示:

var p = new Product();
p.Name = "test";
p.Description = "test 1";

var c = new Category();
c.Id = 2;
c.CatName = "Demo";
p.Category = c;

_context.Product.Add(p);
数据库中已存在该类别(CatName=Demo,Id=2)

我得到一个错误:

当identity\u insert设置为OFF时,无法在表“Category”中为identity列插入显式值

我不想添加新的
类别
。当我从数据库中读取
类别
时,它运行良好

var p = new Product();
p.Name = "test";
p.Description = "test 1";
p.Category = _context.Category.Where(c => c.Id.Equals(2)).FirstOrDefault();

_context.Product.Add(p);
但我不想阅读所有(还有更多)子属性


我做错了什么?

当您将一个上下文作为一个对象引用交给一个它没有跟踪的对象引用作为添加操作的一部分时,它会将每个它没有跟踪的对象引用都作为一个添加的实体来处理。引用是EF中的一切

上下文没有跟踪您的类别,那么它如何知道ID为2的类别对象是指向现有类别还是指向新类别

如果可以保证数据库中会存在类别和您想要的所有其他引用,则可以检查上下文的本地缓存中的实例(如果找到,则使用这些实例),否则会附加类别。例如:

var c = _context.Categories.Local.SingleOrDefault(c => c.Id == 2); // Checks cache, doesn't go to DB.
if(c == null)
{
    c = new Category { Id = 2 }; // Only need PK to associate an entity.
    _context.Categories.Attach(c);
}

var p = new Product()
{ 
    Name = "test",
    Description = "test 1",
    Category = c
};

_context.Product.Add(p);
无论哪种方式,您都需要在跟踪一个或多个引用实体的事件中检查上下文。试图通过创建和附加引用来简化操作可能会导致间歇性错误,尤其是在处理较大的操作或实体集时。例如,您可能测试或通常处理几个使用不同类别且工作正常的实体,但是如果使用共享相同类别的两个产品调用,则第二个产品添加将失败,因为DbContext已经在跟踪具有相同ID的类别

转到上下文加载相关实体并不是一个昂贵的负担,它有助于确保建议的更新有效。通过ID获取引用非常快,DbContext在进入数据库之前会先查看它的本地缓存。即使在您可能需要将多个产品更新为不同类别的情况下,也可以对其进行优化。例如,如果我有一个产品视图模型列表,其中有一个要关联的类别:(一些可能具有相同的类别,或者任意数量的不同类别)


我们不必在一个循环或类似的循环中逐个获取数据。通过检查我们可能需要的数据并获取我们感兴趣的ID,可以在对DB的一次调用中加载它。但是,建议在处理引用时,您希望使用
Single
等方法,而不是
SingleOrDefault
FirstOrDefault
,以便明确您希望在何处找到实体
Single
表示“我希望找到一条符合此条件的记录”,因此,如果它没有找到一条记录,或者碰巧找到了多条记录,则此时会出现一个有意义的异常
FirstOrDefault
如果找到多行,则不会引发异常,它只会选择遇到的第一行,如果没有找到记录,则不会引发异常。稍后可能会发生异常,代码假定找到了引用并尝试访问#null上的属性<在第一个示例中,当检查本地缓存时,code>SingleOrDefault
适用,因为我想处理记录在缓存中被跟踪或显式未被跟踪的可能性。

如果您提供自己的值(标识),则需要启用
IDENTITY\u INSERT
或者不要提供
id
,因为它会为您提供。另一个选项是只运行一个查询来为您执行此操作:
SET IDENTITY\u INSERT Category ON
;您可能还需要更新您的标识模型。如果要将新产品实体连接到现有类别,只需设置
CategoryId
外键值(您应该在
产品
类上设置该值)到现有类别的
Id
——这将是最容易实现的方法this@marc_s:我尝试使用“p.Category.Id=2”,但它会引发相同的错误:(您正在插入一个已经存在的Id=2。请将其设为0(零),以便EF了解这是一个新的CategoryUse\u context.Product.Update(p);而是…它将添加产品而不是类别。。。
class ProductViewModel
{
     string Name { get; set; }
     string Description { get; set; }
     int CategoryId { get; set; } 
     // ...
}

var categoryIds = productViewModels.Select(p => p.CategoryId).Distinct();
var categories = _context.Categories.Where(c => categoryIds.Contains(c.Id)).ToList();

foreach(var productVM in productViewModels)
{
    var c = categories.Single(c => c.Id == productVM.CategoryId);
    var p = new Product()
    { 
        Name = productVM.Name,
        Description = productVM.Description,
        Category = c
    };
    
    _context.Product.Add(p);
}