我在Entity Framework系列文章的CRUD上篇中介绍了EF的数据查询,中篇谈到了EF的数据更新,下篇则聊聊EF实现CRUD的内部原理。

跟踪实体对象状态

在CRUD上篇和中篇谈到,为了实现提取和更新数据的功能,EF必须使用某种机制来跟踪实体对象,以便依据对象当前状态生成相应的SQL命令。

这里的关键是区分清楚内存中的数据实体对象数据库中的记录

当程序运行时,位于内存中的EF数据实体可以处于以下五种状态之一:

1.        Added: 实体对象是新创建的,数据库中没有相应的记录。

2.        Unchanged: 从数据库加载到内存后,实体对象属性值没有任何改变。

3.        Modified: 至少有一个实体对象属性值被改变。

4.        Deleted: 如果用户从实体对象集合中删除了某实体对象,则它将处于此状态。注意数据库中此对象相应的记录还存在。

5.        Detached: 此实体对象未被EF所跟踪,属“黑户”和“盲流”。居于这种状态的对象多出现在拥有分布式多层架构的系统中,在后一篇谈到EF数据存取层设计的文章中我将对此再做分析。

现在,一个有趣的问题出现了:

当EF从数据库中提取一条记录生成一个实体对象之后,应用程序可以针对它的操作太多了,EF是怎么知道哪个对象处于哪个状态的?

EF的解决方案是:

为当前所有需要跟踪的实体对象,创建一个相应的DbEntityEntry对象,此对象包容着实体对象每个属性的三个值:

1.        Current Value:当前值

2.        Original Value:原始值,就是从数据库中刚提取出来的值

3.        Database Value:数据库中对应记录的对应字段的值

刚从数据库中提取出来时,Current Value= Original Value = Database Value,以后随着程序的运行,在调用SaveChange()方法之前,Original Value维持不变,但Current Value很可能会变化,而Database Value一般情况下也不变,不过如果其他用户修改了数据库中的相应记录,则EF提供了GetDataBaseValues方法获取Database Value的新值。

所以现在很清楚了:

EF为每一个需要跟踪状态的实体对象创建一个对应的DbEntityEntry对象,保存实体对象各属性的Current Value、Original Value和Database Value三个值,只要比较这三个值,很容易地就知道哪个属性值被修改了,从而生成相应的Update命令。

对于新加入的实体,没有original values和database values

标记为删除的实体,没有current values.

Detached的实体对象(通常是通过网络从客户端发送过来的),没有相应DbEntityEntry对象,因此EF无法跟踪其状态,只能先Attach它,创建好相应DbEntityEntry对象之后,才能保存或更新到数据库中。

众多DbEntityEntry对象的管理由DbContext.ChangeTracker所引用的对象负责。

从实体对象获得它所对应的DbEntityEntry对象很简单,使用以下代码即可:

DbEntityEntry   entry=DbContext对象.Entry(实体对象引用);

以下这个示例方法提取并输出指定实体对象属性的所有值:

private static voidPrintChangeTrackingInfo(DbContext context, DbEntityEntry entry)

{

//entry.Entity引用相关联的实体对象

Console.WriteLine(entry.Entity);

//entry.State值指示实体对象当前所处的状态,即前面所述几种状态之一

Console.WriteLine("State: {0}", entry.State);

Console.WriteLine("\nCurrent Values:");

PrintPropertyValues(entry.CurrentValues);

Console.WriteLine("\nOriginal Values:");

PrintPropertyValues(entry.OriginalValues);

Console.WriteLine("\nDatabase Values:");

PrintPropertyValues(entry.GetDatabaseValues());

}

DbEntityEntry对象的CurrentValues/OriginalValues属性是一个DbPropertyValues类型的集合对象,以下方法输出其每个成员的值:

private static void PrintPropertyValues(DbPropertyValues values)

{

foreach (var propertyName in values.PropertyNames)

{

Console.WriteLine(" - {0}: {1}",propertyName,  values[propertyName]);

}

}

处理并发冲突

在Web这种高并发的运行环境中,一个用户修改另一个用户正在处理的数据是很容易出现的场景,当这种冲突出现时,EF无法更新数据库中的数据。

举例说明,假设我们有以下实体类:

public class Person

{

public int PersonId { get; set; }

public string Name { get; set; }

public int age { get; set; }

public string Description { get; set; }

}

下面来看第一种情景:要修改的记录己被别人修改。

以下代码模拟了这个场景:

Task t1 = new Task(() =>

{

using (var context = new  MyDbContext())

{

Person p =context.People.First();

p.Description ="Description Modified at " + DateTime.Now.ToShortTimeString();

context.SaveChanges();

}

});

Task t2 = new Task(() =>

{

using (var context = new  MyDbContext())

{

Person p =context.People.First();

p.age *= 2;

context.SaveChanges();

}

});

t1.Start();

t2.Start();

Task.WaitAll(t1, t2);

试验的结果是:当同一条记录被甲乙两人同时修改时,如果两人修改不同的字段,则每个字段都可以得到新值。

