最近在调试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 {
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的一个引用,本质上是一个封箱的过程。