多对多关联场景下插入Component模型记录时“IDENTITY_INSERT为OFF时无法插入标识列值”错误的解决求助
问题回顾
我在Component和Page两个模型之间搭了多对多关联,系统自动生成了中间表ComponentPage。结果插入关联指定Page的新Component记录时,碰上个头疼的错误:
InnerException = {"Cannot insert explicit value for identity column in table 'Components' when IDENTITY_INSERT is set to OFF."}
下面是我用到的相关代码:
创建Component对象的代码
List<Page> pages = new List<Page> { SelectedPage }; Componentobj = new Component { Pages = pages }; Componentobj = await _httpClientComponent.CreateOneJsonAsync("/api/Component", Componentobj);
Component模型定义
public class Component : Entity<int> { public string ParentName { get; set; } [Required(ErrorMessageResourceName = "RequiredError", ErrorMessageResourceType = typeof(Resources.ResourceComponent))] public string ComponentName { get; set; } public ICollection<Page> Pages { get; set; } public ICollection<ComponentsUserRoles> ComponentsUserRoles { get; set; } }
CreateOneJsonAsync方法实现(注:代码里实际叫GetOneJsonAsync)
public async ValueTask<T> GetOneJsonAsync(string url) { using HttpResponseMessage response = await _client.GetAsync(url); ValidateResponse(response); var content = await response.Content.ReadAsStringAsync(); return JsonSerializer.Deserialize<T>(content, new JsonSerializerOptions() { PropertyNameCaseInsensitive = true }); }
Add方法实现
public virtual async Task<T> Add(T obj, string createdBy= null) { obj.CreatedDate = DateTime.UtcNow; obj.CreatedBy = createdBy; var result = await entities.AddAsync(obj); await _context.SaveChangesAsync(); return result.Entity; }
问题分析与解决思路
我帮你拆解下问题根源,再给出具体的解决步骤:
1. 核心问题在哪?
首先,你直接把SelectedPage(大概率是从数据库查出来的带ID的实体)塞进Component的Pages集合里,EF会误以为你要修改这个Page实体,而不是仅仅建立关联。更关键的是——如果你的Component实体的Id(继承自Entity<int>)被意外设置了非默认值(比如不是0的整数),EF会尝试把这个显式ID插入到自增的Components.Id列,而此时数据库的IDENTITY_INSERT是关闭的,自然就报错了。
另外还有个明显的问题:你的CreateOneJsonAsync实际调用的是GetOneJsonAsync——这是GET请求啊!创建资源应该用POST请求,GET请求没法把实体数据正确传到后端,甚至可能反序列化时给Component的Id瞎赋值,直接触发错误。
2. 分步解决
(1)先把HTTP请求方法改对:用POST创建资源
赶紧把你的HTTP客户端方法改成POST版本,GET是用来查数据的,创建新实体必须用POST把数据放请求体里:
public async ValueTask<T> CreateOneJsonAsync(string url, T obj) { var jsonContent = JsonContent.Create(obj); using HttpResponseMessage response = await _client.PostAsync(url, jsonContent); ValidateResponse(response); var content = await response.Content.ReadAsStringAsync(); return JsonSerializer.Deserialize<T>(content, new JsonSerializerOptions() { PropertyNameCaseInsensitive = true }); }
确保前端调用的是这个POST方法,别再用之前的GET版本了。
(2)正确建立多对多关联:别传完整Page实体
不需要把整个SelectedPage都传进去,只要给个Id就行,还要告诉EF这个Page是已存在的,别去修改它:
// 只需要Page的Id,其他属性都不用 var pageReference = new Page { Id = SelectedPage.Id }; // 标记这个实体为Unchanged,让EF知道只做关联,不修改Page本身 _context.Entry(pageReference).State = EntityState.Unchanged; // 别忘了必填的ComponentName! Componentobj = new Component { ComponentName = "你的组件名称", Pages = new List<Page> { pageReference } }; Componentobj = await _httpClientComponent.CreateOneJsonAsync("/api/Component", Componentobj);
如果是在后端的Add方法里处理,也可以在方法内部遍历传入的Pages,把它们的状态都设为Unchanged,确保EF只创建中间表的关联记录,不会动Page表的数据。
(3)确保Component的Id没被显式赋值
检查下你的Entity<int>基类,确保Id的默认值是0(int的默认值),而且创建新Component实例时别手动给Id赋值。如果是前端反序列化时不小心带了Id字段,可以在模型上加[JsonIgnore]让序列化忽略Id:
public class Component : Entity<int> { [JsonIgnore] // 让JSON序列化/反序列化跳过Id字段 public override int Id { get; set; } // ...其他属性 }
或者在反序列化配置里忽略多余的字段:
return JsonSerializer.Deserialize<T>(content, new JsonSerializerOptions() { PropertyNameCaseInsensitive = true, IgnorePropertiesWithMissingMembers = true });
(4)临时开IDENTITY_INSERT(不推荐,除非特殊场景)
如果你真的需要手动给Component的Id赋值,可以在SaveChanges前临时开启IDENTITY_INSERT,但这绝对不是常规操作——自增列本来就该由数据库管,除非你有特殊需求:
public virtual async Task<T> Add(T obj, string createdBy= null) { obj.CreatedDate = DateTime.UtcNow; obj.CreatedBy = createdBy; if (obj.Id != 0) { _context.Database.ExecuteSqlRaw("SET IDENTITY_INSERT Components ON"); var result = await entities.AddAsync(obj); await _context.SaveChangesAsync(); _context.Database.ExecuteSqlRaw("SET IDENTITY_INSERT Components OFF"); return result.Entity; } else { var result = await entities.AddAsync(obj); await _context.SaveChangesAsync(); return result.Entity; } }
还是优先用前面三个方法,保持自增列的正常行为更稳妥。
内容的提问来源于stack exchange,提问作者Nb777

