EF Core 2.1在添加到DbContext并保存时尝试在INSERT查询中包含主键字段

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

在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行为吗?或者我做错了什么?

dbcontext asp.net-core-2.1 ef-core-2.1 .net-core-2.1
1个回答
0
投票

坦率地说,是的,你做错了什么。出于各种原因,您永远不应该将从用户输入创建的实例(即,将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将在保存时正确发布更新语句。

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