.NET 特性Attribute[三]
刚刚接触Attribute的朋友可能很难想明白Attribute究竟有何用处,以及在应用程序中我们如何使用Attribute。在这节,通过一个例子来演示Attribute的用处!
针对数据库的操作通常是新手入门时经常接触的例子。在应用程序中,我们经常会遇见以下代码
{
SqlCommand command=new SqlCommand("AddCustomer", connection);
command.CommandType=CommandType.StoredProcedure;
command.Parameters.Add("@CustomerName",SqlDbType.NVarChar,50).Value=customerName;
command.Parameters.Add("@country",SqlDbType.NVarChar,20).Value=country;
command.Parameters.Add("@Province",SqlDbType.NVarChar,20).Value=province;
command.Parameters.Add("@City",SqlDbType.NVarChar,20).Value=city;
command.Parameters.Add("@Address",SqlDbType.NVarChar,60).Value=address;
command.Parameters.Add("@Telephone",SqlDbType.NvarChar,16).Value=telephone;
command.Parameters.Add("@CustomerId",SqlDbType.Int,4).Direction=ParameterDirection.Output;
connection.Open();
command.ExecuteNonQuery();
connection.Close();
int custId=(int)command.Parameters["@CustomerId"].Value;
return custId;
}
上面的代码,创建一个Command实例,然后添加存储过程的参数,然后调用ExecuteMonQuery方法执行数据的插入操作,最后返回CustomerId。从代码可以看到参数的添加command.Parameters.Add()是一种重复单调的工作。而往往一个项目中,经常有一百多个甚至成千的存储过程,每次编写这样的代码是否会感到枯燥无味?作为开发人员的你,是否会想偷偷懒?
当然,现在喜庆的是出现了非常多的Coder,代码生成器的出现无疑可以解决上面的问题,但在这里,从技术交流的角度,给出一个另类的作法。就是使用Attribute。
在开始之前先理清我们的思路。我们是为了根据方法的参数及方法的名称,自动的给我们“加工”成一个Command对象。而不需要我们自己手工一个个的Add进去。
第1步:创建一个SqlParameterAttribute类
这个类很简单,就普通的一个对象类差不多,就拥有一些属性。但不同的是他继承了Attribute。这样也就是说,这个类可以被当作特性来使用!注意类头的一行[ AttributeUsage(AttributeTargets.Parameter) ]表明该特性,只可作用在方法的参数上!
第2步:考虑到方法中并不是每个参数都是存储过程需要的。所以我们可以再定义一个Attribute来标识这样的参数。
public sealed class NonCommandParameterAttribute : Attribute
{
//仅作为标识使用 用于方法参数中,并不是执行SQL所需要的参数
}
第3步:到此我们已经完成了SQL的参数Attribute的定义,在创建Command对象生成器之前,让我们考虑这样的一个事实,那就是如果我们数据访问层调用的不是存储过程,也就是说Command的CommandType不是存储过程,而是带有参数的SQL语句,我们想让我们的方法一样可以适合这种情况,同样我们仍然可以使用Attribute,定义一个用于方法的Attribute来表明该方法中的生成的Command的CommandType是存储过程还是SQL文本,并且我们同样希望以相对简单的方式来定义存储过程的名称。下面是新定义的Attribute的代码:
public sealed class SqlCommandMethodAttribute : Attribute
{
private string commandText; //SQL执行文本
private CommandType commandType; //SQL执行类型
public SqlCommandMethodAttribute( CommandType commandType, string commandText)
{
this.commandType=commandType;
this.commandText=commandText;
}
public SqlCommandMethodAttribute(CommandType commandType) : this(commandType, null){}
public string CommandText
{
get
{
return commandText==null ? string.Empty : commandText;
}
set
{
commandText=value;
}
}
public CommandType CommandType
{
get
{
return commandType;
}
set
{
commandType=value;
}
}
}
第4步:SqlCommandGenerator类的设计
SqlCommandGEnerator类的设计思路就是通过反射得到方法的参数,使用被SqlCommandParameterAttribute标记的参数来装配一个Command实例。
using System.Reflection;
using System.Data;
using System.Data.SqlClient;
using Debug = System.Diagnostics.Debug;
using StackTrace = System.Diagnostics.StackTrace;
namespace AttributeDemo
{
/// <summary>
/// SqlCommandGenerator 的摘要说明。
/// </summary>
public class SqlCommandGenerator
{
//私有构造器,不允许使用无参数的构造器构造一个实例
private SqlCommandGenerator()
{
throw new NotSupportedException();
}
//静态只读字段,定义用于返回值的参数名称
public static readonly string ReturnValueParameterName = "RETURN_VALUE";
//静态只读字段,用于不带参数的存储过程
public static readonly object[] NoValues = new object[] {};
public static SqlCommand GenerateCommand(SqlConnection connection,
MethodInfo method, object[] values)
{
//如果没有指定方法名称,从堆栈帧得到方法名称
if (method == null)
method = (MethodInfo) (new StackTrace().GetFrame(1).GetMethod());
// 获取方法传进来的SqlCommandMethodAttribute
// 为了使用该方法来生成一个Command对象,要求有这个Attribute。
SqlCommandMethodAttribute commandAttribute =
(SqlCommandMethodAttribute) Attribute.GetCustomAttribute(method, typeof(SqlCommandMethodAttribute));
//Debug.Assert(commandAttribute != null);
//Debug.Assert(commandAttribute.CommandType == CommandType.StoredProcedure || commandAttribute.CommandType == CommandType.Text);
// 创建一个SqlCommand对象,同时通过指定的attribute对它进行配置。
SqlCommand command = new SqlCommand();
command.Connection = connection;
command.CommandType = commandAttribute.CommandType;
// 获取command的文本,如果没有指定,那么使用方法的名称作为存储过程名称
if (commandAttribute.CommandText.Length == 0)
{
//Debug.Assert(commandAttribute.CommandType == CommandType.StoredProcedure);
command.CommandText = method.Name;
}
else
{
command.CommandText = commandAttribute.CommandText;
}
// 调用GeneratorCommandParameters方法,生成command参数,
GenerateCommandParameters(command, method, values);
//同时添加一个返回值参数
command.Parameters.Add(ReturnValueParameterName, SqlDbType.Int).Direction =ParameterDirection.ReturnValue;
return command;
}
private static void GenerateCommandParameters(
SqlCommand command, MethodInfo method, object[] values)
{
// 得到所有的参数,通过循环一一进行处理。
ParameterInfo[] methodParameters = method.GetParameters();
int paramIndex = 0;
foreach (ParameterInfo paramInfo in methodParameters)
{
// 忽略掉参数被标记为[NonCommandParameter ]的参数
if (Attribute.IsDefined(paramInfo, typeof(NonCommandParameterAttribute)))
continue;
// 获取参数的SqlParameter attribute,如果没有指定,那么就创建一个并使用它的缺省设置。
SqlParameterAttribute paramAttribute = (SqlParameterAttribute) Attribute.GetCustomAttribute(
paramInfo, typeof(SqlParameterAttribute));
if (paramAttribute == null)
paramAttribute = new SqlParameterAttribute();
//使用attribute的设置来配置一个参数对象。使用那些已经定义的参数值。如果没有定义,那么就从方法
// 的参数来推断它的参数值。
SqlParameter sqlParameter = new SqlParameter();
//参数名称
if (paramAttribute.IsNameDefined)
sqlParameter.ParameterName = paramAttribute.Name;
else
sqlParameter.ParameterName = paramInfo.Name;
if (!sqlParameter.ParameterName.StartsWith("@"))
sqlParameter.ParameterName = "@" + sqlParameter.ParameterName;
//参数类型
if (paramAttribute.IsTypeDefined)
sqlParameter.SqlDbType = paramAttribute.SqlDbType;
//参数长度
if (paramAttribute.IsSizeDefined)
sqlParameter.Size = paramAttribute.Size;
//参数小数位数
if (paramAttribute.IsScaleDefined)
sqlParameter.Scale = paramAttribute.Scale;
//参数最大位数
if (paramAttribute.IsPrecisionDefined)
sqlParameter.Precision = paramAttribute.Precision;
//参数方向
if (paramAttribute.IsDirectionDefined)
{
sqlParameter.Direction = paramAttribute.Direction;
}
else
{
if (paramInfo.ParameterType.IsByRef)
{
//如果参数是引用类型 则认为是可以输出类型
sqlParameter.Direction = paramInfo.IsOut ?
ParameterDirection.Output :
ParameterDirection.InputOutput;
}
else
{
sqlParameter.Direction = ParameterDirection.Input;
}
}
// 检测是否提供的足够的参数对象值
//Debug.Assert(paramIndex < values.Length);
//把相应的对象值赋于参数。
sqlParameter.Value = values[paramIndex];
command.Parameters.Add(sqlParameter);
paramIndex++;
}
//检测是否有多余的参数对象值
//Debug.Assert(paramIndex == values.Length);
}
}
}
好了,一切就是如此。现在我们就可以使用这些特性来修改在文章开始给出的示例!
public void UP_Customer_ADD( [NonCommandParameter] SqlConnection connection,
[SqlParameter(50)] string customerName,
[SqlParameter(20)] string country,
[SqlParameter(20)] string province,
[SqlParameter(20)] string city,
[SqlParameter(60)] string address,
[SqlParameter(16)] string telephone,
out int customerId )
{
customerId=0; //需要初始化输出参数
//调用Command生成器生成SqlCommand实例
SqlCommand command = SqlCommandGenerator.GenerateCommand( connection, null, new object[]{customerName,country,province,city,address,telephone,customerId } );
connection.Open();
command.ExecuteNonQuery();
connection.Close();
//必须明确返回输出参数的值
customerId=(int)command.Parameters["@CustomerId"].Value;
}
消除了前面所看到的command.Parameters.Add方式。注意方法的头定义的 [SqlCommandMethodAttribute(CommandType.StoredProcedure)]特性,表明该方法执行的是存储过程。那么到这里可能有朋友问,那过程的名称是什么呢,细心看GenerateCommand方法的朋友,可以知道,当没有传入存储过程名称时,会自动以方法的名称作为存储过程的名称,这里是“UP_Customer_ADD” 如果要明确写出存储过程名称,则将方法头改为 [SqlCommandMethodAttribute(CommandType.StoredProcedure,"sp_yourProc")],则sp_yourProc即为将要执行的存储过程名称!
为讲解的完整性下面给出程序的调用及SQL表及存储过程
private void btnSave_Click(object sender, System.EventArgs e)
{
int addID = 0;
SqlConnection con = new SqlConnection("server=.;database=fyDemo;uid=sa;pwd=sa;");
UP_Customer_ADD(con,"姓名","国家","湖北省","武汉","地址","133333333333",out addID);
MessageBox.Show("添加成功:ID为"+addID.ToString());
}