EntityFramework走马观花之CRUD(下)

我在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开发数据存取层的问题。

来源: <http://blog.csdn.net/bitfan/article/details/14231561>

来自为知笔记(Wiz)

时间: 2024-10-09 08:58:11

EntityFramework走马观花之CRUD(下)的相关文章

[转]EntityFramework走马观花之CRUD(下)

学习Entity Framework技术期间查阅的优秀文章,出于以后方便查阅的缘故,转载至Blog,可查阅原文:http://blog.csdn.net/bitfan/article/details/14231561 我在Entity Framework系列文章的CRUD上篇中介绍了EF的数据查询,中篇谈到了EF的数据更新,下篇则聊聊EF实现CRUD的内部原理. 跟踪实体对象状态 在CRUD上篇和中篇谈到,为了实现提取和更新数据的功能,EF必须使用某种机制来跟踪实体对象,以便依据对象当前状态生成

[转]EntityFramework走马观花之CRUD(上)

学习Entity Framework技术期间查阅的优秀文章,出于以后方便查阅的缘故,转载至Blog,可查阅原文:http://blog.csdn.net/bitfan/article/details/13001935 对于任何一个ORM框架,CRUD都是其核心功能,可以这么说,CRUD功能实现得好坏,直接决定了此ORM框架的命运. CRUD是英文Create.Read.Update.Delete四个单词的缩写,对应于汉语,就是“增.删.改.查”四个字.再细分一下,“增.删.改”可归为一类,其特点

[转]EntityFramework走马观花之CRUD(中)

学习Entity Framework技术期间查阅的优秀文章,出于以后方便查阅的缘故,转载至Blog,可查阅原文:http://blog.csdn.net/bitfan/article/details/13023223 如果是独立的实体对象,在底层数据库中它对应一张独立的表,那么,对它进行新建.删除和修改没有任何难度,实在不值浪费笔墨在它上头. 在现实项目中,完全独立的对象少之又少,绝大多数情况都是对象之间有着紧密的关联.这种关联主要分为三种类型:一对一.一对多和多对多. 如果对EF浅尝辄止,则我

EntityFramework走马观花之CRUD(中)

如果是独立的实体对象,在底层数据库中它对应一张独立的表,那么,对它进行新建.删除和修改没有任何难度,实在不值浪费笔墨在它上头. 在现实项目中,完全独立的对象少之又少,绝大多数情况都是对象之间有着紧密的关联.这种关联主要分为三种类型:一对一.一对多和多对多. 如果对EF浅尝辄止,则我几乎可以肯定你一定会在实际开发中被对象间的关联弄得焦头烂额.下面就和大家聊聊EF是如何处理不同对象关联类型数据更新问题的. 一对一关联 在面向对象的世界中,使用对象组合实现一对一关联,这种关联具有方向性.比如A与B对象

EntityFramework走马观花之CRUD(上)

对于任何一个ORM框架,CRUD都是其核心功能,可以这么说,CRUD功能实现得好坏,直接决定了此ORM框架的命运. CRUD是英文Create.Read.Update.Delete四个单词的缩写,对应于汉语,就是"增.删.改.查"四个字.再细分一下,"增.删.改"可归为一类,其特点是要更新数据源,而"查"则归为另一类,它不修改原始的数据源. 我们的技术探索之旅,从"查"开始. 1 两种查询数据的方式 EF主要使用两种方式查询数

EntityFramework Code First 模式下使用数据迁移

启用数据迁移 在程序包管理控制台选择安装了EntityFramework的项目,键入如下指令以启EF用数迁移. Enable-Migrations 命令成功运行后,所选项目下会添加名为Migrations的文件夹及Configuration.cs文件,如下图. 创建迁移版本 程序包管理控制台,键入如下命令创建一个迁移版本(当前Entity与数据库的差异). Add-Migration xxx xxx为迁移文件名,例如此次添加了产品表,为方便记忆可取为 Add-Migration createPr

CRUD生成器DBuilder设计与实现

文章由作者原创,最先发表在我的博客园:http://www.cnblogs.com/lvyahui/p/5626466.html. 可以加群交流:146103720 群链接: 源码位于github:https://github.com/lvyahui8/dbuilder.git .文中图片如果太小看不清楚,请右键点击"在新标签页中打开"即可看到原图 第一章           引言 1.1 研究背景及意义 计算机软件技术发展至今,数据库已成为最广泛使用的存储格式化数据的媒介,数据库程序

.NET MVC4 实训记录之三(EntityFramework 与枚举)

EntityFramework对枚举的引入是从版本5开始的(如果没有记错的话).枚举可以很大程度上提高对程序的可读性.那么在EntityFramework的CodeFirst模式下,如何使用枚举呢?答案很简单:还是那么用! 看似废话,其实不然,看下面(修改上一篇中用户信息定义): /// <summary> /// 性别枚举 /// </summary> public enum Gender { Male, Female } public class UserProfile { [

EntityFramework 插入数据慢的原因

EntityFramework在默认情况下AutoDetectChangesEnabled值为true, 当程序有大量的Entity时,插入和更新数据会非常慢, 我遇到的情况是一条插入语句需要1秒左右 把AutoDetectChangesEnabled设为false后,速度大幅度提升,一条记录几毫秒就完成了 context.Configuration.AutoDetectChangesEnabled = false; EntityFramework 插入数据慢的原因