在ASP .Net Core 2.1 Web API(带有MySQL数据库并使用Pomelo)中,当我在我的一个控制器操作中向数据库添加新实体时,如果API从消费客户端接收的实体具有主键中的值看起来好像EF Core正在尝试添加主键而不是允许数据库为其赋予新值。
所以...在数据库中,我有一个名为person
的表,它有一个名为id
的整数字段,它被设置为PRIMARY KEY和AUTO-INCREMENT。
模型:
public partial class Person
{
public int? Id { get; set; }
public string Name { get; set; }
public string Surname { get; set; }
}
的DbContext:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Person>(entity =>
{
entity.ToTable("person");
entity.HasKey(e => e.Id);
entity.Property(e => e.Id)
.HasColumnName("id")
.HasColumnType("int(11)");
entity.Property(e => e.Name)
.HasColumnName("name")
.HasColumnType("varchar(45)");
entity.Property(e => e.Surname)
.HasColumnName("surname")
.HasColumnType("varchar(45)");
}
}
控制器动作
// POST: api/Person
[HttpPost]
public async Task<IActionResult> AddPerson([FromBody]Person person)
{
if (!ModelState.IsValid)
return BadRequest(ModelState);
_context.Person.Add(person);
await _context.SaveChangesAsync();
return CreatedAtAction("GetPerson", new { id = person.Id }, person);
}
如果我在尝试将其插入数据库(即person.Id = null
)之前没有明确地清除该人的ID,那么我会得到一个例外,抱怨重复的主键。这是正常的EF Core行为吗?或者我做错了什么?
坦率地说,是的,你做错了什么。出于各种原因,您永远不应该将从用户输入创建的实例(即,将Person
实例传递到您的操作并从帖子的请求主体创建)直接保存到您的数据库。其中一个原因是它会对像EF这样的ORM造成严重破坏,这些ORM采用实体跟踪来优化查询。
简单地说,这里的Person
实例未被跟踪 - EF对此一无所知。然后使用Add
将其添加到您的上下文中,这表示EF开始将其作为新事物进行跟踪。当您稍后保存EF时,然后尽职地发出一个插入语句,但由于该插入中包含一个id,因此会出现主键冲突。你想要的是EF进行更新,但它不知道应该更新。
有技术可以解决这个问题的方法。例如,您可以使用Attach
而不是Add
。那只是盲目地告诉EF这是它应该追踪的东西,而不必传达它应该对它做任何事情。如果在跟踪后对此实例进行任何修改,EF会将其更改更改为“已修改”,并且最终会在保存时发出更新语句。但是,如果您没有进行任何更改,只是直接保存它,您还需要将其状态显式设置为“已修改”,否则EF将无需执行任何操作。好的一点是,如果您更改未跟踪实体的状态,那么EF会自动将其附加到跟踪所述状态,因此您无需手动执行Attach
。无论长短,只需将Add
替换为以下内容即可清除异常:
_context.Entry(person).State = EntityState.Modified;
但是,如果您尝试完全添加新人,则会导致问题。你在这里遇到的一个更大的问题是你有一个行动是双重职责。根据REST,POST不可重放,只能用于或幂等的资源。更简单地说,你只发布到像/api/person
这样的资源(而不是像/api/person/1
这样的东西,每次你这样做都应该创建一个新人。对于更新,你应该向该实际资源发出请求,即/api/person/1
和HTTP相反,动词应该是PUT。对同一资源的相同PUT请求将始终具有相同的结果,这是对特定资源的更新的情况。
除了理论之外,简单的一点是你应该有两个动作:
[HttpPost("")]
public async Task<IActionResult> AddPerson([FromBody]Person person)
[HttpPut("{id}")]
public async Task<IActionResult> UpdatePerson(int id, [FromBody]Person person)
最后,即使有了这一切,在进行更新时,直接保存人员参数会对用户产生过多的信任。最终用户可能无法使用更新修改任何数量的属性(例如,诸如“创建”日期之类的东西),但是当您执行此操作时它们可以。在某些方面更糟糕的是,即使用户不是恶意的,您仍然依靠它们发布该实体的所有数据。例如,如果你确实有一个创建的日期属性,但用户没有发布它们的更新(老实说,为什么你要发布创建的日期以及更新资源的请求),那么它将具有清除该财产。如果有一个默认值,它将被设置回,如果没有,如果列为NOT NULL,则实际上可能会在保存时出现异常。
无论长短,这都不是一个好主意。而是使用视图模型,DTO或类似的。该类应该只包含您希望允许用户首先修改甚至影响创建的属性。然后,对于更新的情况,您从数据库中提取新资源,并将param实例中的值映射到该数据库。最后,将数据库中的版本保存回数据库。这确保了1)用户无法修改您未明确允许的任何内容,2)用户只需要发布他们实际关心修改的内容,3)实体将被正确跟踪,EF将在保存时正确发布更新语句。