EF Core正在运行我没有调用的查询。

huangapple go评论52阅读模式
英文:

EF Core running query's I did not call

问题

在我的Blazor Server项目中,我有以下两个models

Vergadering:

public class Vergadering
{
    [Key]
    public int Id { get; set; }
    public string Naam { get; set; }
    public DateTime DatumTijd { get; set; }
    public ICollection<Bestuurslid> Aanwezigen { get; set; }
    public string? Notulen { get; set; }
    public ICollection<Vergadering>? HoofdVergadering { get; set; }
    public ICollection<Vergadering>? GekoppeldeVergaderingen { get; set; }
    public ICollection<Bestand>? Bestanden { get; set; }
    public string? UserLastEditId { get; set; }
    public IdentityUser? UserLastEdit { get; set; }
    public DateTime? LastEdit { get; set; }
    public ICollection<VergaderingAgendaItem>? vergaderingAgendaItems { get; set; }
}

VergaderingAgendaItem:

public class VergaderingAgendaItem
{
    public int Id { get; set; }
    public string Omschrijving { get; set; }
    public bool Afgerond { get; set; }
    public int? ParentId { get; set; }
    public VergaderingAgendaItem? Parent { get; set; }
    public int VergaderingId { get; set; }
    public Vergadering Vergadering { get; set; }
    public string? UserAangedragenId { get; set; }
    public IdentityUser? UserAangedragen { get; set; }
}

这导致三个表:

  • Vergaderingen
  • VergaderingAgendaItems
  • VergaderingVergadering

在我的存储库中,我有以下的更新方法:

public async Task ChangeAfgerondStatusAsync(VergaderingAgendaItem item)
{
    using (var _db = _factory.CreateDbContext())
    {
        _db.VergaderingAgendaItems.Update(item);
        await _db.SaveChangesAsync();
    }
}

每当Vergadering没有GekoppeldeVergadering时,这个更新方法就不会有任何问题。

但是每当VergaderingGekoppeldeVergadering并且我更新该VergaderingVergaderingAgendaItem时,我会得到这个错误:

在保存实体更改的过程中发生错误。有关详细信息,请参阅内部异常。

查看运行项目时打开的命令提示符,我看到了以下查询和错误。

查询:

EF Core正在运行我没有调用的查询。

错误:

在保存上下文类型为'AVA_ZICHT.Data.ApplicationDbContext'的更改时,数据库中发生了异常。

Microsoft.EntityFrameworkCore.DbUpdateException: 在保存实体更改的过程中发生错误。有关详细信息,请参阅内部异常。

Microsoft.Data.SqlClient.SqlException (0x80131904): 违反主键约束'PK_VergaderingVergadering'。无法在对象'dbo.VergaderingVergadering'中插入重复键。重复键的值为(4, 3)。

为什么EF Core会尝试在VergaderingVergadering表中更新GekoppeldeVergadering?我的方法中明确写着VergaderingAgendaItem.Update()

英文:

I have the following two models within my Blazor Server project:

Vergadering:

public class Vergadering
{
    [Key]
    public int Id { get; set; }
    public string Naam { get; set; }
    public DateTime DatumTijd { get; set; }
    public ICollection&lt;Bestuurslid&gt; Aanwezigen { get; set; }
    public string? Notulen { get; set; }
    public ICollection&lt;Vergadering&gt;? HoofdVergadering { get; set; }
    public ICollection&lt;Vergadering&gt;? GekoppeldeVergaderingen { get; set; }
    public ICollection&lt;Bestand&gt;? Bestanden { get; set; }
    public string? UserLastEditId { get; set; }
    public IdentityUser? UserLastEdit { get; set; }
    public DateTime? LastEdit { get; set; }
    public ICollection&lt;VergaderingAgendaItem&gt;? vergaderingAgendaItems { get; set; }
}

VergaderingAgendaItem:

public class VergaderingAgendaItem
{
    public int Id { get; set; }
    public string Omschrijving { get; set; }
    public bool Afgerond { get; set; }
    public int? ParentId { get; set; }
    public VergaderingAgendaItem? Parent { get; set; }
    public int VergaderingId { get; set; }
    public Vergadering Vergadering { get; set; }
    public string? UserAangedragenId { get; set; }
    public IdentityUser? UserAangedragen { get; set; }

}

This results in three tables:

Vergaderingen
VergaderingAgendaItems
VergaderingVergadering

In my repository I have the following update method:

public async Task ChangeAfgerondStatusAsync(VergaderingAgendaItem item)
{
    using (var _db = _factory.CreateDbContext())
    {
        _db.VergaderingAgendaItems.Update(item);
        await _db.SaveChangesAsync();
    }
}

Whenever the Vergadering does not have a GekoppeldeVergadering this update method does not create any problem.

But whenever the Vergadering does have a GekoppeldeVergadering and I update a VergaderingAgendaItem of that Vergadering I get this error:

> An error occurred while saving the entity changes. See the inner exception for details.

Looking at the command prompt that opens up while running the project I saw the following query and error.

Queries:

EF Core正在运行我没有调用的查询。

Error:

> An exception occurred in the database while saving changes for context type 'AVA_ZICHT.Data.ApplicationDbContext'.
>
> Microsoft.EntityFrameworkCore.DbUpdateException: An error occurred while saving the entity changes. See the inner exception for details.
>
> Microsoft.Data.SqlClient.SqlException (0x80131904): Violation of PRIMARY KEY constraint 'PK_VergaderingVergadering'. Cannot insert duplicate key in object 'dbo.VergaderingVergadering'. The duplicate key value is (4, 3).

How is it that EF Core tries to update the GekoppeldeVergadering in VergaderingVergadering table. My method states VergaderingAgendaItem.Update()?

答案1

得分: 1

当传递一个分离的实体并告诉 EF 更新它时,EF 也会考虑任何相关联的实体。由于这些引用未由 DbContext 跟踪,上下文将视这些实体为新项目要插入。这可能导致重复键异常(正如您所看到的)或者插入具有新主键的重复数据,如果这些键被设置为标识列。

解决此问题的一种方法是使用配置为仅更新您希望更改的列的 Automapper:

public async Task ChangeAfgerondStatusAsync(VergaderingAgendaItem item)
{
    using (var _db = _factory.CreateDbContext())
    {
        var existingItem = _db.VergaderingAgendaItems.Single(x => x.Id == item.Id);
        Mapper.Map(item, existingItem);
        await _db.SaveChangesAsync();
    }
}

或者,可以通过手动将值从 item 复制到 existingItem 来执行此操作。existingItem 是跟踪的实体,所以一旦它被更新,只需调用 SaveChanges。与 Update 相比,这的优势在于生成的 UPDATE SQL 语句仅涉及实际更改的列,如果没有实际更改,则不会执行 UPDATE。

这假设我们只想从该实体复制字段,而不是子/关联实体。如果您想更改集合/关联,那么您需要急加载它们并分别处理它们。例如,更改 UserLastEdit 引用,这可能是您希望急加载的内容,以便可以使用当前用户记录进行更新。

我的一般建议是避免像这样处理分离的实体,而是使用 POCO 视图模型。使用分离的实体的麻烦之一是它们通常是实体状态的不完整表示,最坏的情况是从视图状态反序列化并转换为实体对象的东西。视图模型也可以缩减到客户端需要的数据和允许更改的数据。当数据返回到服务器时,就不会混淆它是什么与它所假装的是什么。在多用户系统中应用更新的另一个考虑因素是检测陈旧数据。像这样编写更新应用了“最后一次获胜”的方法,理想情况下,您应该检查当前数据库数据状态的并发令牌是否与该用户原始版本读取时的令牌/版本匹配。使用分离的实体的吸引力在于在执行更新时避免与数据库的往返,但老实说,您应该进行一次往返以确保记录实际上是有效的,用户实际上可以更新该记录,并且在此用户编辑时该记录尚未被其他人更新。

