【问题标题】:Violation of PRIMARY KEY constraint Cannot insert duplicate key in object违反 PRIMARY KEY 约束不能在对象中插入重复键
【发布时间】:2020-09-16 19:26:03
【问题描述】:

我在使用延迟加载和virtual 时遇到问题,但是使用它时会产生以下错误

违反PRIMARY KEY约束不能在对象中插入重复键

我该如何解决?

这些是我的课程:

public class Producto
{
    [Key]
    public Guid ProductoId { get; set; }
    public Guid InquilinoId { get; set; }
    public string Nombre { get; set; }
    public decimal Precio_Publico { get; set; }
    public string Detalle_producto { get; set; }
    public DateTime? Fecha_publicacion { get; set; }
    public bool Activo { get; set; }    
    public string Img_Producto { get; set; }
    public string CodigoBarras { get; set; }

    public virtual Concepto VigenciaPrecio { get; set; }
    public virtual ICollection<Precio> Precios { get; set; }     

    public bool Es_Almacenable { get; set; }
    public int Dias_de_Garantia { get; set; }
    public bool Es_Importado { get; set; }

    public virtual List<Categoria> Categoria { get; set; } = new List<Categoria>();
    public virtual Impuesto Impuesto { get; set; }
    public virtual Precio Precio { get; set; }
}

public class Categoria
{
    public Guid CategoriaId { get; set; }       
    public string Nombre { get; set; }
    public virtual Producto Producto { get; set; }
}

[HttpPost]
public async Task<ActionResult<Guid>> Post(Producto producto)
{
    var user = await userManager.GetUserAsync(HttpContext.User);
    var usercontodo = context.Users.Where(x => x.Id == user.Id).Include(x => x.InquilinoActual).FirstOrDefault();

    if (!string.IsNullOrWhiteSpace(producto.Img_Producto))
    {
        var imgProducto = Convert.FromBase64String(producto.Img_Producto);
               
        producto.Img_Producto = await almacenadorDeArchivos.GuardarArchivo(imgProducto, "jpg", "productos");
    }
           
    producto.InquilinoId = usercontodo.InquilinoActual.ClienteId;         

    context.Add(producto);
    await context.SaveChangesAsync();

    return producto.ProductoId;
}

这是categorias的表格:

这是表productos

这是错误:

错误

Microsoft.EntityFrameworkCore.DbUpdateException:更新条目时出错。有关详细信息,请参阅内部异常。

Microsoft.Data.SqlClient.SqlException (0x80131904):违反 主键约束“PK_Categorias”。无法插入重复键 对象'dbo.Categorias'。重复键值为 (1737b24b-93a4-4ad9-4d8b-08d85a75ca8e)

【问题讨论】:

  • 将错误发布为文本,而不是图片。

标签: c# entity-framework blazor


【解决方案1】:

您的类意味着,使用标准约定,一个类别只能属于 1 个产品,而一个产品有多个类别。

您没有包含构建产品的代码(在发布之前),但错误意味着您尝试将现有类别添加到新产品。

我认为你想使用 Categoria 作为链接表,这应该或多或少的工作:

public class Categoria
{
    [Key]
    public Guid CategoriaId { get; set; }       
    [Key]
    public Guid ProductoId { get; set; }

    public string Nombre { get; set; }
    public virtual Producto Producto { get; set; }
}

但您可以通过将其映射到 DbContext 类的 OnModelCreating 中来获得更多控制权。

