简体   繁体   English

试图理解为什么在这种情况下会发生 EF Core InvalidOperationException

[英]Trying to understand why an EF Core InvalidOperationException happens in this situation

I'm learning EF Core and I've hit an issue I can't quite understand why it happens:我正在学习 EF Core,我遇到了一个我不太明白为什么会发生的问题:

Right now I have a view PAYMENTSView with a datagrid bound to a List<PAYMETHOD> ACTIVEPAYMETHODs property located on PAYMENTSViewModel .现在我有一个视图PAYMENTSView ,其中的数据网格绑定到位于PAYMENTSViewModel上的List<PAYMETHOD> ACTIVEPAYMETHODs属性。

When I instantiate a new PAYMENTSView I also set a SALE CURRENT_SALE property on its viewmodel.当我实例化一个新的PAYMENTSView时,我还在其视图模型上设置了一个SALE CURRENT_SALE属性。 The user then selects a PAYMETHOD , SelectedPayMethod .然后用户选择一个PAYMETHODSelectedPayMethod Then it instantiates a new PAYMENT with PAYMENT.PAYMETHOD = SelectedPayMethod , and adds this new PAYMENT to CURRENT_SALE.PAYMENTs .然后它使用PAYMENT.PAYMETHOD = SelectedPayMethod实例化一个新的PAYMENT ,并将这个新的PAYMENT添加到CURRENT_SALE.PAYMENTs

Then it calls context.Update(CURRENT_SALE) and context.SaveChanges() ;然后调用context.Update(CURRENT_SALE)context.SaveChanges()

I've seen, at least here in SO, people saying that I shouldn't instantiate one DbContext for each access to the database (in this case one when filling PAYMENTSView and one when calling Update and SaveChanges ), while others say I should stick to one context during the whole transaction (ie one new DbContext for each new SALE ).我已经看到,至少在这里,有人说我不应该为每次访问数据库实例化一个DbContext (在这种情况下,一个是在填充PAYMENTSView时,一个是在调用UpdateSaveChanges时),而其他人说我应该坚持在整个交易期间到一个上下文(即每个新SALE一个新的 DbContext)。 Right now, I'm using multiple DbContext, so I should have any conflicts.现在,我正在使用多个 DbContext,所以我应该有任何冲突。

Now, to the issue: I can successfully instantiate one SALE , fill it, update and save it, alright.现在,问题:我可以成功实例化一个SALE ,填充它,更新并保存它,好的。 But when I try calling Update on another SALE that uses the same PAYMETHOD , I get this error:但是,当我尝试在另一个使用相同PAYMETHODSALE上调用Update时,出现此错误:

The instance of entity type 'PAYMETHOD' cannot be tracked because another instance of this type with the same key is already being tracked.无法跟踪实体类型“PAYMETHOD”的实例,因为已跟踪具有相同密钥的该类型的另一个实例。

Now, I understand this happens because PAYMETHOD is already being tracked due to being used by the first SALE .现在,我知道发生这种情况是因为PAYMETHOD由于被第一个SALE使用而已经被跟踪。 I can bypass this issue by, rather than setting PAYMENT.PAYMETHOD setting PAYMENT.PAYMETHOD_ID .我可以绕过这个问题,而不是设置PAYMENT.PAYMETHOD设置PAYMENT.PAYMETHOD_ID But I still want to understand why this happens.但我还是想明白为什么会这样。 Since I've already called Update and SaveChanges , as well as instantiating new instances of both SALE and PAYMENT , shouldn't it be a different instance of PAYMETHOD , and hence, should have different tracking?由于我已经调用了UpdateSaveChanges ,并实例化了SALEPAYMENT的新实例,它不应该是PAYMETHOD的不同实例,因此应该有不同的跟踪吗?

My relevant models are as follows:我的相关模型如下:

public class SALE
{
    public int ID {get;set;}
    public string PROPERTY1 {get;set;}
    public DateTime PROPERTY2 {get;set;}
    public List<PAYMENT> PAYMENTs {get;set;}
}

public class PAYMENT
{
    public int ID {get;set;}
    public string PROPERTY3 {get;set;}
    public int PROPERTY4 {get;set;}
    #region FK
    public SALE SALE_PAYMENT {get;set;}
    public int SALE_PAYMENT_ID {get;set;}
    public PAYMETHOD PAYMETHOD_PAYMENT {get;set;}
    public int PAYMETHOD_PAYMENT_ID {get;set;}
    #endregion
}

