[英]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
.然后用户选择一个
PAYMETHOD
, SelectedPayMethod
。 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
时,一个是在调用Update
和SaveChanges
时),而其他人说我应该坚持在整个交易期间到一个上下文(即每个新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:但是,当我尝试在另一个使用相同
PAYMETHOD
的SALE
上调用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?由于我已经调用了
Update
和SaveChanges
,并实例化了SALE
和PAYMENT
的新实例,它不应该是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
是一个尚未保存到数据库的新实体。
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__1
和PAYMETHOD__2
。 Each instance is tracked by a different context, let's call them context__1
and context__2
.每个实例都由不同的上下文跟踪,我们称它们为
context__1
和context__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 检测到您在上下文中做错了什么并抛出异常,而不是让您的应用程序破坏您的数据库。
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.