代码改变世界

解决(另一个 SqlParameterCollection 已包含带有 ParameterName“@UserName”的 SqlParameter。)同时讨论CopyTo和Clone (原创)

2006-06-25 14:18  BAsil  阅读(2240)  评论(0编辑  收藏

最近在调试SqlHelper程序的时候发现,利用从SqlCommandBuilder.DerivedParameters(command)得到的command.Parameters(为SqlParameterCollection)传给ExecuteNonQuery中的SqlParameter[],代码如下,

public static SqlParameter[] getParameters(SqlConnection conn,string spName,bool needsreturnvalue) {

SqlCommand command=new SqlCommand(spName,conn);
command.CommandType=CommandType.StoredProcedure; conn.Open();
SqlCommandBuilder.DeriveParameters(command);
conn.Close();
if(!needsreturnvalue)
{
command.Parameters.RemoveAt(0);
}
SqlParameter[] sqlparams=new SqlParameter[command.Parameters.Count];

command.Parameters.CopyTo(sqlparams,0); return sqlparams
;

}

传入ExecuteNonQuery

ExecuteNonQuery(...)
{
...

if(SqlParams!=null)
{

for(int i=0;i<SQLPARAMS.LENGTH;I++)
{
command.Parameters.Add(SqlParams[i]);
}


}



...
}

出现The SqlParameter with ParameterName 'xxxx' is already contained by another SqlParameterCollection

下面讨论解决办法

对用command.Parameters.CopyTo和SqlParameter[]的Clone

(1)SqlParameter[] sqlparams=new SqlParameter[command.Parameters.Count];

command.Parameters.CopyTo(sqlparams,0);

(2)SqlParameter[] sqlparams=new SqlParameter[command.Parameters.Count];

command.Parameters.CopyTo(sqlparams,0);
SqlParameter[] sqlparams1=(SqlParameter[])sqlparams.Clone();(这种方法只是为了说明clone的意思)

CopyTo和数组的Clone都是浅备份(shadow copy)因此仍然和command.Parameters的数组元素指向相同的对象。而当调用ExecuteNonQuery时将得到的SqlParameter数组传给另外的command.Parameters ,而由于垃圾回收机制可能并未将上一个command包括parameters回收。因此理论上两个command.Parameters同时指向相同的对象。而由于framework机制这个是不允许的将会跑出异常。

原因
#region from dudu's blog

既然异常是在执行command.Parameters.Add(p);产生的,那我们要首先分析一下这里为什么会抛出异常?
用Reflector要查看一下SqlParameterCollection.Add的代码:

public SqlParameter Add(SqlParameter value)
...{
this.OnSchemaChanging();
this.AddWithoutEvents(value);
return value;

}

继续看看AddWithoutEvents的代码:

private void AddWithoutEvents(SqlParameter value)
...{
this.Validate(-1, value);
value.Parent = this;
this.ArrayList().Add(value);

}

这里的value.Parent = this;应该引起我们的注意,参数value的Parent属性在SqlParameterCollection.Add
中被改变,这就使SqlParameter value与SqlParameterCollection关联起来,一个SqlParameter value只能
同时属于一个SqlParameterCollection。那我们再看看Validate(-1, value):

internal void Validate(int index, SqlParameter value)
...{
if (value == null)
...{
throw ADP.ParameterNull("value", this, this.ItemType);

}
if (value.Parent != null)
...{
if (this != value.Parent)
...{
throw ADP.ParametersIsNotParent(this.ItemType, value.ParameterName, this);

}
if (index != this.IndexOf(value))
...{
throw ADP.ParametersIsParent(this.ItemType, value.ParameterName, this);

}

}
string text1 = value.ParameterName;
if (!ADP.IsEmpty(text1))
...{
return;

}
index = 1;
do
...{
text1 = string.Concat("Parameter", index.ToString());
index = (index + 1);

}
while ((-1 != this.IndexOf(text1)));
value.ParameterName = text1;

}
  从上面的代码就可以看出异常是如何产生的,如果value被另外一个SqlParameterCollection使用(this != value.Parent),就会引发异常。

#endregion

如果放置commnd.Parameters.Clear()则将Parameters设置为空引用,而framework机制只是限制两个SqlParameterCollection指向同一个对象,并没有限制数组和SqlParameterCollection指向同一个对象。此时单线程调用时不会出错。但因为web本身就是多线程,在多线程的情况下,只有通过deep copy才能避免SqlParameterCollection指向同一个对象,所以此种方法不可行

解决的方法是

SqlParameter[] clonedParameters = new SqlParameter[originalParameters.Length];

for (int i = 0, j = originalParameters.Length; i < j; i++)
{
clonedParameters[i] = (SqlParameter)((ICloneable)originalParameters[i]).Clone();
}
这将执行一个深拷贝(deep copy)不会出现问题


foreach(SqlParameter p in commandparams)
{

// block 1
command.Parameters.Add(p);

// block 2

if(p!=null)
{
if( (p.Direction==ParameterDirection.InputOutput||p.Direction==ParameterDirection.Input) && p.Value==null)
{
p.Value=DBNull.Value;

}

}
}

在上面的一段代码中,block1和block2的顺序不重要,因为只是将p分配给ParametersCollection的一个引用,本质上是一个封箱的过程。