编辑:Automapper 示例:

var config = new MapperConfiguration(cfg => 
{
    cfg.CreateMap<VergaderingAgendaItem, VergaderingAgendaItem>()
        .ForMember(x => x.Id, opt => opt.Ignore());
});
var mapper = config.CreateMapper();

mapper.Map(item, existingItem);

通常情况下,您要么在应用程序启动时设置一个集中的 MapperConfiguration 并将映射器注册到需要它的控制器/服务的 IoC 容器中,或者可以创建 Mapper 工厂类/函数以根据需要构建和提供映射器。上面的示例代码还设置了一个映射器,它将复制所有数据,除了 Id 列。排除您永远不希望更改的任何属性是个好主意,这可以帮助防止在使用 Automapper 与手动复制值时意外更新。

英文:

When handed a detached entity and told to Update it, EF will consider any associated entities as well. Since those references aren't tracked by the DbContext, the context will see those entities as new items to be inserted. This can result in duplicate key exceptions (as you are seeing) or inserting duplicate data with new PKs if those keys are set up as Identity columns.

One way to get around this issue is to use Automapper configured to just update the columns you expect to change:

public async Task ChangeAfgerondStatusAsync(VergaderingAgendaItem item)
{
    using (var _db = _factory.CreateDbContext())
    {
        var existingItem = _db.VergaderingAgendaItems.Single(x =&gt; x.Id == item.Id);
        Mapper.Map(item, existingItem);
        await _db.SaveChangesAsync();
    }
}

Alternatively this can be done manually by copying values from item to existingItem. existingItem is tracked entity so once it's updated, just call SaveChanges. The advantage of this over Update is that the resulting UPDATE SQL statement will only be for any columns that have actually changed, and it won't execute an UPDATE if nothing has actually changed.

This assumes we only want to copy fields from that entity, and none of the child/related entities. If you want to alter the collections/associations then you will need to eager load them and handle these separately. For instance changing the UserLastEdit reference, this is likely something you would want to eager-load so that it can be updated with the current User record.

My general advice is to avoid working with detached entities for concerns like this and instead use POCO view models. The trouble with using detached entities is that these are often incomplete representations of entity state, at worst, something deserialized from view state and cast into an Entity object. View Models can also be scaled down to just the data your client needs and what data is allowed to change. When it gets back to the server there is no confusion about what it is vs. what it pretends to be. Another consideration of applying updates which is important in multi-user systems is detecting stale data. Writing updates like this applies a "last in wins" approach where you should ideally check that the current DB data state concurrency token matches the token/version at the time that this user's original version was read. The attraction of using detached entities is the thought of avoiding a round-trip to the DB when performing an update, but in all honesty you should justify a round trip to ensure that the record is actually valid, the user actually can update that record, and the record hasn't been updated by someone else in the time this user was editing it.

Edit: Automapper Example:

var config = new MapperConfiguration( cfg =&gt; 
{
    cfg.CreateMap&lt;VergaderingAgendaItem,VergaderingAgendaItem&gt;()
        .ForMember(x =&gt; x.Id, opt =&gt; opt.Ignore());
});
var mapper = config.CreateMapper();

mapper.Map(item, existingItem);

Now normally you would either have a centralized MapperConfiguration set up at the start of the application and register the mapper with the IoC container to be injected into the controllers/services that need it, or you can create Mapper Factory classes/functions to construct and provide mappers on demand. The above example code also sets up a mapper that will copy across all data except the Id column. It is a good idea to exclude any properties you never expect to be changed. This can help guard against unexpected updates when using Automapper vs. manually copying values across.

huangapple
  • 本文由 发表于 2023年2月19日 01:05:12
  • 转载请务必保留本文链接:https://go.coder-hub.com/75494947.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定