public PAYMETHOD
{
    public int ID {get;set}
    public string PROPERTY5 {get;set}
    public bool PROPERTY6 {get;set;}
    public List<PAYMENT> PAYMENTs {get;set;}
} 

Further information to reproduce it:重现它的更多信息:

    ...
    <DataGrid IsTabStop="False" Grid.RowSpan="6" Grid.Column="0" BorderThickness="2" Margin="10" AutoGenerateColumns="False" ItemsSource="{Binding ACTIVEPAYMETHODs}" IsReadOnly="True"/>
    ...
    <TextBox Text="{Binding PAYMETHODCODE,StringFormat=C2, UpdateSourceTrigger=PropertyChanged}" KeyDown="txb_KeyDown"/>
    ...
public partial class PAYMENTSView : Window
{
    private void txb_ValorPagto_KeyDown(object sender, KeyEventArgs e)
    {
        if (e.Key == Key.Enter)
            ((FECHAMENTOCUPOMViewModel)DataContext).ProcessPaymentMethod() == true    
    }
}

public class FECHAMENTOCUPOMViewModel
{
    public SALE CURRENT_SALE {get;set;}
    internal PAYMETHOD SelectedPayMethod {get;set;}
    public int PAYMETHODCODE
    {
        set
        {
            SelectedPayMethod = ACTIVEPAYMETHODs.Select(x=>x).Where(x=>x.ID == value).FirstOrDefault();
        }
    }
    
    public PAYMENTSViewModel
    {
        ACTIVEPAYMETHODs = new List<PAYMETHOD>(_context
                    .PAYMETHODs
                    .Select(x => x)
                    .Where(x =>
                        x.STATUS == Status.Active)
                    .ToArray()
                    );
    }
    public void ProcessaMetodoAtual()
    {
        PAYMENT payment = new PAYMENT();
        payment.PAYMETHOD_PAYMENT = SelectedPayMethod;
        CURRENT_SALE.PAYMENTs.Add(payment);
        SelectedPayMethod = null;
        SaveToDatabase();
    }
    
    public void SaveToDatabase()
    {
        DbContext _context = new DbContextFactory().CreateDbContext();
        _context.Update(CURRENT_SALE);
        try
        {
        _context.SaveChanges();
        }
        catch (Exception ex)
        {
        
        }
    }
}

EDIT: Here's the actual relevant code if anyone wants to give it a look (after @ArwynFr changes): https://github.com/AKANexus/EFCoreQuestion/tree/master/AmbiStore编辑:如果有人想看一下(@ArwynFr 更改后),这是实际的相关代码: https://github.com/AKANexus/EFCoreQuestion/tree/master/AmbiStore

EDIT2: I forgot a very important piece of information: CURRENT_SALE is a new entity that hasn't been saved to the database yet. EDIT2:我忘记了一条非常重要的信息: CURRENT_SALE是一个尚未保存到数据库的新实体。

Why it happens为什么会发生

Since I've already called Update and SaveChanges, as well as instantiating new instances of both SALE and PAYMENT, shouldn't it be a different instance of PAYMETHOD, and hence, should have different tracking?由于我已经调用了 Update 和 SaveChanges,并且实例化了 SALE 和 PAYMENT 的新实例,它不应该是 PAYMETHOD 的不同实例,因此应该有不同的跟踪吗?

It is a different instance of PAYMETHOD .它是PAYMETHOD的不同实例。

You have two entity instances, let's call them PAYMETHOD__1 and PAYMETHOD__2 .您有两个实体实例,我们称它们为PAYMETHOD__1PAYMETHOD__2 Each instance is tracked by a different context, let's call them context__1 and context__2 .每个实例都由不同的上下文跟踪,我们称它们为context__1context__2 However, the entities refer to the same database row, which means PAYMETHOD__1.Id == PAYMETHOD__2.Id .但是,实体引用相同的数据库行,这意味着PAYMETHOD__1.Id == PAYMETHOD__2.Id

