获取EF提交操作的SQL语句
Entity Framework code-fist 的SaveChanges()方法一次性提交对实体的更改到数据库。类似于ADO.net 的
DataAdapter 对象提交 DataSet 的更新。 只不过,EF作为ORM ,实体是强类型的,这样linq也能做很好的支持。
最近,在做工作流的项目中,使用EF作为数据库访问层,需要在 EF提交实体的更新的时候,同时执行一段sql语句或者存储过程,而且要保证在同一个事务当中。
关于EF事务提交的问题,网上已经有方案,主要是通过 TransactionScope:
using (TransactionScope scope = new TransactionScope()) { dbContext.Database.ExecuteSqlCommand(sql, param); dbContext.SaveChanges(); scope.Complete(); }
甚至在TransactionScope包含的代码中可以 提交多个 DBContext,当然如果多个DBContext对于的是不同的数据库,这个事务肯定要上升到分布式事务 msdtc.
而且我们同时执行的sql语句,只能通过ExecuteSqlCommand 这条通道,如果不是的话,还是会上升到msdtc,可以参考 http://www.cnblogs.com/lovvver/archive/2012/06/10/2543762.html
由于,EF每次查询或者更新操作, 无论是ExecuteSqlCommand 还是SaveChanges,都是打开Connection,然后及时就关闭。所以,上面的事务是在多个连接 的事务。要命的是,在不支持多连接事务的数据库中,比如sql server 2005, 还是会上升为 msdtc。关于这个问题,请参考 http://www.digitallycreated.net/Blog/48/entity-framework-transactionscope-and-msdtc。
因为,在实际并不是分布式的场景中,要用到以上的方式提交事务,就不得不配置msdtc,msdtc服务浪费系统资源 和 不易配置 不讲,我们并没有实实在在做分布式操作,而且
可以的话,我们提交的各个操作都可以在一个连接内完成的。EF 没有提供方法让我们实现我们的功能,因此开始怀念ADO.net。
但是又不舍得,仅仅是因为操作提交的通道问题 ,就要舍弃 EF提供的强大的映射和查询功能吗 ? 要知道这方面的确比 SQLinq , L2S , 强大很多。
如果我们能获取SaveChanges要提交的sql语句,然后我们就能为所欲为。查询的时候走的是传统的ef通道,提交的时候走的是ado.net 爱怎么样就怎么样,而EF参与提交的方式,只是提供sql语句. 查询和操作分离这种架构也符合CQRS。
前段时间,EF不是开源了吗http://www.cnblogs.com/shanyou/archive/2012/07/20/2600465.html ,嘿嘿 ,还有什么是我们做不到的呢?
首先从http://entityframework.codeplex.com/下载源代码,下载的版本需要用vs2012 打开,而且framework版本是4.5的。
不过庆幸的是,4.0的程序可以调用4.5编译的dll。
下载vs2012 rc ,打开解决方案。实际只需要 EntityFramework 和 EntityFramework.SqlServer 两个项目,其他的强制签名和nuget和其他不相关的项目可以移除。并且编译成功。
找到 ,DbContext 的类 , 的SaveChanges方法,顺藤摸瓜,
DbContext -》 InternalContext -》ObjectContext-》EntityAdapter-》UpdateTranslator -》 DynamicUpdateCommand
终于找到 DynamicUpdateCommand 的方法 Execute ,里面看到了 DbCommand 如何被创建出来。
internal override long Execute( Dictionary<int, object> identifierValues, List<KeyValuePair<PropagatorResult, object>> generatedValues) { // Compile command using (var command = CreateCommand(identifierValues)) { //更新的command var connection = Translator.Connection; // configure command to use the connection and transaction for this session command.Transaction = ((null == connection.CurrentTransaction) ? null : connection.CurrentTransaction.StoreTransaction); command.Connection = connection.StoreConnection; if (Translator.CommandTimeout.HasValue) { command.CommandTimeout = Translator.CommandTimeout.Value; } // Execute the query int rowsAffected; if (_modificationCommandTree.HasReader) { Debug.WriteLine("查询的sql :" + command.CommandText); // retrieve server gen results rowsAffected = 0; using (var reader = command.ExecuteReader(CommandBehavior.SequentialAccess)) { if (reader.Read()) { rowsAffected++; var members = TypeHelpers.GetAllStructuralMembers(CurrentValues.StructuralType); for (var ordinal = 0; ordinal < reader.FieldCount; ordinal++) { // column name of result corresponds to column name of table var columnName = reader.GetName(ordinal); var member = members[columnName]; object value; if (Helper.IsSpatialType(member.TypeUsage) && !reader.IsDBNull(ordinal)) { value = SpatialHelpers.GetSpatialValue(Translator.MetadataWorkspace, reader, member.TypeUsage, ordinal); } else { value = reader.GetValue(ordinal); } // retrieve result which includes the context for back-propagation var columnOrdinal = members.IndexOf(member); var result = CurrentValues.GetMemberValue(columnOrdinal); // register for back-propagation generatedValues.Add(new KeyValuePair<PropagatorResult, object>(result, value)); // register identifier if it exists var identifier = result.Identifier; if (PropagatorResult.NullIdentifier != identifier) { identifierValues.Add(identifier, value); } } } // Consume the current reader (and subsequent result sets) so that any errors // executing the command can be intercepted CommandHelper.ConsumeReader(reader); } } else { Debug.WriteLine("更新的sql :" + command.CommandText); // return 1; rowsAffected = command.ExecuteNonQuery(); } return rowsAffected; } }
rowsAffected = command.ExecuteNonQuery(); 这个代码正是真正执行的操作。
思路是我们对 DbCommand 阻止提交 ,并且记录到一个集合里面。因此我们新建一个类:
public class CommandItem { public string CommandText { get; set; } public CommandType CommandType { get; set; } public DbParameterCollection Parameters { get; set; } }
并且组合到 ObjectContext 之中
public List<CommandItem> CommandItems { get; set; }
在DbContext就能访问到:
public List<CommandItem> CommandItems { get { return this.InternalContext.ObjectContext.CommandItems; } }
拷贝 DynamicUpdateCommand类里面的方法Excute方法,传入List<CommandItem> items 作为容器
,注释掉执行的代码,修改成一个新的方法:
internal override long AtawExecute(List<CommandItem> items, Dictionary<int, object> identifierValues, List<KeyValuePair<PropagatorResult, object>> generatedValues) { // Compile command using (var command = CreateCommand(identifierValues)) { //更新的command //var connection = Translator.Connection; //// configure command to use the connection and transaction for this session //command.Transaction = ((null == connection.CurrentTransaction) // ? null // : connection.CurrentTransaction.StoreTransaction); //command.Connection = connection.StoreConnection; //if (Translator.CommandTimeout.HasValue) //{ // command.CommandTimeout = Translator.CommandTimeout.Value; //} // Execute the query // int rowsAffected; if (_modificationCommandTree.HasReader) { Debug.WriteLine("查询的sql :" + command.CommandText); // retrieve server gen results //rowsAffected = 0; //using (var reader = command.ExecuteReader(CommandBehavior.SequentialAccess)) //{ // if (reader.Read()) // { // rowsAffected++; // var members = TypeHelpers.GetAllStructuralMembers(CurrentValues.StructuralType); // for (var ordinal = 0; ordinal < reader.FieldCount; ordinal++) // { // // column name of result corresponds to column name of table // var columnName = reader.GetName(ordinal); // var member = members[columnName]; // object value; // if (Helper.IsSpatialType(member.TypeUsage) // && !reader.IsDBNull(ordinal)) // { // value = SpatialHelpers.GetSpatialValue(Translator.MetadataWorkspace, reader, member.TypeUsage, ordinal); // } // else // { // value = reader.GetValue(ordinal); // } // // retrieve result which includes the context for back-propagation // var columnOrdinal = members.IndexOf(member); // var result = CurrentValues.GetMemberValue(columnOrdinal); // // register for back-propagation // generatedValues.Add(new KeyValuePair<PropagatorResult, object>(result, value)); // // register identifier if it exists // var identifier = result.Identifier; // if (PropagatorResult.NullIdentifier != identifier) // { // identifierValues.Add(identifier, value); // } // } // } // // Consume the current reader (and subsequent result sets) so that any errors // // executing the command can be intercepted // CommandHelper.ConsumeReader(reader); //} } else { Debug.WriteLine("更新的sql :" + command.CommandText); items.Add(new CommandItem() { CommandText = command.CommandText , CommandType = command.CommandType , Parameters = command.Parameters }); //return 1; // rowsAffected = command.ExecuteNonQuery(); } return 1; } //throw new NotImplementedException(); }
然后,同样的,一路修改应该调用的方法。
internal virtual List<CommandItem> AtawUpdate(List<CommandItem> items)
public int AtawUpdate(IEntityStateManager entityCache, List<CommandItem> items) { //if (!IsStateManagerDirty(entityCache)) //{ // return 0; //} //// Check that we have a connection before we proceed //if (_connection == null) //{ // throw Error.EntityClient_NoConnectionForAdapter(); //} //// Check that the store connection is available //if (_connection.StoreProviderFactory == null // || _connection.StoreConnection == null) //{ // throw Error.EntityClient_NoStoreConnectionForUpdate(); //} //// Check that the connection is open before we proceed //if (ConnectionState.Open // != _connection.State) //{ // throw Error.EntityClient_ClosedConnectionForUpdate(); //} var updateTranslator = _updateTranslatorFactory(entityCache, this); updateTranslator.AtawUpdate(items); return 0; }
private void AtawSaveChangesToStore(SaveOptions options) { int entriesAffected; // var mustReleaseConnection = false; var connection = (EntityConnection)Connection; // get data adapter if (_adapter == null) { _adapter = (IEntityAdapter)((IServiceProvider)EntityProviderFactory.Instance).GetService(typeof(IEntityAdapter)); } // only accept changes after the local transaction commits _adapter.AcceptChangesDuringUpdate = false; _adapter.Connection = connection; _adapter.CommandTimeout = CommandTimeout; //try //{ //EnsureConnection(); // mustReleaseConnection = true; // determine what transaction to enlist in // var needLocalTransaction = false; //if (null == connection.CurrentTransaction // && !connection.EnlistedInUserTransaction) //{ // // If there isn't a local transaction started by the user, we'll attempt to enlist // // on the current SysTx transaction so we don't need to construct a local // // transaction. // needLocalTransaction = (null == _lastTransaction); //} //// else the user already has his own local transaction going; user will do the abort or commit. //DbTransaction localTransaction = null; //try //{ // // EntityConnection tracks the CurrentTransaction we don't need to pass it around // if (needLocalTransaction) // { // localTransaction = connection.BeginTransaction(); // } entriesAffected = _adapter.AtawUpdate(ObjectStateManager, CommandItems); // if (null != localTransaction) // { // // we started the local transaction; so we also commit it // localTransaction.Commit(); // } // // else on success with no exception is thrown, user generally commits the transaction // } // finally // { // if (null != localTransaction) // { // // we started the local transaction; so it requires disposal (rollback if not previously committed // localTransaction.Dispose(); // } // // else on failure with an exception being thrown, user generally aborts (default action with transaction without an explict commit) // } //} //finally //{ // if (mustReleaseConnection) // { // // Release the connection when we are done with the save // ReleaseConnection(); // } //} //if ((SaveOptions.AcceptAllChangesAfterSave & options) != 0) //{ // // only accept changes after the local transaction commits // try // { // AcceptAllChanges(); // } // catch (Exception e) // { // // If AcceptAllChanges throw - let's inform user that changes in database were committed // // and that Context and Database can be in inconsistent state. // throw new InvalidOperationException(Strings.ObjectContext_AcceptAllChangesFailure(e.Message)); // } //} // return items; }
public int AtawSaveChanges(SaveOptions options) { PrepareToSaveChanges(options); var entriesAffected = ObjectStateManager.GetObjectStateEntriesCount(EntityState.Added | EntityState.Deleted | EntityState.Modified); // if there are no changes to save, perform fast exit to avoid interacting with or starting of new transactions if (0 < entriesAffected) { entriesAffected = 0; AtawSaveChangesToStore(options); } ObjectStateManager.AssertAllForeignKeyIndexEntriesAreValid(); //return items; return 0; }
public virtual int AtawSaveChanges() { try { if (ValidateOnSaveEnabled) { var validationResults = Owner.GetValidationErrors(); if (validationResults.Any()) { throw new DbEntityValidationException( Strings.DbEntityValidationException_ValidationFailed, validationResults); } } var shouldDetectChanges = AutoDetectChangesEnabled && !ValidateOnSaveEnabled; var saveOptions = SaveOptions.AcceptAllChangesAfterSave | (shouldDetectChanges ? SaveOptions.DetectChangesBeforeSave : 0); return ObjectContext.AtawSaveChanges(saveOptions); } catch (UpdateException ex) { throw WrapUpdateException(ex); } }
最外面的DbContext 调用:
public virtual void SaveAtawChanges() { HasSaveChanges = true; // List<CommandItem> items = new List<CommandItem>(); // return items; InternalContext.AtawSaveChanges(); }
编译,然后测试一下:
WorkflowDbContext context = new WorkflowDbContext(); var list = context.WF_WORKFLOW_INST.ToList(); list.ForEach(a => a.WI_NAME = "ff123"); context.SaveAtawChanges(); var gg = context.CommandItems.Select(a => { string ff = ""; for (int i = 0; i < a.Parameters.Count; i++) { var par = a.Parameters[i]; ff = ff + par.ParameterName + "=" + par.Value; } return new { sql语句 = a.CommandText, 语句类型 = a.CommandType, 参数 = ff }; } ); //执行事务操作 int ggg = 0; var con = context.Database.Connection as SqlConnection; using (con) { con.Open(); var trans = con.BeginTransaction(); try { foreach (var com in context.CommandItems) { List<SqlParameter> sqls = new List<SqlParameter>(); for (int i = 0; i < com.Parameters.Count; i++) { sqls.Add((((ICloneable)com.Parameters[i]).Clone() as SqlParameter)); } // SqlHelper.ExecuteNonQuery( ggg = ggg + SqlHelper.ExecuteNonQuery(trans, com.CommandType, com.CommandText, sqls.ToArray()); } } catch (Exception ex) { //发生异常,事务回滚 Response.Write("数据更新错误:" + ggg); trans.Rollback(); } } Response.Write("数据更新:"+ggg); this.GridView1.DataSource = gg; this.GridView1.DataBind();
sqls.Add((((ICloneable)com.Parameters[i]).Clone() asSqlParameter));
这个代码的目的是因为SqlParameter 不允许被两个DbCommond引用,所以要克隆出来。
最终 我们用
sqlHelper执行sql语句
返回影响行数
GridView上 显示生产的sql 语句
成功。