在使用EF来处理业务逻辑时,一个必不可少的feature就是事务(Transaction),如何在使用EF时使用事务,在MSDN上的这篇文章有详细的说明:How to: Manage Transactions in the Entity Framework。由于有些描述理解起来担心有歧义,所以对一些基本的场景我做了如下检查。
一,数据库准备
数据库1:EFTransactionDemo (表:User, Thread)
CREATE TABLE [dbo].[User]( [ID] [int] NOT NULL, [Name] [varchar](20) NULL, [Email] [varchar](200) NULL, PRIMARY KEY CLUSTERED ( [ID] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] ) ON [PRIMARY]
CREATE TABLE [dbo].[Thread]( [ID] [int] NOT NULL, [Title] [varchar](100) NULL, [Content] [varchar](500) NULL, [Author] [int] NULL, [CreatedDate] [datetime] NULL, PRIMARY KEY CLUSTERED ( [ID] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] ) ON [PRIMARY] GO ALTER TABLE [dbo].[Thread] WITH CHECK ADD CONSTRAINT [FK_Thread_User] FOREIGN KEY([Author]) REFERENCES [dbo].[User] ([ID]) GO ALTER TABLE [dbo].[Thread] CHECK CONSTRAINT [FK_Thread_User] GO
数据库2:EFTransactionDemo2 (表:BO, BOM)
CREATE TABLE [dbo].[BO]( [ID] [int] NOT NULL, [Code] [varchar](10) NULL, [BillDate] [datetime] NULL, PRIMARY KEY CLUSTERED ( [ID] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] ) ON [PRIMARY]
CREATE TABLE [dbo].[BOM]( [ID] [int] NOT NULL, [BOID] [int] NULL, [MaterialCode] [int] NULL, [Num] [int] NULL, PRIMARY KEY CLUSTERED ( [ID] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] ) ON [PRIMARY] GO ALTER TABLE [dbo].[BOM] WITH CHECK ADD CONSTRAINT [FK_BOM_BO] FOREIGN KEY([BOID]) REFERENCES [dbo].[BO] ([ID]) GO ALTER TABLE [dbo].[BOM] CHECK CONSTRAINT [FK_BOM_BO] GO
之所以增加外键,是想通过外键约束来模拟插入失败的场景。
创建VS工程
创建一个Console Application: EFTransactionDemo1。
创建Library Project: DemoDataModel11, DemoDataModel12, DemoDataModel21。前两个对应EFTransactionDemo数据库,后一个是用于EFTransactionDemo2数据库的。
场景一:Object Context中SaveChanges是用一个事务提交的
using (EFTransactionDemoEntities context = new EFTransactionDemoEntities()) { User user = new User { ID = 1, Email = "bright.zhang@abc.com", Name = "Bright Zhang" }; context.AddToUsers(user); Thread thread = new Thread { ID = 1, Author = 2, Title = "how to do this", Content = "dfdfd ", CreatedDate = DateTime.Now }; context.Threads.AddObject(thread); context.SaveChanges(); }
通过提交一个不存在的Author的Thread来模拟插入失败,从而验证了在一个Object Context中,一次SaveChanges是默认使用一个事务来提交的。
场景二:一个objectcontext内的多次SaveChanges是在不同事务内提交的
using (EFTransactionDemoEntities context = new EFTransactionDemoEntities()) { User user = new User { ID = 1, Email = "bright.zhang@abc.com", Name = "Bright Zhang" }; context.AddToUsers(user); context.SaveChanges(); Thread thread = new Thread { ID = 1, Author = 2, Title = "how to do this", Content = "dfdfd ", CreatedDate = DateTime.Now }; context.Threads.AddObject(thread); context.SaveChanges(); }
场景三:对两个相同的Object Context使用TransactionScope来实现EF的事务
using (TransactionScope scope = new TransactionScope()) { using (EFTransactionDemoEntities context = new EFTransactionDemoEntities()) { User user = new User { ID = 1, Email = "bright.zhang@abc.com", Name = "Bright Zhang" }; context.AddToUsers(user); context.SaveChanges (System.Data.Objects.SaveOptions.DetectChangesBeforeSave); } using (EFTransactionDemoEntities context = new EFTransactionDemoEntities()) { Thread thread = new Thread { ID = 1, Author = 2, Title = "how to do this", Content = "dfdfd ", CreatedDate = DateTime.Now }; context.Threads.AddObject(thread); context.SaveChanges(System.Data.Objects.SaveOptions.DetectChangesBeforeSave); } scope.Complete(); }
注意,这里使用多个Object Context进行提交,可以通过TransactionScope来确保他们在同一个事务中完成。注意一点,在每个Object Context提交修改时提供保存选项:context.SaveChanges(System.Data.Objects.SaveOptions.DetectChangesBeforeSave);,这样就可以了。据我所知,TransactionScope的使用会用到MSDTC。如果Object Context连接到相同的数据库,那么TransactionScope将直接使用SqlTransaction / OleDbTransaction提交,如果连接到不同的数据库,那么连接到两个数据库的事务将会被promote到DTC进行协调。在上面的这个例子中,同一个Object Context,在停止MSDTC之后仍然可以正常工作。那么连到同一个数据库的不同Object Context呢?(这里就用到了DemoDataModel21了)
场景四:对连接到同一数据库的两个不同Object Context使用TransactionScope来实现EF事务
using (TransactionScope scope = new TransactionScope()) { using (DemoDataModel11.EFTransactionDemoEntities11 context = new DemoDataModel11.EFTransactionDemoEntities11()) { DemoDataModel11.User user = new DemoDataModel11.User { ID = 1, Email = "bright.zhang@abc.com", Name = "Bright Zhang" }; context.AddToUsers(user); context.SaveChanges(System.Data.Objects.SaveOptions.DetectChangesBeforeSave); } using (DemoDataModel12.EFTransactionDemoEntities12 context = new DemoDataModel12.EFTransactionDemoEntities12()) { DemoDataModel12.Thread thread = new DemoDataModel12.Thread { ID = 1, Author = 2, Title = "how to do this", Content = "dfdfd ", CreatedDate = DateTime.Now }; context.AddToThreads(thread); context.SaveChanges(System.Data.Objects.SaveOptions.DetectChangesBeforeSave); } scope.Complete(); }
通过上面的代码确认,连接到同一数据库的不同Object Context不需要被上升到MSDTC进行事务协调即可实现事务。那么连接到不同数据库的不同Object Context呢?先停掉MSDTC: net stop MSDTC。
场景五:对连接到不同数据库的两个不同Object Context使用TransactionScope来实现EF事务
using (TransactionScope scope = new TransactionScope()) { using (DemoDataModel11.EFTransactionDemoEntities11 context = new DemoDataModel11.EFTransactionDemoEntities11()) { DemoDataModel11.User user = new DemoDataModel11.User { ID = 1, Email = "bright.zhang@abc.com", Name = "Bright Zhang" }; context.AddToUsers(user); context.SaveChanges(System.Data.Objects.SaveOptions.DetectChangesBeforeSave); } using (DemoDataModel12.EFTransactionDemoEntities12 context = new DemoDataModel12.EFTransactionDemoEntities12()) { DemoDataModel12.Thread thread = new DemoDataModel12.Thread { ID = 1, Author = 1, Title = "how to do this", Content = "dfdfd ", CreatedDate = DateTime.Now }; context.AddToThreads(thread); context.SaveChanges(System.Data.Objects.SaveOptions.DetectChangesBeforeSave); } using (DemoDataModel21.EFTransactionDemo2Entities context = new DemoDataModel21.EFTransactionDemo2Entities()) { DemoDataModel21.BO bo = new DemoDataModel21.BO { ID = 1, BillDate = DateTime.Now, Code = "001" }; context.AddToBOes(bo); DemoDataModel21.BOM bom = new DemoDataModel21.BOM { ID = 1, BOID = 2, MaterialCode = 1001, Num = 10 }; context.AddToBOMs(bom); context.SaveChanges(SaveOptions.DetectChangesBeforeSave); } scope.Complete(); }
通过在提交BO/BOM的时候,利用外键约束模拟插入失败的例子,发现报异常:
MSDTC on server 'XXX' is unavailable.
从这句话已经说明,在同一个TransactionScope下的不同Object Context访问不同数据库,会将各自的事务上升至MSDTC进行协调。
然后启用MSDTC,再执行,发现事务已经能够正常工作了。
场景六:EF和ADO.NET/Enterprise Library混用
如果把EF和Enterprise Library混用,那么TransactionScope又是如何实现事务的呢?
using (TransactionScope scope = new TransactionScope()) { using (DemoDataModel11.EFTransactionDemoEntities11 context = new DemoDataModel11.EFTransactionDemoEntities11()) { DemoDataModel11.User user = new DemoDataModel11.User { ID = 1, Email = "bright.zhang@abc.com", Name = "Bright Zhang" }; context.AddToUsers(user); context.SaveChanges(System.Data.Objects.SaveOptions.DetectChangesBeforeSave); } // insert a record using ado.net using (SqlConnection connection = new SqlConnection("Database=EFTransactionDemo;Server=.;Integrated Security=SSPI;")) { connection.Open(); SqlCommand command = new SqlCommand("insert into [User] select 100, 'admin', 'admin@test.com'", connection); command.ExecuteNonQuery(); } using (DemoDataModel12.EFTransactionDemoEntities12 context = new DemoDataModel12.EFTransactionDemoEntities12()) { DemoDataModel12.Thread thread = new DemoDataModel12.Thread { ID = 1, Author = 100, Title = "how to do this", Content = "dfdfd ", CreatedDate = DateTime.Now }; context.AddToThreads(thread); context.SaveChanges(System.Data.Objects.SaveOptions.DetectChangesBeforeSave); } scope.Complete(); }
根据上面的实例代码,可以发现,对于混用的情况,当在TransactionScope中,如果有EF和ADO.NET混用,那么其事务需要升级到MSDTC进行协调。
场景七:ObjectContext.ExecuteStoreCommand/ExecuteStoreQuery/ExecuteFunction避免事务被升级到MSDTC
对于在EF中需要处理其他SQL语句,或者通过其他SQL语句实现特殊逻辑时,可以使用EF提供的一些途径。尤其在使用TransactionScope时非常必要。
using (TransactionScope scope = new TransactionScope()) { using (DemoDataModel11.EFTransactionDemoEntities11 context = new DemoDataModel11.EFTransactionDemoEntities11()) { DemoDataModel11.User user = new DemoDataModel11.User { ID = 1, Email = "bright.zhang@abc.com", Name = "Bright Zhang" }; context.AddToUsers(user); context.SaveChanges(System.Data.Objects.SaveOptions.DetectChangesBeforeSave); } using (DemoDataModel11.EFTransactionDemoEntities11 context = new DemoDataModel11.EFTransactionDemoEntities11()) { context.ExecuteStoreCommand(@"insert dbo.[User] values(100, 'admin', 'admin@test.com')"); } using (DemoDataModel12.EFTransactionDemoEntities12 context = new DemoDataModel12.EFTransactionDemoEntities12()) { DemoDataModel12.Thread thread = new DemoDataModel12.Thread { ID = 1, Author = 101, Title = "how to do this", Content = "dfdfd ", CreatedDate = DateTime.Now }; context.AddToThreads(thread); context.SaveChanges(System.Data.Objects.SaveOptions.DetectChangesBeforeSave); } scope.Complete(); }
通过EF提供的渠道,可以直接利用Object Context的连接,这也是TransactionScope可以发挥作用的主要原因。
我尝试用ObjectContext的Connection直接创建DbCommand,然后执行一个操作,结果提示错误。代码如下:
using (DemoDataModel11.EFTransactionDemoEntities11 context = new DemoDataModel11.EFTransactionDemoEntities11()) { context.Connection.Open(); DbCommand command0 = context.Connection.CreateCommand(); command0.CommandType = System.Data.CommandType.Text; command0.CommandText = @"insert dbo.[User] values(100, 'admin', 'admin@test.com')"; command0.ExecuteNonQuery(); }
错误如下:
The query syntax is not valid. Near identifier 'dbo', line 1, column 8.
at System.Data.Common.EntitySql.CqlParser.yyerror(String s)
at System.Data.Common.EntitySql.CqlParser.yyparse()
at System.Data.Common.EntitySql.CqlParser.internalParseEntryPoint()
at System.Data.Common.EntitySql.CqlParser.Parse(String query)
at System.Data.Common.EntitySql.CqlQuery.Parse(String commandText, ParserOpti
ons parserOptions)
at System.Data.Common.EntitySql.CqlQuery.CompileCommon[TResult](String comman
dText, Perspective perspective, ParserOptions parserOptions, Func`3 compilationF
unction)
at System.Data.Common.EntitySql.CqlQuery.Compile(String commandText, Perspect
ive perspective, ParserOptions parserOptions, IEnumerable`1 parameters)
at System.Data.EntityClient.EntityCommand.MakeCommandTree()
at System.Data.EntityClient.EntityCommand.CreateCommandDefinition()
at System.Data.EntityClient.EntityCommand.TryGetEntityCommandDefinitionFromQu
eryCache(EntityCommandDefinition& entityCommandDefinition)
at System.Data.EntityClient.EntityCommand.GetCommandDefinition()
at System.Data.EntityClient.EntityCommand.InnerPrepare()
at System.Data.EntityClient.EntityCommand.Prepare()
at System.Data.EntityClient.EntityCommand.ExecuteReader(CommandBehavior behav
ior)
at System.Data.EntityClient.EntityCommand.ExecuteScalar[T_Result](Func`2 resu
ltSelector)
at System.Data.EntityClient.EntityCommand.ExecuteNonQuery()
at EFTransactionDemo1.Program.Main(String[] args) in D:\temp\vs\EFTransaction
Demo1\EFTransactionDemo1\Program.cs:line 96
这个问题,没有找到原因。
总结
Entity Framework可以通过TransactionScope来使用事务,其中,多个Object Context操作同一个数据库时,不需要将事务上升到MSDTC,而直接使用SqlTransaction/OleDbTransaction。而如果访问多个数据库,那么MSDTC则需要启动,用于不同Object Context之间的事务协调。
尤其是多个Object Context访问同一个数据库时,使用TransactionScope不需要使用MSDTC这一点,对于一些小型的应用系统的架构有着明显的好处。因为MSDTC是一个昂贵的方案,不管是性能问题,还是对组件服务的依赖,都可能成为架构决策时的重要的考量点。
此外,当需要通过其他SQL脚本/存储过程来实现特殊的业务逻辑或操作时,尽量使用EF提供的渠道:ObjectContext.ExecuteStoreCommand, ObjectContext.ExecuteStoreQuery, ObjectContext.ExecuteFunction。如果需要实现事务,且不想通过MSDTC进行事务协调,那么只能使用这些渠道。反之,多个操作的事务会被提交到MSDTC进行协调处理。