This is a problem, because context__1 is able to alter the entry in the database without context__2 knowing.这是一个问题,因为context__1能够在context__2不知道的情况下更改数据库中的条目。 In that situation, context__2 will consider PAYMETHOD__2 to be clean, although the entity state is actually outdated and does not reflect changes made by context__1 in the database.在这种情况下, context__2将认为PAYMETHOD__2是干净的,尽管实体 state 实际上已经过时并且不反映context__1在数据库中所做的更改。 This can easily lead your application to alter your data in an inconsistent state.这很容易导致您的应用程序以不一致的方式更改您的数据 state。

A DbContext acts as an in-memory collection of entities, and it will keep track of changes you do on the entities in order to persist these changes to the database whenever you call SaveChanges() . DbContext 充当内存中的实体集合,它将跟踪您对实体所做的更改,以便在您调用SaveChanges()时将这些更改持久保存到数据库中。 You should always use a single instance of the context to handle, as long as you are operating in a coherent business transaction context.只要您在连贯的业务事务上下文中操作,就应该始终使用上下文的单个实例来处理。 EF core detects you are doing something wrong with your contexts and throws an exception, rather than letting your application breaking your database. EF Core 检测到您在上下文中做错了什么并抛出异常,而不是让您的应用程序破坏您的数据库。

What you are doing wrong and how to fix it你做错了什么以及如何解决

The first thing to note is that your code does a lot of mixing between models and viewmodels, and does not follow the principles of MVVM.首先要注意的是,您的代码在模型和视图模型之间做了很多混合,并且没有遵循 MVVM 的原则。 The most notably wrong thing you are doing is setting the current sale to your viewmodel from the view, and the having your viewmodel instantiate a new DbContext.您正在做的最明显的错误是从视图中将当前销售设置为您的视图模型,并让您的视图模型实例化一个新的 DbContext。

If your application is already manipulating entities, especially a SALE object when the FECHAMENTOCUPOMViewModel is instanciated, then you already have a DbContext and that context instance has to be provided to the viewmodel so it can manipulate entities in a coherent context.如果您的应用程序已经在操纵实体,尤其是 SALE object 实例化FECHAMENTOCUPOMViewModel时,那么您已经拥有一个 DbContext 并且必须向视图模型提供该上下文实例,以便它可以在连贯的上下文中操纵实体。 You also either need to provide the SALE object, or it's Id.您还需要提供 SALE object 或其 Id。 Since you set CURRENT_SALE , you already have the sale object so let's implement that:因为你设置了CURRENT_SALE ,所以你已经有了销售 object 所以让我们实现它:

public class FECHAMENTOCUPOMViewModel
{
    private readonly DbContext context;
    private readonly SALE model;

    public FECHAMENTOCUPOMViewModel(DbContext context, SALE model)
    {
        this.context = context ?? throw new ArgumentNullException(nameof(context));
        this.model = model ?? throw new ArgumentNullException(nameof(model));
    }
}

Now we need to obtain the list of pay methods the user will choose among.现在我们需要获取用户将要选择的支付方式列表。 Ideally, you should rather encapsulate your PAYMETHOD objects (models) into a PayMethodViewModel.理想情况下,您应该将 PAYMETHOD 对象(模型)封装到 PayMethodViewModel 中。

public class FECHAMENTOCUPOMViewModel
{
    public IEnumerable<PAYMETHOD> ACTIVEPAYMETHODs => context.PAYMETHODs
        .Where(method => method.STATUS == Status.Active)
        .ToArray();
}

Then let's handle the pay method selection by the user.然后让我们处理用户的支付方式选择。 Once again, this should rather be a viewmodel than an entity.再一次,这应该是一个视图模型而不是一个实体。

public class FECHAMENTOCUPOMViewModel
{
    private PAYMETHOD selectedPayMethod;
    public int PAYMETHODCODE
    {
        set
        {
            selectedPayMethod = ACTIVEPAYMETHODs
                .FirstOrDefault(method => method.Id == value);
        }
    }
}

Finally, lets add the payment:最后,让我们添加付款:

public class FECHAMENTOCUPOMViewModel
{
    public void ProcessaMetodoAtual()
    {
        PAYMENT payment = context.PAYMENTs.Add(new PAYMENT()
        {
            PAYMETHOD_PAYMENT = selectedPayMethod,
            SALE_PAYMENT = model // create the payment -> sale relationship
        }) as PAYMENT;
        model.PAYMENTs.Add(payment); // do not forget to add the inverse relationship
        context.SaveChanges(); // executes INSERT statement
    }
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM