问题
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