我刚刚花了几天时间来发现一个由实体框架(版本 4.4.0.0)的一些奇怪行为引起的错误。为了解释,我写了一个小测试程序。最后,您会发现我对此有一些疑问。
宣言
这里我们有一个类“Test”,它代表我们的测试数据集。它只有一个 ID(主键)和一个“值”属性。在我们的 TestContext 中,我们实现了一个 DbSet 测试,它将我们的“测试”对象作为数据库表来处理。
public class Test
{
public int ID { get; set; }
public int value { get; set; }
}
public class TestContext : DbContext
{
public DbSet<Test> Tests { get; set; }
}
初始化
现在,我们从“测试”表中删除任何(如果存在)条目,并添加我们唯一的“测试”对象。它的 ID=1(主键)和值=10。
// Create a new DBContext...
TestContext db = new TestContext();
// Remove all entries...
foreach (Test t in db.Tests) db.Tests.Remove(t);
db.SaveChanges();
// Add one test entry...
db.Tests.Add(new Test { ID = 1, value = 10 });
db.SaveChanges();
测试
最后,我们运行一些测试。我们通过它的原始值 (=10) 选择我们的条目,并将我们的条目的“值”更改为 4711。但是,我们不调用 db.SaveChanges(); !!!
// Find our entry by it's value (=10)
var result = from r in db.Tests
where r.value == 10
select r;
Test t2 = result.FirstOrDefault();
// change its value from 10 to 4711...
t2.value = 4711;
现在,我们尝试通过原始值 (=10) 找到(旧)条目并对其结果进行一些测试。
// now we try to select it via its old value (==10)
var result2 = from r in db.Tests
where r.value == 10
select r;
// Did we get it?
if (result2.FirstOrDefault() != null && result2.FirstOrDefault().value == 4711)
{
Console.WriteLine("We found the changed entry by it's old value...");
}
运行程序时,我们实际上会看到“我们通过它的旧值找到了更改的条目......”。这意味着我们已经运行了 r.value == 10 的查询,发现了一些东西......这是可以接受的。但是,接收已经更改的对象(不满足值 == 10)!!!
注意:您将获得“where r.value == 4711”的空结果集。
在进一步的测试中,我们发现 Entity Framework 总是分发对同一个对象的引用。如果我们改变一个引用中的值,它也会在另一个引用中改变。好吧,没关系……但应该知道它会发生。
Test t3 = result2.FirstOrDefault();
t3.value = 42;
if (t2.value == 42)
{
Console.WriteLine("Seems as if we have a reference to the same object...");
}
概括
在同一数据库上下文上运行 LINQ 查询时(不调用 SaveChanges()),如果它具有相同的主键,我们将收到对同一对象的引用。奇怪的是:即使我们改变一个对象,我们会(仅!)通过它的旧值找到它。但是我们将收到对已更改对象的引用。这意味着对于自上次调用 SaveChanges() 以来我们更改的任何条目,我们的查询中的限制(值 == 10)不能保证。
问题
当然,我可能不得不在这里忍受一些影响。但我想避免在每次小改动后都使用“SaveChanges()”。特别是,因为我想将它用于事务处理......如果出现问题,能够恢复一些更改。
如果有人能回答我以下一个或两个问题,我会很高兴:
是否有可能改变实体框架的行为,就像在事务期间与普通数据库进行通信一样?如果是这样……怎么办?
回答“如何使用实体框架的上下文?”的好资源在哪里?它回答了诸如“我可以依靠什么?”之类的问题。和“如何选择我的 DBContext 对象的范围”?
编辑#1
Richard 刚刚解释了如何访问原始(未更改的)数据库值。虽然这很有价值和有帮助,但我有明确目标的冲动......
让我们看看使用 SQL 时会发生什么。我们设置了一个表“测试”:
CREATE TABLE Tests (ID INT, value INT, PRIMARY KEY(ID));
INSERT INTO Tests (ID, value) VALUES (1,10);
然后我们有一个事务,它首先查找值为 10 的实体。之后,我们更新这些条目的值并再次查找这些条目。在 SQL 中,我们已经在使用更新版本,因此我们不会为第二个查询找到任何结果。毕竟我们做了一个“回滚”,所以我们条目的值应该又是 10...
START TRANSACTION;
SELECT ID, value FROM Tests WHERE value=10; {1 result}
UPDATE Tests SET value=4711 WHERE ID=1; {our update}
SELECT ID, value FROM Tests WHERE value=10; {no result, as value is now 4711}
ROLLBACK; { just for testing transactions... }
我希望实体框架(EF)具有这种行为,其中 db.SaveChanges(); 等效于“COMMIT”,其中所有 LINQ 查询都等效于“SELECT”语句,并且对实体的每次写入访问都类似于“UPDATE”。我不关心 EF 何时实际调用 UPDATE 语句,但它的行为方式应该与直接使用 SQL 数据库的方式相同......当然,如果调用“SaveChanges()”并成功返回它应该保证所有数据都正确保存。
注意:是的,我可以在每次查询之前调用 db.SaveChanges(),但是我会失去“回滚”的可能性。
问候,
斯特凡