Violation of PRIMARY KEY constraint 'PK_XY'. Cannot insert duplicate key in object 'dbo.XY'

自作多情 提交于 2021-02-11 08:23:44

问题


Using EF Core, I have a Zone that can have multiple Sol (soils), same Sol can be attached to multiple 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);                
}

When adding my Sols to a Zone however I get the following exception:

Microsoft.EntityFrameworkCore.DbUpdateException: An error occurred while updating the entries. See the inner exception for details.

Microsoft.Data.SqlClient.SqlException (0x80131904): Violation of PRIMARY KEY constraint 'PK_SolZone'. Cannot insert duplicate key in object 'dbo.SolZone'. The duplicate key value is (1, 2).

Some details of implementation:

In my controller, when retrieving the object I do

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

where

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);
    }
}

before updating the object, I convert my ZoneDTO to Zone and then add it to the database:

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);
}

I use the base controller, that uses the BeforeUpdate, like this

[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();
    }
}

The repository code

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

I use AutoMapper

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

And the mapping is like this

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();

回答1:


When you are mapping from dto to entity, your FromDto method is giving you a Zone entity whose Sols list is not populated with the zone's existing sols. Its an empty list. So, when you are calling -

zone.Sols.Clear();

its doing nothing, and at database level, the zone still holds its sols. Then when you are re-populating the Sols list, you are trying to insert some previously existing Sol to the list.

You have to fetch the existing Zone along with its Sols list from the database, before clearing and repopulating it. How you can do that depends on how your repository is implemented.

On how to update many-to-many entity in EF 5.0, you can check this answer

EDIT :
Try modifying your base controller's Edit method as -

[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();
    }
}

Not related to your issue :
You are fetching one Sol a time inside a loop -

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

which is not efficient at all. Try something like -

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



回答2:


Since the relationship between SOL and Zone is a many to many relationship a Separate table containing the primary keys of both the tables will be created. A table definition is needed for this many to many relationship. Also Try including the UsingEnity to specify the entity used for defining the relationship

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



回答3:


It seems to me that you have many to many relationship. So you need to set up a new entity with primary key to be composite for mapping and to configure the entities the right way: (In EF Core up to and including 3.x, it is necessary to include an entity in the model to represent the join table, and then add navigation properties to either side of the many-to-many relations that point to the join entity instead:)

    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);
    }

The primary key for the join table is a composite key comprising both of the foreign key values. In addition, both sides of the many-to-many relationship are configured using the HasOne, WithMany and HasForeignKey Fluent API methods.

This is sufficient if you want to access Sol Zone data via the Sol or Zone entities. If you want to query SolZone data directly, you should also add a DbSet for it:

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

You can look up different relationships in EF Core here: https://docs.microsoft.com/en-us/ef/core/modeling/relationships?tabs=fluent-api%2Cfluent-api-simple-key%2Csimple-key



来源:https://stackoverflow.com/questions/66042438/violation-of-primary-key-constraint-pk-xy-cannot-insert-duplicate-key-in-obje

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!