【问题标题】:Violation of PRIMARY KEY constraint 'PK_XY'. Cannot insert duplicate key in object 'dbo.XY'违反主键约束“PK_XY”。无法在对象“dbo.XY”中插入重复键
【发布时间】:2021-05-08 13:34:02
【问题描述】:

使用 EF Core,我有一个Zone,可以有多个Sol(土壤),相同的Sol 可以附加到多个Zone

public class Sol
{
    // ...  
    public ICollection<Zone> Zones { get; set; } = new List<Zone>();
}

public class Zone
{
    // ...
    public ICollection<Sol> Sols { get; set; } = new List<Sol>();        
}

public override void Configure(EntityTypeBuilder<Zone> builder)
{
    // ...
    builder
        .HasMany(p => p.Sols)
        .WithMany(p => p.Zones);                
}

将我的 Sols 添加到区域时,我得到以下异常:

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

Microsoft.Data.SqlClient.SqlException (0x80131904):违反主键约束“PK_SolZone”。无法在对象“dbo.SolZone”中插入重复键。重复键值为 (1, 2)。

一些实现细节:

在我的控制器中,当我检索对象时

public override async Task<IActionResult> Edit(int id, [FromQuery] bool fullView)
{
    var currentZone = _repository.SingleOrDefault(new GetZoneWithPaysAndSolsSpecification(id));

在哪里

public class GetZoneWithPaysAndSolsSpecification : Specification<Zone>
{
    public GetZoneWithPaysAndSolsSpecification(int zoneId)
    {
        Query.Where(p => p.Id == zoneId);
        Query.Include(p => p.Pays);
        Query.Include(p => p.Sols);
    }
}

在更新对象之前,我将我的 ZoneDTO 转换为 Zone,然后将其添加到数据库中:

protected override void BeforeUpdate(Zone zone, ZoneDTO sourceDto)
{
    zone.Sols.Clear();

    foreach (var solId in sourceDto.SolIds)
    {
        var sol = _repository.GetById<Sol>(solId);
        zone.Sols.Add(sol);
    }

    base.BeforeUpdate(zone, sourceDto);
}

我使用基本控制器,它使用BeforeUpdate,就像这样

[HttpPost]
[ValidateAntiForgeryToken]
public virtual async Task<IActionResult> Edit(TDto dto)
{
    try
    {
        var entity = FromDto(dto);
        BeforeUpdate(entity, dto);
        await _repository.UpdateAsync(entity);
        return RedirectToAction(nameof(Index));
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "when editing an object after submit");
        return PartialView();
    }
}

存储库代码

public Task UpdateAsync<T>(T entity) where T : BaseEntity
{
    _dbContext.Entry(entity).State = EntityState.Modified;
    return _dbContext.SaveChangesAsync();
}

我使用 AutoMapper

protected TBussinesModel FromDto(TDto dto)
{
    return _mapper.Map<TBussinesModel>(dto);
}

而且映射是这样的

CreateMap<Zone, ZoneDTO>()
    .ForMember(p => p.SolIds, o => o.MapFrom(p => p.Sols.Select(s => s.Id).ToArray()))
    .ForMember(p => p.SolNoms, o => o.MapFrom(p => p.Sols.Select(s => s.Nom).ToArray()))
    .ReverseMap();

【问题讨论】:

  • 从错误消息中,您在数据库中插入了重复键。
  • @Karney。我明白了,谢谢。我在 OP 中添加了更多细节,以便更好地阐明上下文
  • @Serge 你能分享FromDto方法的代码吗?
  • @atiyar 我添加了代码,不确定它是否与问题相关,无论如何它会很棒

标签: c# .net entity-framework-core ef-core-5.0


【解决方案1】:

当您从 dto 映射到实体时,您的 FromDto 方法会为您提供一个 Zone 实体,其 Sols 列表未填充区域的现有 sols。它是一个空列表。所以,当你打电话时 -

zone.Sols.Clear();

它什么都不做,在数据库级别,该区域仍然拥有它的 sols。然后,当您重新填充Sols 列表时,您会尝试将一些以前存在的Sol 插入到列表中。

您必须先从数据库中获取现有的Zone 及其Sols 列表,然后才能清除并重新填充它。如何做到这一点取决于您的存储库是如何实现的。

关于如何在 EF 5.0 中更新多对多实体,可以查看this answer

编辑:
尝试将基本控制器的 Edit 方法修改为 -