以下是使用SQL Server  Profiler截获的EF发往SQL Server数据库的SQL命令:

exec sp_executesql N'UPDATE [dbo].[People]

SET [Description] = @0

WHERE ([PersonId] = @1)

',N'@0 nvarchar(max) ,@1int',@0=N'Description Modified at 18:15',@1=11

exec sp_executesql N'UPDATE [dbo].[People]

SET [age] = @0

WHERE ([PersonId] = @1)

',N'@0 int,@1 int',@0=320,@1=11

可以很清楚地看到:EF能依据修改的实体属性名生成相应的Update命令,从而在外部看来,这相当于“合并”了甲乙两人的修改。

我们可以动手修改上述试验代码,很容易得到下述的另一个结果:

如果甲乙两人修改的是相同的字段,则到底谁胜利,取决于谁发出的SQL命令是最后执行的,即“后来者居上”。

第二种情景:要修改的记录己被其他人删除

这种情景是否出现,取决于数据库先执行哪个EF发出的Update和Delete命令:Update First or Delete First。

  • 如果是先Delete后Update,则EF会抛出:DbUpdateConcurrencyException。其给出的信息为:

Store update, insert, or delete statementaffected an unexpected number of rows (0). Entities may have been modified ordeleted since entities were loaded. Refresh ObjectStateManager entries.

  • 如果是先Update后Delete,则不会有任何异常出现,其实数据己被删除!但由于EF没有抛出任何异常,所以提交数据更新请求的人对此一无所知,他还以为更新成功了!这实在是不太妙的事!

所以,程序抛出异常是件好事,别害怕异常,要感谢它,它能让我们知道犯错了,知错能改,就是好同志!

话又说回来,应该怎么对付上述的情景呢?

有两个方法。

先来看第一个法子:指定实体对象的某属性用于并发检测。

public class Person2

{

public int Person2Id { get; set; }

public string Name { get; set; }

public int age { get; set; }

 [ConcurrencyCheck]

public string Description { get; set; }

}

上述代码是采用Code First的EF代码,如果采用Database First方式,则可在实体设计器中设置相应属性的Concurrency Mode属性值为Fixed。

这样一来,下面尝试更新age属性的代码,

Person2 p = context.People2.First();

p.age *= 2;

context.SaveChanges();

将生成不一样的SQL命令:

exec sp_executesql N'UPDATE [dbo].[Person2]

SET [age] = @0

WHERE (([Person2Id] = @1) AND ([Description] = @2))

',N'@0 int,@1 int,@2 nvarchar(max)',@0=80,@1=1,@2=N'Description Modified at 18:49'

可以看到,加了[ConcurrencyCheck]的属性名和值将出现在Where子句中。

这就是关键所在了:

只要给实体类指定一个或多个并发冲突属性(利用[ConcurrencyCheck]),EF就会把它们作为Where子句的条件加入到生成的SQL命令中,如果Update命令返回结果为0,那肯定是出错了,因为原始记录给别人改了。

这种方式需要在实体类中指定特定的属性作为并发冲突检测依据,如果项目中实体类很多,而且程序需要运行于高并发的环境中,为每个实体类都单独地设定实在太麻烦了。这时,数据库跑来帮忙了。

许多数据库系统支持定义一种唯一标识整条记录的特殊字段,当本记录的任一其他字段值有变动时,这一特殊字段马上就会有一个不同的值。这一字段的值是由数据库生成并维护的,应用程序不要显式设置它。

在EF中,我们可以这样为指定实体类指定一个特殊属性:

public class Person3

{

public int Person3Id { get; set; }

public string Name { get; set; }

public int age { get; set; }

public string Description { get; set; }

 [Timestamp]

       public byte[ ] RowVersion { get; set; }

}

在SQL Server中相应的字段类型为timestamp。

添加这样的一个字段之后,在Update数据时,如果记录被他人所修改,则EF将总是抛出DbUpdateConcurrencyException,让用户知道有数据冲突发生,从而用户能采取相应行动以保证数据安全可靠。

如果有多个实体类都希望支持并发冲突检测,可以设定一个实体基类,如下所示:

public class  EntityBase

{

[Timestamp]

public byte[ ] RowVersion { get; set; }

}

让所有相关实体类都派生自它即可。这是一个偷懒的方法,却很好用。

事务处理

默认情况下,当EF调用SaveChanges()时,会把生成的所有SQL命令“包”到一个“事务(transaction)”中,只要有一个数据更新操作失败,整个事务将回滚。

在多数情况下,如果你总在数据更新操作代码中使用一个而不是多个DbContext对象,并且只是在最后调用一次SaveChanges(),那么EF的默认事务处理机制己经够用了,无需做额外的事情。

然而,如果出现以下的情形,你就必须显式地处理事务了。

第一种情况:你需要分阶段地保存数据,因而需要多次调用SaveChanges()或者执行修改数据库的SQL命令。

请看以下示例代码:

using (var context = new MyDbContext())

