代码之家  ›  专栏  ›  技术社区  ›  Fabricio Rodriguez

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

  •  0
  • Fabricio Rodriguez  · 技术社区  · 7 年前

    在ASP。Net Core 2.1 Web API(使用MySQL数据库并使用Pomelo),当我在控制器的一个操作中向数据库添加新实体时,如果API从消费客户端接收到的实体在主键中有值,似乎EF Core正在尝试添加主键,而不是允许数据库给它一个新值。

    所以在数据库中,我有一个名为 person 它有一个整数字段,名为 id 设置为主键和自动递增。

    型号:

    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);
    }
    

    如果我在试图将其插入数据库之前没有明确清除此人的Id(即。 person.Id = null )然后我得到一个异常,抱怨主键重复。这是正常的EF核心行为吗?还是我做错了什么?

    1 回复  |  直到 7 年前
        1
  •  1
  •   Chris Pratt    7 年前

    坦白说,是的,你做错了什么。出于很多原因,您永远不应该保存从用户输入创建的实例(即 Person 实例被传递到您的操作中,并从post的请求正文中直接创建到您的数据库。其中一个原因是,它会对EF这样的ORMs造成严重破坏,后者使用实体跟踪来优化查询。

    简单地说,这 这里的实例是未追踪的——EF对此一无所知。然后使用 Add 将其添加到您的上下文中,这表明EF开始将其作为新事物进行跟踪。稍后保存EF时,会尽职尽责地发出insert语句,但由于该insert中包含id,因此会出现主键冲突。你想要的是EF做一个更新,但它不知道应该做什么。

    有很多方法可以从技术上解决这个问题。例如,你可以使用 Attach 而不是 添加 。这只是盲目地告诉EF,这是它应该跟踪的事情,而不必传达它应该用它做任何事情。如果在跟踪该实例后对其进行任何修改,EF会将其更改更改为“modified”,并在保存时发出update语句。然而,如果你 进行任何更改,但只是直接保存,还需要显式地将其状态设置为“已修改”,否则EF基本上什么都不做。很好的一点是,如果您更改了未跟踪实体的状态,EF会自动将其附加到跟踪所述状态,因此您不需要这样做 贴上 手动。无论长短,只要替换 添加 符合:

    _context.Entry(person).State = EntityState.Modified;
    

    然而,如果你试图添加一个完全不同的人,那就会产生问题。这里有一个更大的问题是,你有一个行动,承担双重责任。根据REST,POST是不可替换的,只应被制作成包含或幂等元的资源。更简单地说,你只发布到一个资源,比如 /api/person (而不是类似于 /api/person/1 每次你这么做 应该创造一个新的人 。对于更新,您应该向该实际资源发出请求,即 /api/人/1 而HTTP动词应该放在这里。对同一资源的相同PUT请求将始终具有相同的结果,这是对特定资源进行更新的情况。

    撇开理论不谈,简单的一点是你应该有两个行动:

    [HttpPost("")]
    public async Task<IActionResult> AddPerson([FromBody]Person person)
    
    [HttpPut("{id}")]
    public async Task<IActionResult> UpdatePerson(int id, [FromBody]Person person)
    

    最后,即使如此,在进行更新时,保存person参数直接会让用户过于信任。最终用户可能无法通过更新修改任意数量的属性(例如“创建”日期),但当您这样做时,它们可以修改。在某些方面,更糟糕的是,即使用户不是恶意的,你仍然依赖他们发布帖子 全部的 该实体的数据。例如,如果您确实创建了日期属性,但用户没有在更新时发布该属性(老实说,为什么 您发布了一个创建的日期以及一个更新资源的请求),那么它将具有清除该属性的效果。如果有默认值,它将被设置回该值,如果没有,如果列不为空,您可能会在保存时实际收到异常。

    长话短说,这不是个好主意。相反,请使用视图模型、DTO或类似工具。该类应该只包含您想让用户在创建时修改甚至影响的属性。然后,在更新的情况下,从数据库中提取新的资源,并将param实例中的值映射到该数据库上。最后,将版本从数据库保存回数据库。这确保了1)用户不能修改您不明确允许的任何内容,2)用户只需发布他们真正关心修改的内容,3)实体将被正确跟踪,EF将在保存时正确发出update语句。

    推荐文章