1

我刚刚花了几天时间来发现一个由实体框架(版本 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()”。特别是,因为我想将它用于事务处理......如果出现问题,能够恢复一些更改。

如果有人能回答我以下一个或两个问题,我会很高兴:

  1. 是否有可能改变实体框架的行为,就像在事务期间与普通数据库进行通信一样?如果是这样……怎么办?

  2. 回答“如何使用实体框架的上下文?”的好资源在哪里?它回答了诸如“我可以依靠什么?”之类的问题。和“如何选择我的 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(),但是我会失去“回滚”的可能性。

问候,

斯特凡

4

3 回答 3

3

正如您所发现的,Entity Framework 跟踪它已加载的实体,并为访问同一实体的每个查询返回相同的引用。这意味着从您的查询返回的数据与数据的当前内存版本匹配,而不一定与数据库中的数据匹配。

如果您需要访问数据库值,您有几个选择:

  1. 使用 newDbContext加载实体;
  2. 用于.AsNoTracking()加载您的实体的未跟踪副本;
  3. 用于context.Entry(entity).GetDatabaseValues()从数据库加载属性值;

如果你想用数据库中的值覆盖本地实体的属性,你需要调用context.Entry(entity).Reload().

于 2012-10-30T13:29:34.563 回答
2

您可以将更新包装在事务中以实现与 SQL 示例中相同的结果:

using (var transaction = new TransactionScope())
{

    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;
    // send UPDATE to Database but don't commit transcation
    db.SaveChanges();

    var result2 = from r in db.Tests
                    where r.value == 10
                    select r;
    // should not return anything
    Trace.Assert(result2.Count() == 0);

    // This way you can commit the transaction:
    // transaction.Complete();

    // but we do nothing and after this line, the transaction is rolled back
}

有关详细信息,请参阅http://msdn.microsoft.com/en-us/library/bb896325(v=vs.100).aspx

于 2012-10-30T15:57:43.893 回答
1

我认为你的问题是表达式树。SaveChanges()正如您已经提到的,当您说 时,实体框架会执行您对数据库的查询。在上下文中操作某些内容时,更改不会发生在数据库上,而是发生在您的物理内存中。就在您调用时,SaveChanges()您的操作将被翻译为 SQL。

当您执行一个简单select的操作时,数据库会在您访问数据的那一刻进行查询。因此,如果您没有调用SaveChanges(),它会使用(SQL)在数据库中找到数据集,SELECT* FROM Test WHERE VALUE = 10但从表达式树中解释它必须是value == 4711

EF 中的事务正在您的存储中进行。你之前所做的一切都是你SaveChanges()的交易。阅读更多信息:MSDN

一个非常好的资源,可能是最新的,关于 EF 的信息是Microsoft Data Developer Center

于 2012-10-30T13:17:30.590 回答