{

try

{

Person3 p = context.People3.First();

p.Name ="newName" + (new Random().Next(1, 100));

context.SaveChanges();

context.Database.ExecuteSqlCommand("update Person3 setDescription={0} where Person3Id={1}",

"DescriptionModified at " + DateTime.Now.ToShortTimeString(),

p.Person3Id);

p.age *= 2;

context.SaveChanges();

}

catch (Exception e)

{

Console.WriteLine(e.Message);

}

上述代码中,调用两次SaveChanges(),还有一次执行Update命令。

如果在最后一次SaveChanges()中出现异常,虽然最后一次没成功,但你会发现前两次数据己经保存!这就带来了数据不一致的问题。

对于这种场景,你需要显式地编写事务代码了(注:以下代码适用于EF6):

using (var context = new MyDbContext())

{

using (var transaction =context.Database.BeginTransaction())

{

try

{

……

context.SaveChanges();

context.Database.ExecuteSqlCommand("……);

……

context.SaveChanges();

  transaction.Commit();

}

catch (Exception e)

{

Console.WriteLine(e.Message);

transaction.Rollback();

}

}

}

特别要注意一定要调用commit(),我测试发现,只要不Commit,即使没有异常发生,事务仍将回滚,数据库中的数据不会更新。

第2种情况,你需要使用多个DbContext保存数据

以下是处理这种场景的典型代码:

static void TestTransactionScope2()

{

using (TransactionScope scope = new TransactionScope())

{

String connStr = ……;

using (var conn = newSqlConnection(connStr))

{

try

{

conn.Open();

using (var context1 =new MyDbContext(conn, contextOwnsConnection: false))

{

……

context1.SaveChanges();

}

using (var context2 =new MyDbContext(conn, contextOwnsConnection: false))

{

context2.Database.ExecuteSqlCommand(……);

context2.SaveChanges();

}

using (var context3 =new MyDbContext2(conn, contextOwnsConnection: false))

{

……

context3.SaveChanges();

}

scope.Complete();

}

catch (Exception e)

{

Console.WriteLine(e.ToString());

}

finally

{

 conn.Close();

}

}

}

}

上述代码中有几个关键点:

(1)在构造DbContext对象时,需要把一个己打开的数据库连接对象传给它,并且需要指定EF在DbContext对象销毁时不关闭数据库连接。

为实现此目的,你的DbContext对象应该类似于是这样的,提供两个重载的构造函数:

public class MyDbContext2 : DbContext

{

public MyDbContext2(DbConnection conn, boolcontextOwnsConnection):base(conn,contextOwnsConnection)

{

}

public MyDbContext2():base()

{

}

public DbSet<OtherEntity> OtherEntities { get; set; }

……

}

注意在代码结束时关闭连接。

(2)如果不Commit,则所有数据将不会保存。

(3)你的计算机需要启动MSDTC(分布式交易协调器),请先在控制面板中打开Distributed Transaction Coordinator服务,否则上述代码将在运行时抛出MSDTC服务不可用的异常。

很明显,当事务需要使用多个不同类型的DbContext对象时,Windows需要启动MSDTC,这会对性能有所影响,因此在开发中应该尽量避免这种情况,如无必要,不要在单个事务中使用多个不同种类的DbContext对象。

小结:

到此为止,我己经把EF中与CRUD的几个话题讨论完了,下一篇将是我的Entity Framework系列的收尾之作,讨论使用EF开发数据存取层的问题。

最新文章

  1. 如何根据执行计划,判断Mysql语句是否走索引
  2. SQL查询关于相对路径、矢代、绝对路径、递归、计算列的速度对比跟优化-SOD群记录
  3. UVA-11517 Exact Change(DP)
  4. gulp-rev:项目部署缓存解决方案----gulp系列(六)
  5. ParagraphString - 段落样式的简易处理
  6. 关于ActionContext.getContext()的用法心得
  7. MySQL删除表数据
  8. Nova相关命令收集
  9. uboot官方FTP下载地址
  10. docker 镜像中包含数据库环境和运行环境
  11. Linux下用命令格式化U盘
  12. BZOJ 1644: [Usaco2007 Oct]Obstacle Course 障碍训练课( BFS )
  13. YII 1.0 扩展第三方类
  14. 神经网络JOONE的实践
  15. ExtJS学习(二)Ext组件模型
  16. python 基础之自动类型转换和强制类型转换
  17. mac ssh,mac xshell,xshell替代,ssh客户端,ssh工具,远程桌面加速
  18. IntelliJ IDEA的main方法,for循环,syso的快捷键
  19. 如何封装使用api形式调用的vue组件
  20. Sqlite3并发读写注意事项

热门文章

  1. MyBatis一级缓存(转载)
  2. codevs——3111 CYD啃骨头(背包)
  3. 北冥有 Data,其名为鲲,鲲之大,一个 MySQL 放不下!
  4. retain和copy还有assign的区别
  5. Scut游戏服务器引擎6.1.5.6发布,直接可运行,支持热更新
  6. hdu1874最短路
  7. flask的日志输出current_app.logger.debug
  8. 在Delphi中应用AOP实现日志功能
  9. ASP.NET Web API是如何根据请求选择Action的?[下篇] 【转】
  10. ofstream的使用方法