【讨论】:

    【解决方案2】:

    此错误通常是将分离的实体图或在客户端中构建的实体图传递到要添加/更新的服务器的结果。

    Producto 有一组类别。如果 EF 尝试持久化一个类别,这意味着您发送它并添加到上下文中的 Producto 中至少有一个类别。

    比如说,在我的客户端代码(JavaScript 或调用 API 的其他服务器代码)中,我创建了一个新产品:

    // C# Pseudo
    var producto = new Producto 
    {
       // set up product fields...
       // I have a list of categories pre-loaded, so I want to assign one of them...
       Categorias = (new [] { new Categoria { CategoriaId = 3, Nombre = "Three" }}).ToList()
    }
    

    现在我将该 Producto 传递给我的服务。该请求具有自己的范围 DbContext,我在 Producto 上设置了一些信息并将其添加到上下文的 Producto DbSet。我们可以假设数据库已经有一个 ID 为 3 的类别,但上下文不知道它,因为 Producto.Categorias 引用了一个恰好 ID 为 3 的新实体。EF 将处理它类别作为新实体并尝试将其与产品一起插入。因此,当 EF 尝试插入另一个类别 ID=3 时,FK 违规。

    复杂的解决方案是将实体附加到当前的 DbContext。举个简单的例子,上面的 Producto 进来了:

    foreach(var categoria in producto.Categorias)
        context.Attach(categoria); // which will treat the category as unmodified, but expect it to be an existing record.
    
    context.Producto.Add(producto);
    context.SaveChanges();
    

    必须为与产品关联和引用的每个现有实体执行此操作。这变得很复杂,因为它假设每个关联实体都不为 DbContext 所知。如果您有处理多个 Producto 对象的场景,或者 Producto 可以引用 Category 并且 Producto 下的另一个新行,或者 DbContext 在保存 Producto 之前可能已经读取了类别,如果 EF 已经在跟踪具有相同 ID 的类别,则尝试 Attach 类别可能会失败。这可能会导致间歇性错误。

    为了更安全,在附加实体之前,您应该测试 Context 是否已经在跟踪它。如果它已经在跟踪引用,那么您需要在 Producto 中替换引用:

    foreach(var categoria in producto.Categorias)
    {
        var existingCategoria = context.Categorias.Local.SingleOrDefault(x => x.CategoriaId == categoria.CategoriaId);
        if (existingCategoria != null)
        {   // Context is tracking one already, so replace the reference in Producto
            product.Categorias.Remove(categoria);
            product.Categorias.Add(existingCategoria);
        }
        else
           context.Attach(categoria); // context isn't tracking it yet.
    
    context.Producto.Add(producto);
    context.SaveChanges();
    

    同样,需要对每个引用都执行此操作以安全地保存分离的实体。

    更好的解决方案是避免在客户端和服务器之间传递实体结构,而是传递视图模型或 DTO,它们是 POCO(普通旧 C# 对象),仅包含构建实体所需的详细信息。

    给定这样的 Producto ViewModel:

    [Serializable]
    public class NewProductoViewModel
    {
        public string Nombre { get; set; }
        public ICollection<Guid> CategoriaIds { get; set; } = new List<Guid>();
        //  ... Other details needed to create a new Producto
    }
    

    当我们去添加这个新产品时:

    [HttpPost]
    public async Task<ActionResult<Guid>> Post(NewProductoViewModel viewModel)
    {
        var user = await userManager.GetUserAsync(HttpContext.User);
        var usercontodo = context.Users.Where(x => x.Id == user.Id).Include(x => x.InquilinoActual).FirstOrDefault();
    
        var producto = new Producto(); // Here is our new, fresh entity..
    
        // TODO: Here we would copy across all non-reference data, strings, values, etc. from the ViewModel to the Entity...
    
        if (!string.IsNullOrWhiteSpace(viewModle.Img_Producto))
        {
            var imgProducto = Convert.FromBase64String(viewModel.Img_Producto);
                   
            producto.Img_Producto = await almacenadorDeArchivos.GuardarArchivo(imgProducto, "jpg", "productos"); // This is fine, we get our object from the DbContext.
        }
               
        producto.InquilinoId = usercontodo.InquilinoActual.ClienteId;         
    
        // Now handle the Categorias....
        foreach(var categoriaId in viewModel.CategoriaIds)
        {
            var categoria = context.Categorias.Single(categoriaId);
            producto.Categorias.Add(categoria)
        }
    
        context.Add(producto);
        await context.SaveChangesAsync();
    
        return producto.ProductoId;
    }
    

    您的大部分代码几乎保持原样,但它接受的是反序列化视图模型,不要与可能与实体混淆的反序列化数据块混淆。该方法构造一个新实体,然后将复制视图模型中的任何细节,然后针对上下文执行查找以查找与新实体关联的任何引用。在上面的示例中,我使用了.Single(),如果我们传递了一个不存在的 CategoriaId,它将引发异常。或者,您可以使用 .SingleOrDefault() 并忽略不存在的 CategoriaId。

    使用 ViewModel 的额外好处是,您可以最大限度地减少发送到所需字段的数据量。我们不会将整个 Categoria 类发送到服务器,只是将关联的 ID 发送到服务器。大多数情况下,我看到人们传递实体,原因是避免多次重新加载实体。 (一旦读取类别以发送给客户端,然后再次保存 Producto)这个理由是有缺陷的,因为发送回服务器的内容可能“看起来”像服务器发送给客户端的实体,但是它不是一个实体。它是一个反序列化的 JSON 块,具有相同的实体签名。从某种意义上说,它也是“陈旧的”,即发送到服务器的任何数据都可能已有数分钟之久。更新实体时,您应该检查的第一件事是来自客户端的数据的行版本是否与服务器中的数据匹配。 (从那以后服务器数据是否已更新?)这意味着无论如何都要接触数据库。我们也不应该相信来自服务器的任何东西。 Attach 实体很诱人,设置修改状态并调用 SaveChanges 但这将覆盖该实体上的每个字段。聪明的人可以拦截来自他们浏览器的请求并修改序列化到请求中的数据,这意味着如果您仅附加该实体,则可以替换您不打算允许更新的数据。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2016-04-14
      • 1970-01-01
      • 1970-01-01
      • 2013-05-16
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多