一个小问题,把我难了半小时,发现答案后,把我差点没气晕!
昨天下午看www.asp.net提供的IBuySpy示例程序(相信很多人都知道IBuySpy和Stater Kits吧)。Register.aspx提供用户注册。我记得以前开发e-commerce网站时,对于用户注册要加很多审核条件的。想看看它是怎么完成的。
代码很简单:该方法响应用户单击“注册Register”按钮。
private void RegisterBtn_Click(object sender, System.Web.UI.ImageClickEventArgs e) {
// Only attempt a login if all form fields on the page are valid
if (Page.IsValid == true) {
// Store off old temporary shopping cart ID
IBuySpy.ShoppingCartDB shoppingCart = new IBuySpy.ShoppingCartDB();
String tempCartId = shoppingCart.GetShoppingCartId();
// Add New Customer to CustomerDB database
IBuySpy.CustomersDB accountSystem = new IBuySpy.CustomersDB();
String customerId = accountSystem.AddCustomer(Name.Text, Email.Text, Password.Text);
if (customerId != "") {
// Set the user's authentication name to the customerId
FormsAuthentication.SetAuthCookie(customerId, false);
// Migrate any existing shopping cart items into the permanent shopping cart
shoppingCart.MigrateCart(tempCartId, customerId);
// Store the user's fullname in a cookie for personalization purposes
Response.Cookies["IBuySpy_FullName"].Value = Server.HtmlEncode(Name.Text);
// Redirect browser back to shopping cart page
Response.Redirect("ShoppingCart.aspx");
}
else {
MyError.Text = "Registration failed: That email address is already registered.
";
}
}
}
AddCustomer()方法就是完成添加用户功能的。如果返回ID为空,则添加失败,否则添加Cookie,并将临时用户的购物车内容迁移到已注册用户的购物车中。最后转向购物车页面。
注意到:添加失败显示的信息是:电子邮件已经注册。
试验了一下,Register对同名的用户名没有限制,但限制了同名的e-mail。因为登陆是通过E-mail来完成的。
因为AddCustomer()方法是在CustomerDB类中,而CustomerDB类无非是调用一个存储过程而已,添加新用户的存储过程如下:
CREATE Procedure CustomerAdd
(
@FullName nvarchar(50),
@Email nvarchar(50),
@Password nvarchar(50),
@CustomerID int OUTPUT
)
AS
INSERT INTO Customers
(
FullName,
EMailAddress,
Password
)
VALUES
(
@FullName,
@Email,
@Password
)
SELECT
@CustomerID = @@Identity
除了要求CustomerID需要唯一性外(数据库是将CustomerID设为自增的),对E-mail并没有特别的要求啊。
在回头看CustomerDB类,也确实是老老实实地用SqlCommand调用该存储过程。传递参数的时候也没有限制。然而当我试图用相同的E-mail注册,就是通不过。显然程序是受到条件限制的。我反复看他自身的帮助文档,又以为是Validator控件来限制。不幸的是,我看清楚了,对于接收输入的电子邮件文本框,Validator控件只是用RequiredField控件要求此项不能为空,用RegularExpressionValidator,加入正则表达式要求输入的e-mail地址合法。并没有搜索数据表,判断e-mail是否重复的功能啊!我真是很困惑啊!
最后我接连注册了几个用户,再打开Sql Server的企业管理器,发现Customer数据表的CustomerID的增长有问题。按道理,随着用户的增加,只要中间没删除,每条记录的ID应是依此递增的。但我却发现我新增加的ID号中间有“断层”。后来分析规律发现,当我新增加一个用户时,例如ID为20;如果此时我连续增加两条e-mail为重复的用户,结果当然是没有添加成功。但当我再添加一个合法用户时,此时ID变为了23。中间恰好空出了2个ID。又试了一次,仍然如此,绝对不是巧合。
恍然大悟,明白了这其实是数据库本身的限制。不过看了Customer表,除了ID加了标识种子,e-mail和name相同没有什么异样。那么难道是trigger在搞鬼?打开触发器一看,根本就没有。
那么为什么呢?思索良久,看到菜单中选项“管理索引”。忽然想到索引就是要求列的值是唯一的。打开来看,果然,表对E-mail添加了索引,选项中有“唯一性”。于是我为用户名也添加了索引,结果自然是也不能添加同名用户了。不过提示信息还是邮件地址错误。为什么呢,代码是就是这样写的嘛。
就是这样一个小问题,折磨了我这么久。归根结底,还是在于自己对数据库太无知了。用这种方法避免重复,自然会很好,不用花时间在存储过程中判断(我以前就是这样做的)。当然也有缺点,就是当要限制多个字段时,例如name,email甚至IDCard,那么给用户的提示警告信息就很麻烦了。因为他们得到的结果都是customerID为空。也许可以用Catch捕获异常来判断,也许索引值不一,抛出的异常不一样?
一会儿试试!
试了一下,原来的CustomerDB类中添加新用户的方法是:
public String AddCustomer(string fullName, string email, string password)
{
// Create Instance of Connection and Command Object
SqlConnection myConnection = new SqlConnection(ConfigurationSettings.AppSettings["ConnectionString"]);
SqlCommand myCommand = new SqlCommand("CustomerAdd", myConnection);
// Mark the Command as a SPROC
myCommand.CommandType = CommandType.StoredProcedure;
// Add Parameters to SPROC
SqlParameter parameterFullName = new SqlParameter("@FullName", SqlDbType.NVarChar, 50);
parameterFullName.Value = fullName;
myCommand.Parameters.Add(parameterFullName);
SqlParameter parameterEmail = new SqlParameter("@Email", SqlDbType.NVarChar, 50);
parameterEmail.Value = email;
myCommand.Parameters.Add(parameterEmail);
SqlParameter parameterPassword = new SqlParameter("@Password", SqlDbType.NVarChar, 50);
parameterPassword.Value = password;
myCommand.Parameters.Add(parameterPassword);
SqlParameter parameterCustomerID = new SqlParameter("@CustomerID", SqlDbType.Int, 4);
parameterCustomerID.Direction = ParameterDirection.Output;
myCommand.Parameters.Add(parameterCustomerID);
try {
myConnection.Open();
myCommand.ExecuteNonQuery();
myConnection.Close();
// Calculate the CustomerID using Output Param from SPROC
int customerId = (int)parameterCustomerID.Value;
return customerId.ToString();
}
catch {
return String.Empty;
}
为了捕获异常,我修改为:
catch (System.Exception e)
{
return e.Message.ToString()
}
然后将Register.aspx后面的代码改为:
if (customerId != "") {
MyError.Text = customerId;
// Set the user's authentication name to the customerId
FormsAuthentication.SetAuthCookie(customerId, false);
// Migrate any existing shopping cart items into the permanent shopping cart
shoppingCart.MigrateCart(tempCartId, customerId);
// Store the user's fullname in a cookie for personalization purposes
Response.Cookies["IBuySpy_FullName"].Value = Server.HtmlEncode(Name.Text);
// Redirect browser back to shopping cart page
//注释掉转移页面功能,使我能看到错误的提示信息!
//Response.Redirect("ShoppingCart.aspx");
}
我是将CustomerId的值赋给MyError的Text属性中。这里CustomerId是存储过程返回的Id值,为string类型。如果出现异常,其内容应为异常的描述。MyError是提示错误信息的Label控件。这样一改后,当然不好了,因为会将错误信息显示在页面上,不过我现在的目的只是要得到异常的内容。然后我在Customer数据表中添加了对FullName的索引,设置为唯一值。
运行后,我试图添加一同名非法用户,显示异常为:
不能在具有唯一索引 'ix_Name' 的对象 'Customers' 中插入重复键的行。语句已终止。
如果我添加一同e-Mail的非法用户,则显示为:
约束 'IX_Customers'。不能在对象 'Customers' 中插入重复键。语句已终止。
可惜没有所谓异常号,否则可以直接根据异常号,判断是何错误,然后在MyError中显示出来。当然通过字符串也可以办到。就是通过string.IndexOf()去判断是否有该索引就可以了。因为对于每一个索引,其名字是不同的。例如ix_Name代表用户名FullName的索引,IX_Customers代表EmailAddress的索引。根据在异常中能否找到该索引字来判断提示信息。
虽然可以办到,不过比较麻烦,不知道有没有更好的办法呢?我的意思是说不要用存储过程去判断是否有重复列。