本文实验用代码请从这里下载:KeyAndModifiedFieldInDataAdapter.rar。
先在SQL Server 2000中建立一名为DBApp的数据库,然后用查询分析器执行SQL-GenDB目录下的.sql文件建立Student表。
让DataAdapter实现KeyAndModifiedField更新
使用DataAdapter(这里我用的是SqlDataAdapter,后面所有DataAdapter的地方均指SqlDataAdapter)进行数据库更新时,可以很容易的实现"只包括主键"、"在WHERE短语中包含所有列"以及"主键和时间戳列"的并发方式,但DataAdapter并没有为我们提供"主键和已修改列"的并发模式。因为该种并发模式尽管可以产生精简的UPDATE命令,但设计代价比较高。David Sceppa在它的《ADO.NET Core Reference》一书中仅仅说了一句可以在DataAdapter的RowUpdateing事件中进行处理,却没有详细的论述。为此我尝试了以此思路实现KeyAndModifiedFiled并发模式。
在代码实现过程中,主要遇到的问题包括:
1、缺乏有效的Schema信息。由于更新命令中SET短语包含的字段以及WHERE短语包含的字段都是动态创建出来的,要根据DataRow中修改的列进行创建,因此需要了解字段类型以及长度和其它相关信息。这些信息本应包含在Table的Schema信息中,但这些信息是设计时有生成器生成的(我反编译了一下DataAdapter的Designer设计器代码,发现微软通过了COM完成的底层实现并用.net进行调用),并且无法在RowUpdating事件中获取,因此如何保存足够的Schema信息就成为实现的一个难题。
2、动态生成更新命令后要动态的将DataRow中的数据存入不同的DataParameter中,如何动态获取不同版本的字段值并填入DataParameter中也具有一定难度。
3、由于缺少必要的Schema信息,所以很难获得主键信息。
4、DataSet中字段名可能与实际Table的字段名不同,它们之间是通过TableMapping完成映射的。在动态生成SQL命令时要根据TableMapping中的信息进行处理,不能出现字段名不相符的差错。
针对上面问题,在程序代码实现中主要采取了以下策略:
1、在向导生成DataAdapter时采用开放式并发,这样DataAdapter的Designer会生成所有字段的Current与Original类型的参数,并且保存在UpdateCommand的Parameters属性中。我的程序在执行时首先备份这些信息到自定义的paramCollection中,将来用Parameter名进行检索(DataParameters支持string类型的Indexer),这样就省去了了解Schema信息的麻烦。但主键信息仍然无法得到很好的解决,只能手工指定。
在程序初始化时会有类似如下几条命令,就是用来保存足够的Parameter信息和主键信息的。
private SqlParameterCollection paramCollection; private string keyFieldName = "id"; paramCollection = sqlUpdateCommand1.Parameters;
在后面的AddParameterToCommand方法中,我们只需要根据参数名检索paramCollection,就可以得到对应参数的SqlType,而不再需要Schema信息了。
private void AddParameterToCommand(IDbCommand cmd, string paraName) { SqlParameter tmpSqlParameter; tmpSqlParameter = new SqlParameter(); tmpSqlParameter.ParameterName = paraName; tmpSqlParameter.SqlDbType = this.paramCollection[paraName].SqlDbType; tmpSqlParameter.SourceVersion = this.paramCollection[paraName].SourceVersion; tmpSqlParameter.SourceColumn = this.paramCollection[paraName].SourceColumn; tmpSqlParameter.Size = this.paramCollection[paraName].Size; tmpSqlParameter.Direction = this.paramCollection[paraName].Direction; tmpSqlParameter.Precision = this.paramCollection[paraName].Precision; cmd.Parameters.Add(tmpSqlParameter); }
2、通过使用Reflactor反编译DataAdapter类,可以看到里面已经有了一个名为ParameterInput的方法就是根据DataRow中的数据向SqlCommand里面填写参数用的,只是为internal类型。我将其拷贝出来,放到了我的程序代码中发挥作用,不过还需要做一些小的改动。
3、主键信息只能自己手工指定,由于在程序中没有获取数据库的Schema信息,所以只能手工指定。如果需要了解Schema信息,也可以自己设计程序实现。David Sceppa在《ADO.NET Core Reference》一书提供的工具中给了一个DataAdapter Builder工具,是用VB.net写的,里面实现的读取数据库表的Schema信息功能,可供参考。代码可以从书配套光盘CD下载(http://www.wenyuan.com.cn/Soft_Show.asp?SoftID=34)。
4、在RowUpdating事件中,我们可以通过SqlRowUpdatingEventArgs得到所需的DataRow和TableMapping以及相关的StatementType信息。然后利用这些信息实现动态生成更新命令,最后填入所需参数并执行。于是,我们便实现了KeyAndModified方式更新数据。
private void daStudent_RowUpdating(object sender, System.Data.SqlClient.SqlRowUpdatingEventArgs args) { //-- 在这段程序中我们只拦截UPDATE命令 if(args.StatementType != StatementType.Update) return; string strMsg; strMsg = "Beginning Update...\r\n"; strMsg += "\r\n----------------------------\r\n"; SqlCommand cmd = GenerateUpdateCommand(args.Row, args.TableMapping, true); cmd.Connection = args.Command.Connection; cmd.Transaction = args.Command.Transaction; args.Command = cmd; string p = ParameterInput(args.Command.Parameters, args.StatementType, args.Row, args.TableMapping); strMsg += "Command Text:\r\n\r\n"; strMsg += args.Command.CommandText + "\r\n\r\n----------------------------\r\n\r\n"; strMsg += p; this.txtMessages.Text = strMsg; } private SqlCommand GenerateUpdateCommand(DataRow row, DataTableMapping mappings, bool RefreshRowAfterUpdate) { SqlCommand cmd = new SqlCommand(); string paraName=""; string TableName = mappings.DataSetTable; StringBuilder commandTextBuilder = new StringBuilder(); StringBuilder modifiedFieldsBuilder = new StringBuilder(); StringBuilder whereClauseBuilder = new StringBuilder(); StringBuilder tableFieldsBuilder = new StringBuilder(); commandTextBuilder.Append("UPDATE " + Delimit(TableName)); foreach(DataColumnMapping map in mappings.ColumnMappings) { // 判断该列是否发生修改 if(!row[map.DataSetColumn, DataRowVersion.Current].Equals(row[map.DataSetColumn, DataRowVersion.Original])) { if (modifiedFieldsBuilder.ToString() != "") { modifiedFieldsBuilder.Append(", "); } paraName = "@" + map.SourceColumn + "Current"; modifiedFieldsBuilder.Append(Delimit(map.SourceColumn) + " = " + paraName); AddParameterToCommand(cmd, paraName); } } commandTextBuilder.Append(" SET " + modifiedFieldsBuilder.ToString()); // 添加主键约束 paraName = "@" + this.keyFieldName + "Original"; whereClauseBuilder.Append(Delimit(this.keyFieldName) + " = " + paraName); AddParameterToCommand(cmd, paraName); foreach(DataColumnMapping map in mappings.ColumnMappings) { // 判断该列是否发生修改 if(!row[map.DataSetColumn, DataRowVersion.Current].Equals(row[map.DataSetColumn, DataRowVersion.Original])) { if (whereClauseBuilder.ToString() != "") { whereClauseBuilder.Append(" AND "); } paraName = "@" + map.SourceColumn + "Original"; whereClauseBuilder.Append(Delimit(map.SourceColumn) + " = " + paraName); AddParameterToCommand(cmd, paraName); } } commandTextBuilder.Append(" WHERE " + whereClauseBuilder.ToString()); if (RefreshRowAfterUpdate) { foreach(DataColumnMapping map in mappings.ColumnMappings) { if (tableFieldsBuilder.ToString() != "") { tableFieldsBuilder.Append(", "); } tableFieldsBuilder.Append(Delimit(map.SourceColumn)); } tableFieldsBuilder.Append(" FROM " + TableName + " WHERE " + Delimit(this.keyFieldName) + " = @" + this.keyFieldName + "Original"); commandTextBuilder.Append("; \r\n\r\nSELECT " + tableFieldsBuilder.ToString()); } cmd.CommandText = commandTextBuilder.ToString(); return cmd; }
通过这种方式更新数据可以减少Update命令的复杂度,尤其是在网络带宽受到限制的时候,能够减少命令长度,提高通讯效率。但程序编写比较麻烦。在上面的程序中仅仅实现了UPDATE命令的KeyAndModifiedField更新,更完整的代码可留给读者自己去设计。贴张图上来: