.Text中SqlParameter引起的Bug

   这篇文章中讨论的Bug是最近博客园频繁出现的两个异常:
1、"ArgumentException The SqlParameter with ParameterName '@EntryID' is already contained by another SqlParameterCollection."
2、"ArgumentException The SqlParameter with ParameterName
'@ItemCount' is already contained by another SqlParameterCollection."
    这个Bug我在.Text中的Bug 文章中已经讨论过,但当时并没有找出问题的真正原因,文章中的解决方法也没有解决问题。最后,在韩磊的指点下才消除了这个Bug。在这篇文章中, 我谈谈自己的一些心得。
    当出现上述两个异常时, 我首先想到的是找出异常抛出的位置。根据异常的内容,异常应该发生在数库访问代码中, 也就是SQLHelper.cs, .Text中所有的数据库访问都是通过SqlHelper。但在SqlHelper中并没有对异常进行捕获, 只有一处try...catch语句, 所做也只是在catch中关闭SqlConnection。为了发现异常在哪抛出的,需要增加捕获异常的代码,根据异常内容,可以判断出是在执行ExecuteReader过程中出现的异常。我就在ExecuteReader(SqlConnection connection, SqlTransaction transaction, CommandType commandType, string commandText, SqlParameter[] commandParameters, SqlConnectionOwnership connectionOwnership)中增加了try...catch代码,在catch中通过Logger.LogManager.CreateExceptionLog(e,"ExecuteReader Exception");将异常写入日志。在这里我犯一个错误:.Text中的日志是存在数据库中,CreateExceptionLog是要向数据库写入日志信息,但在catch中, 数据库访问已经出现了异常,再进行数据库写入操作,只会继续抛出异常。我这样寻找异常发生的位置显然是徒劳无获的,而且增加了新的异常,更不利于问题的解决。
    对于这两个异常,我自然而然认为问题出在SqlHelper中,所以我要在SqlHelper捕获到异常发生的位置。“.Text中的Bug ”文章中想通过在finally中cmd.Parameters.Clear();来解决问题,可是异常仍然存在。所以我决定首先要捕获发生异常的位置,我发现捕获异常代码的问题后,改成了将.Text的日志写入xml文件,这样在发生数据库操作异常,也能将异常写入日志。经过这样的更改,我终于找到了异常发生的位置,异常发生在SqlHelper.AttachParameters中,在循环执行command.Parameters.Add(p);时产生了异常。
    我开始仔细分析command.Parameters.Add(p);,实际就是SqlParameter.Add(SqlParameter value),用Reflector查看了一下其中的代码,没什么收获。这时我开始怀疑是多线程并发执行command.Parameters.Add(p)引起的问题。可command并不是共享资源,在每次执行ExecuteReader时,command都是一个新的实例(SqlCommand cmd = new SqlCommand();).不应该存在同步问题。
    我百思不得其解,于是与韩磊交流了这个问题,开始他也没想到解决的方法。后来,突然他问我是不是只有
“@EntryID”与“@ItemCount”会出现错误,其他的存储过程参数没有出现?根据日志,我的回答是“是”。然后,他告诉问题出在.Text的SqlDataProvider中,他以前遇到过并且解决了这个问题,只不过一时忘记了。
    问题出在SqlDataProvider中的两个私有静态成员DefaultEntryQueryParameter、DefaultEntryParameters,类型都是SqlParameter[]。它们在SqlDataProviderr 的构造函数中被初始化:
    DefaultEntryQueryParameters = BuildDefaultEntryQueryParameters();
    DefaultEntryParameters = BuildDefaultEntryParameters();
解决方法就是去掉构造函数中的初如化,在每处调用DefaultEntryQueryParameters或DefaultEntryParameters的地方,重新初始化它们,也就是使它们指向新的SqlParameter[]实例。更改代码最简单的方法就是将这两个私有成员改成属性:
public static
SqlParameter[] DefaultEntryParameters
       
{
           
get

           
{
               
return
BuildDefaultEntryParameters();
            }

        }
public static
SqlParameter[] DefaultEntryQueryParameters
       
{
           
get

           
{
               
return
BuildDefaultEntryQueryParameters();
            }

        }

下面,我来分析一下原因,不对之处请大家指正。
既然异常是在执行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),
就会引发异常。

那为什么出现SqlParameterCollection使用同一个SqlParameter的情况?

  罪魁祸首就是两个私有静态成员DefaultEntryQueryParameter、DefaultEntryParameters,私有静态成员被类的所有实例共享。在SqlDataProvider的不同实例的生命周期中, 都共享这两个静态成员。当SqlDataProvider的多个实例同时执行command.Parameters.Add(p)操作时,如果都用到DefaultEntryQueryParameter或DefaultEntryParameters,就会引发异常"...is already contained by another SqlParameterCollection."
  解决这个问题的方法除了前面的每次调用DefaultEntryQueryParameter或
DefaultEntryParameters,重新创建SqlParameter[],也可以将DefaultEntryQueryParameter与DefaultEntryParameters变成非静态私有成员,但这种在\方法在多线程的情况下,也会出现同样的问题。最安全的方法就是每次使用SqlParameter,都重新创建SqlParameter的实例。 
  这个bug一直存在.Text中,那为什么现在才发现?而且有很多.Text的网站为什么没有发现这个Bug?因为这个Bug只会出
现在ExecuteReader中,所以即使发生异常,对系统没什么影响,只要重新刷新一下就行了。而且这个异常只会出现在SqlDataProvider的多个实例同时执行command.Parameters.Add(p)操作时,同时发生的概率与网站的访问量有关。以前博客园很少出现这个异常,最近因为博客园访问量变大,同时执行command.Parameters.Add(p)的概率变高了,所以异常出现的次数也变多了。

从这个Bug中,我们应该吸取两个教训:
1、慎用私有静态成员。
2、安全地使用SqlParameter,每次使用,每次新建。

非常感谢韩磊在解决这个问题中给予指点。

posted on 2004-06-30 09:06 dudu 阅读(2981) 评论(7)  编辑 收藏 网摘 所属分类: .Text相关

评论

#1楼 2004-06-30 09:22 unruledboy(灵感之源)

mmm,我对dudu的专业精神甚是佩服。   回复  引用    

#2楼 2004-06-30 10:32 韩磊[未注册用户]

谢谢dudu,与我们分享这么好的经验!   回复  引用    

#3楼 2004-09-01 11:54 白面青铜

这是一个低级错误,明显是重复的把参数加入了SqlParameterCollection,
这个错误我早在.NET Beta1年代犯过两次.
  回复  引用    

#4楼 2004-10-28 09:52 求救啊

你们好啊,
我的整个程序都是用sqlhelper,来做数据访问的,开发速度到是挺快的,但现在出问题了,

所有的程序在我的机器上访问都没问题,

但别人的机器访问就出问题了,问题是他们的机器都说找不到我机器上sql seaver2000上的我定义的存储过程,怎么办啊,请大侠帮忙啊



请帮忙啊
  回复  引用    

#5楼 2005-07-15 16:59 cw[未注册用户]

不错,谢谢大哥的分享,向你学习啊!   回复  引用    

#6楼 2006-04-11 14:42 大哥.小李      

:)

Parameters怪怪的。

我自己建立了一个DbObject类,在类里有一个方法,方法里有一个非静态的局部变量SqlCommand command.

我在类MbMsg里,实现了一个方法,方法里实例化了两个DbObject,然后都调用了那含有SqlCommand command.的方法,结果就抱错了。

今天看了这篇文章就解决了。

唉。
  回复  引用  查看    

#7楼 2008-05-09 13:47 山郎      

太感谢了,我遇到这个问题都好长时间了,知道问题在哪,就是搞不明白,今天总算明白了,谢谢dudu,佩服   回复  引用  查看    




发表评论

昵称: [登录] [注册]

主页:

邮箱:(仅博主可见)

评论内容:

  登录  注册

[使用Ctrl+Enter键快速提交评论]

0 19628




相关文章:

相关链接:

导航

公告

人生的真正价值在于从何种程度与何种意义上摆脱自我!
明天继续更新评论功能
<2005年7月>
262728293012
3456789
10111213141516
17181920212223
24252627282930
31123456

统计

与我联系

搜索

 

常用链接

留言簿

随笔分类

随笔档案

新闻分类

相册

HJ

朋友的博客

网站收藏

小组

友情链接

最新随笔

最新评论

阅读排行榜

评论排行榜

60天内阅读排行