Entity Framework Core 8:更新数据时出现多对多关系问题

问题描述 投票:0回答:1

我正在使用代码优先迁移构建 Entity Framework Core Web 项目,当我通过更新方法更新数据时,我收到以下错误消息:

违反主键约束“PK_ApplicationUserLanguages”。无法在对象“dbo.ApplicationUserLanguages”中插入重复的键。重复的键值为 (29, a7314beb-fcbc-4e28-b331-6e2f5d3c44e8)。

这是我的数据结构:

public class Languages
{
     [Key]
     [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
     public int Id { get; set; }
     public string Name { get; set; }
     public Guid? UserId { get; set; }
     [JsonIgnore]
     public virtual List<ApplicationUser>? Users { get; set; }
}

public class ApplicationUser : IdentityUser
{
    public List<Languages>? Languages { get; set; }
}

这是我的

DbContext

public DbSet<Languages> Languages { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    base.OnModelCreating(modelBuilder);

    modelBuilder.Entity<Languages>().HasMany(l => l.Users).WithMany(u => u.Languages);

modelBuilder.Entity<Languages>().HasData(
   new Languages { Id = 1, Name = "English" },
   new Languages { Id = 2, Name = "Spanish" },
   new Languages { Id = 3, Name = "French" },
   new Languages { Id = 4, Name = "German" },
   new Languages { Id = 5, Name = "Italian" },
   new Languages { Id = 6, Name = "Portuguese" },
   new Languages { Id = 7, Name = "Dutch" },
   new Languages { Id = 8, Name = "Russian" },
   new Languages { Id = 9, Name = "Chinese" },
   new Languages { Id = 10, Name = "Japanese" },
   new Languages { Id = 11, Name = "Korean" },
   new Languages { Id = 12, Name = "Arabic" },
   new Languages { Id = 13, Name = "Hindi" },
   new Languages { Id = 14, Name = "Bengali" },
   new Languages { Id = 15, Name = "Urdu" },
   new Languages { Id = 16, Name = "Punjabi" },
   new Languages { Id = 17, Name = "Telugu" },
   new Languages { Id = 18, Name = "Marathi" },
   new Languages { Id = 19, Name = "Tamil" },
   new Languages { Id = 20, Name = "Gujarati" },
   new Languages { Id = 21, Name = "Kannada" },
   new Languages { Id = 22, Name = "Odia" },
   new Languages { Id = 23, Name = "Malayalam" },
   new Languages { Id = 24, Name = "Sindhi" },
   new Languages { Id = 25, Name = "Assamese" },
   new Languages { Id = 26, Name = "Nepali" },
   new Languages { Id = 27, Name = "Sanskrit" },
   new Languages { Id = 28, Name = "Sindhi" });
}

这是我更新数据库的代码:

var user = await _context.Users.FirstOrDefaultAsync(u => u.Id == updateProfile.Id);

if (user != null)
{
    user.Languages = updateProfile.Languages;

    _context.Entry(user).State = EntityState.Modified;
    await _context.SaveChangesAsync();
}

我尝试执行代码先删除语言,然后重新添加它们,但我不断收到错误。我不确定我错过了什么,因为我期待 Entity Framework Core 来管理外键

entity-framework-core ef-code-first ef-core-8.0
1个回答
0
投票

简短的回答是,您无法通过这种方式更新参考列表:

user.Languages = updateProfile.Languages;

当您想要更新拥有一组现有关系的一对多或多对多关系、对这些关系进行更改并想要保存更新后的关系时,使用 EF 就不那么简单了作为替换集合。

首先,为了避免尝试此操作的诱惑或错误,对于所有集合导航属性,请删除任何可访问的设置器。例如在用户中:

public virtual ICollection<Language> Languages { get; } = [];

这将立即突出显示代码中的问题操作。如果您有初始化集合的构造函数,则不再需要。如果您有任何初始化或覆盖集合的代码,则需要将其删除。

现在要更新集合,从数据库的角度来看它会有所帮助。虽然通过让数据库删除所有关联记录然后插入更新的关联来将集合设置为新集似乎是合理的,但 EF 中的更改跟踪却无法以这种方式工作。相反,您需要具体告诉 EF 需要删除和添加哪些项目,以便启用跟踪集合的代理可以中继该信息来构建 SQL 语句。如果您在实体上“设置”集合,则会对集合进行任何跟踪功能。

var user = await _context.Users
    .Include(u => u.Languages)
    .FirstOrDefaultAsync(u => u.Id == updateProfile.Id);

if (user == null) return;

var existingLanguageIds = user.Languages.Select(l => l.Id).ToList();
var updatedLanguageIds = updateProfile.Languages.Select(l => l.Id).ToList();

var languageIdsToRemove = existingLanguageIds.Except(updatedLanguageIds);
var languageIdsToAdd = updatedLanguageIds.Except(existingLanguageIds);

if(languageIdsToRemove.Any())
{
    var languagesToRemove = user.Languages.Where(l => languageIdsToRemove.Contains(l.Id));
    foreach(var language in languagesToRemove)
        user.Languages.Remove(language);
}

if(languageIdsToAdd.Any())
{
    var languagesToAdd = await _context.Languages
        .Where(l => languageidsToAdd.Contains(l.Id))
        .ToListAsync();
    foreach(var language in languagesToAdd)
        user.Languages.Add(language);
}

await _context.SaveChangesAsync();

基本上,我们会立即加载当前与用户关联的语言并识别要添加和删除的 ID,然后调整用户的跟踪语言集合。这样,我们不需要使用 updateProfile 发送分离的语言实体,我们可以只发送当前更新的 languageId 集。 (线路上的数据较少)

在传递分离的实体时,您可以要求

Attach()
添加这些实例,然后将它们关联到用户,而不是从
DbContext
获取它们,但获取它们会断言所有传递的语言都是实际的当前记录,然后再尝试关联它们,并且在将实体附加到 DbContext 之前,您应该始终检查 DbContext 上的本地跟踪缓存,以确保它尚未跟踪实例,否则您会遇到另一个令人讨厌的运行时异常情况。

© www.soinside.com 2019 - 2024. All rights reserved.