使用Entity Framework添加导航属性时出现主键重复异常的问题求助
问题场景与代码
我定义了以下实体类:
public abstract class Entity { public Guid Id { get; set; } public Entity() { Id = Guid.NewGuid(); } } public class Account : Entity { public Account() { } public Account(string name, Integration integration, User creator, List<User> usersAccessRights) { Name = name; IsActive = integration.HasSuccessfullyConnected; CreatedAt = DateTimeOffset.Now.ToUnixTimeSeconds(); Integration = integration; Creator = creator; Users.AddRange(usersAccessRights); } public string Name { get; private set; } public long CreatedAt { get; protected set; } public bool IsActive { get; protected set; } public Guid CreatedBy { get; protected set; } public virtual User Creator { get; set; } public Guid IntegrationId { get; private set; } public virtual Integration Integration { get; private set; } public virtual List<User> Users { get; private set; } = new List<User>(); } public class User : Entity { public User() { } public User(string name, string email, string phone, string password) { Name = name; Email = email; Phone = phone; Password = password; } public string Name { get; private set; } public string Email { get; private set; } public string Phone { get; private set; } public string Password { get; private set; } public virtual List<Account> Accounts { get; set; } = new List<Account>(); } public class Integration : Entity { public Integration() { } protected Integration(string name, bool hasSuccessfullyConnected) { Name = name; IsActive = HasSuccessfullyConnected = hasSuccessfullyConnected; CreatedAt = DateTimeOffset.Now.ToUnixTimeSeconds(); } public string Name { get; protected set; } public long CreatedAt { get; protected set; } public bool IsActive { get; protected set; } public bool HasSuccessfullyConnected { get; protected set; } public string Type { get; protected set; } public virtual string IntegrationType() => throw new NotImplementedException(); }
仓库方法:
private TEntity AddEntity(TEntity entity) { _context.Set<TEntity>().Add(entity); _context.SaveChanges(); } public virtual TEntity GetById(Guid id) { return _context.Set<TEntity>().Find(id); }
在创建Account时,我先查询到Creator User和有权限的User列表,然后构造Account并添加:
var account = CreateAccount(createAccountRequest.Name, integration, user, usersWithAccess); var createAccountResult = _accountRepository.Create(account);
结果触发了以下异常:
SqlException: Violation of PRIMARY KEY constraint 'PK_User'. Cannot insert duplicate key in object 'dbo.User'. The duplicate key value is (3c8a0035-4bc0-4001-bfa9-498d182a7f36). The statement has been terminated.
明明只是关联已存在的User,却被EF当成新用户插入,尝试了多种方法都没解决,直到调整了操作顺序:
var createAccountResult = _accountRepository.Create(account); _accountRepository.Commit(); account.Creator = creatorResult.Entity; account.Users.AddRange(usersAccessRights); _accountRepository.Commit();
这次就正常工作了。
问题原因分析
出现这个异常的核心原因是EF上下文对关联的User实体的追踪状态问题:
当你通过GetById查询到User后,如果这些User没有被当前上下文追踪(比如之前用了AsNoTracking(),或者上下文实例是新创建的),那么当你把这些User关联到新的Account上,再调用Add方法时,EF会遍历整个实体图,把所有未被追踪的关联实体都标记为Added状态——也就是说EF认为这些User是新的,要插入到数据库,自然就和已存在的主键冲突了。
你的解决方案为啥有效?
调整后的操作顺序,先把Account单独保存到数据库,此时Account已经被上下文追踪;之后再关联已查询的User,EF这时候只会识别出需要建立Account和User之间的关联关系(更新两者的中间关联表),而不会把User当成新实体插入,因此避免了主键重复的问题。
其他可行的解决方法
除了调整操作顺序,你还可以尝试这两种方案:
- 确保User被上下文追踪:检查你的查询逻辑,
Find方法默认是会追踪实体的,但如果查询时使用了AsNoTracking(),会取消追踪。如果是这种情况,移除AsNoTracking()即可。 - 显式标记User为已存在状态:如果必须在构造Account时就关联User,可以在添加Account之前,手动告诉EF这些User是已存在的,不需要插入:
// 在调用AddEntity之前添加这段代码 _context.Entry(creator).State = EntityState.Unchanged; foreach (var user in usersWithAccess) { _context.Entry(user).State = EntityState.Unchanged; }
这样EF就会将这些User标记为Unchanged状态,只会处理Account的新增和关联关系,不会尝试插入重复的User。
备注:内容来源于stack exchange,提问作者Vitor Archanjo