[HttpPost]
[ValidateAntiForgeryToken]
public virtual async Task<IActionResult> Edit(TDto dto)
{
    try
    {
        var zone = _repository.SingleOrDefault(new GetZoneWithPaysAndSolsSpecification(dto.Id));
        
        zone.Sols.Clear();
        
        foreach (var id in dto.SolIds)
        {
            var sol = _repository.GetById<Sol>(solId);
            zone.Sols.Add(sol);
        }

        await _repository.UpdateAsync(zone);
        return RedirectToAction(nameof(Index));
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "when editing an object after submit");
        return PartialView();
    }
}

与您的问题无关:
您在循环中一次获取一个 Sol -

foreach (var id in dto.SolIds)
{
    var sol = _repository.GetById<Sol>(solId);
    zone.Sols.Add(sol);
}

这根本没有效率。尝试类似 -

var sols = // fetch all the sols from the database
foreach (var id in dto.SolIds)
{
    zone.Sols.Add(sols.FirstOrDefault(p => p.Id == id));
}

【讨论】:

  • @Serge 请检查编辑部分。
  • 如果这样做,我会丢失从请求中获得的 DTO 的所有其他属性
  • @Serge 首先试试这个,看看你的问题是否得到解决。如果是这样,那么您可以从那里更新代码。
  • 我试过了,但现在它说:“System.InvalidOperationException:无法跟踪实体类型'Zone'的实例,因为已经在跟踪另一个具有键值'{Id:1}'的实例. 附加现有实体时,请确保仅附加一个具有给定键值的实体实例。"
【解决方案2】:

由于 SOL 和 Zone 之间的关系是多对多关系,因此将创建一个包含两个表的主键的单独表。 这种多对多关系需要一个表定义。也尝试包括 UsingEnity 来指定用于定义关系的实体

modelBuilder
    .Entity<Post>()
    .HasMany(p => p.SOL)
    .WithMany(p => p.ZONE)
    .UsingEntity(j => j.ToTable("SOL_ZONE"));

【讨论】:

  • 我没有指定表,所以它自动为我创建了表“SolZone”...默认表名对我来说没问题
【解决方案3】:

在我看来,你们有多对多的关系。因此,您需要设置一个具有主键的新实体以进行复合以进行映射并以正确的方式配置实体: (在 EF Core 直到并包括 3.x 中,需要在模型中包含一个实体来表示连接表,然后将导航属性添加到指向的多对多关系的任一侧而是加入实体:)

    public class Sol
    {
        // ...  
        public ICollection<SolZone> SolZones { get; set; } = new List<Zone>();
    }

    public class Zone
    {
        // ...
        public ICollection<SolZone> SolZones { get; set; } = new List<Sol>();        
    }

    public class SolZone
    {
        public int SolId { get; set; }
        public Sol Sol { get; set; }
        public int ZoneId { get; set; }
        public Zone Zone { get; set; }
    }

    // And in the OnModelCreating
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<SolZone>().HasKey(sc => new {sc.SolId , sc.ZoneId});

        modelBuilder.Entity<SolZone>()
             .HasOne<Sol>(s => s.Sol)
             .WithMany(sz => sz.SolZones)
             .HasForeignKey(s => s.SolId)`


        modelBuilder.Entity<SolZone>()
             .HasOne<Zone>(z => z.Zone)
             .WithMany(sz => sz.SolZones)
             .HasForeignKey(z => z.ZoneId);
    }

连接表的主键是包含两个外键值的复合键。此外,多对多关系的双方均使用 HasOne、WithMany 和 HasForeignKey Fluent API 方法进行配置。

如果您想通过 Sol 或 Zone 实体访问 Sol Zone 数据,这就足够了。如果你想直接查询 SolZone 数据,你还应该为其添加一个 DbSet:

public DbSet<SolZone> SolZones { get; set; }

您可以在此处查找 EF Core 中的不同关系:https://docs.microsoft.com/en-us/ef/core/modeling/relationships?tabs=fluent-api%2Cfluent-api-simple-key%2Csimple-key

【讨论】:

  • 所以我需要真正手动编写每个链接表的所有数据表结构,这会在业务代码中造成混乱......它真的变成了数据库对象,不再是业务了
  • 顺便说一句,我使用的是最新的 EF 5,可能那里的管理方式不同?
  • 嗯,“EF Core 5.0 支持多对多关系,无需显式映射连接表。”这是一个以不同方式管理的链接:docs.microsoft.com/en-us/ef/core/what-is-new/ef-core-5.0/…
  • 好的,那么您的解决方案对于以前版本的 ef 应该没问题... EF 5 自动为我创建了表
  • 可以,也不需要在 OnModelCreating 中添加配置。
猜你喜欢
  • 2017-06-09
  • 2019-04-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多