生成存储过程包装器方法和相关包装器类





3.00/5 (7投票s)
此工具可帮助开发人员生成其 ADO.NET 存储过程包装器方法以及任何相关的强类型数据对象类。

引言
为应用程序需要调用的每个存储过程创建一个包装器方法通常是一个好主意。然后可以将这些方法分组到一个数据访问实用程序类中。这种方法可以提高类型安全性和可移植性。
这些方法通常调用 Command.ExecuteScalar
、Command.ExecuteNonQuery
和 Command.ExecuteReader
ADO.NET 方法。它们还执行诸如检查 Connection
对象是否仍处于连接状态、将存储过程参数添加到 Command
对象、确保 DataReader
对象被正确处置等任务。
平均而言,一个编写良好的包装 ExecuteReader
调用的方法大约需要五十行代码!编写这些方法很容易侵占数据密集型项目中访问许多存储过程的整体开发时间。
开发人员通常会从其他包装器方法中复制粘贴代码,然后修改代码以适应存储过程调用。此过程通常由于人为错误而导致错误。
我考虑到,由于这些方法大多共享一个通用的编程模式;因此,应该有可能向代码生成工具描述您的存储过程的外观,并让该工具生成这些方法。不至于像 AutoSproc 那么复杂,只是一个让开发人员指定存储过程细节的轻量级工具,然后生成包装器方法代码。
实现
此工具是使用 ASP.NET 2.0 开发的。它基于用户提供的信息做出代码生成决策——几乎与人类开发人员做出编码决策的方式相同。
它支持 .NET 1.1 和 .NET 2.0 功能,例如,如果选择了 .NET 2.0,它会为可为空的字段创建可为空的变量。它支持 SQL Server 和 ODBC 数据提供程序。
实际的代码生成代码(无双关语)位于 APP_Code\MethodGen.cs 中,而用户界面代码位于 sprocmethodgen.aspx 中。
代码生成代码可以轻松地被另一个具有不同用户界面的应用程序使用(例如,一个从实际数据库架构中提供大部分输入的桌面应用程序)。它也可以轻松修改以遵循不同的编程模式或支持更多 ADO.NET 功能。
代码生成代码的核心位于 MethodGenerator
类中的 GenerateMethod
、GenerateTryClause
和 GenerateResultsWrapperClass
方法中。GenerateMethod
方法生成方法中不变化的部分,例如将参数添加到命令对象的部分。它还调用 GenerateTryClause
方法,并可以选择调用 GenerateResultsWrapperClass
方法。
GenerateTryClause
方法生成方法中的大型 try
子句,该子句因所选的执行类型而异。
GenerateResultsWrapperClass
方法生成一个类,该类存储 DataReader
返回的结果。(返回一个强类型对象列表比返回一个 DataTable
更好。)
使用工具
本示例使用了 Northwind 数据库中的“按年销售额”存储过程。
1. 运行 ASP.NET 解决方案,然后导航到网页。
该工具也可在 www.ahuwanya.net/tools/sprocmethodgen/ 获得。
2. 指定 .NET 版本、数据提供程序、存储过程名称和执行类型。
3. “按年销售额”存储过程有两个输入参数,因此请指定这两个输入参数。
4. 运行存储过程后,发现结果集中有四个列,因此请指定这四个结果列。
5. 指定将存储结果的类的名称以及生成方法的名称。
6. 点击 生成代码! 按钮生成代码。(您可能需要向下滚动页面才能看到生成的代码)
internal static List<SalesInfo> GetSalesByYear
(DateTime StartDate, DateTime EndDate, SqlConnection DBConn )
{
//TODO: Insert method into your data access utility class
//Check if connection is null
if(DBConn == null)
{
throw new ArgumentNullException("DBConn");
}
//Open connection if it's closed
bool connectionOpened = false;
if(DBConn.State == ConnectionState.Closed)
{
DBConn.Open();
connectionOpened = true;
}
//TODO: Move constant declaration below directly into containing class
const string sprocGetSalesByYear = "[Sales by Year]";
string sproc = sprocGetSalesByYear;
SqlCommand cmd = new SqlCommand(sproc,DBConn);
cmd.CommandType = CommandType.StoredProcedure;
cmd.Parameters.Add("@Beginning_Date",SqlDbType.DateTime ).Value = StartDate;
cmd.Parameters.Add("@Ending_Date",SqlDbType.DateTime ).Value = EndDate;
List<SalesInfo> result = new List<SalesInfo>();
SqlDataReader rdr;
try
{
rdr = cmd.ExecuteReader();
try
{
if (rdr.HasRows)
{
int shippeddateOrdinal =
rdr.GetOrdinal("ShippedDate");
int orderidOrdinal = rdr.GetOrdinal("OrderID");
int subtotalOrdinal = rdr.GetOrdinal("Subtotal");
int yearOrdinal = rdr.GetOrdinal("Year");
while (rdr.Read())
{
// declare variables to store
// retrieved row data
DateTime? shippeddateParam;
int orderidParam;
decimal subtotalParam;
string yearParam;
// get row data
if (rdr.IsDBNull(shippeddateOrdinal))
{
shippeddateParam = null;
}
else
{
shippeddateParam =
rdr.GetDateTime
(shippeddateOrdinal);
}
orderidParam = rdr.GetInt32
(orderidOrdinal);
subtotalParam = rdr.GetDecimal
(subtotalOrdinal);
if (rdr.IsDBNull(yearOrdinal))
{
yearParam = null;
}
else
{
yearParam = rdr.GetString
(yearOrdinal);
}
// add new SalesInfo object to result list
result.Add(new SalesInfo
(shippeddateParam,orderidParam,
subtotalParam,yearParam));
}
}
}
finally
{
rdr.Close();
}
}
catch(Exception ex)
{
//TODO: Handle Exception
throw ex;
}
finally
{
cmd.Dispose();
if(connectionOpened ) // close connection if this method opened it.
{
DBConn.Close();
}
}
return result;
}
[Serializable]
public class SalesInfo
{
//TODO: Integrate this class with any existing data object class
private DateTime? shippeddate;
private int orderid;
private decimal subtotal;
private string year;
public SalesInfo(DateTime? ShippedDate, int OrderID,
decimal SubTotal, string Year)
{
shippeddate = ShippedDate;
orderid = OrderID;
subtotal = SubTotal;
year = Year;
}
public SalesInfo()
{
shippeddate = null;
orderid = 0;
subtotal = 0;
year = null;
}
public DateTime? ShippedDate
{
get { return shippeddate; }
set { shippeddate = value; }
}
public int OrderID
{
get { return orderid; }
set { orderid = value; }
}
public decimal SubTotal
{
get { return subtotal; }
set { subtotal = value; }
}
public string Year
{
get { return year; }
set { year = value; }
}
}
7. 将生成的代码复制到您的项目中。
8. 向您的项目添加以下命名空间。
using System.Data;
using System.Data.SqlClient; //if using SQL Server Data Provider
using System.Data.Odbc; //if using ODBC Provider
using System.Collections.Generic; //if using .NET 2.0 or later
using System.Collections //if using .NET 1.1
9. 在生成的代码中查找 //TODO: 注释并采取相应行动。即使忽略 //TODO: 注释,代码仍然可以工作。
10. 现在,您可以简单地通过以下语句从您的项目访问销售数据。
//Create SQL connection
SqlConnection conn = new SqlConnection(@"Data Source=.\SQLEXPRESS;
Initial Catalog=Northwind;Integrated Security=True");
//Get Sales Information
List<SalesInfo> sales = GetSalesByYear(new DateTime(1992,1,1),
new DateTime(2008,1,1),conn);
注意事项和局限性
显然,此工具无法生成适用于所有可能 ADO.NET 数据库访问场景的代码,但是,生成大部分代码然后根据需要修改代码,总比手动输入所有代码要好得多。
此工具的一些局限性包括:
仅生成 C# 代码: 使此工具实现语言中立的最佳方法是使用 CodeDom 来生成代码,但是,这种方法将使此工具更难维护和扩展——对于此项目的范围来说,这有点大材小用。
幸运的是,对于希望使用此工具的 VB.NET 开发人员来说,有许多 C#-to-VB.NET 代码转换工具可用。
不支持输出参数:此工具仅支持输入参数,并可以选择返回存储过程的返回参数。生成的代码可以手动修改以适应其他类型的参数。
不支持 OLEDB 和 Oracle 数据提供程序:此工具仅为 ODBC 和 SQL Server 数据提供程序生成代码。
仅读取第一个结果集:如果您的存储过程返回多个结果集,一种处理方法是为第一个结果集生成一个方法(选择 ExecuteReader
),然后生成另一个方法,假装第二个结果集实际上是第一个结果集,将读取结果数据位的代码复制并粘贴到调用 rdr.NextResults()
之后的第一种方法中,更改粘贴代码中结果变量的名称,并将其作为 out
参数传递。
为每个返回的结果集重复此操作。
不支持 DbCommand 对象属性:如果您正在寻找 Transaction
、CommandTimeOut
、CommandBehavior
对象等——很容易修改生成的代码以适应这些属性。
不适用于大型结果集:此工具生成的代码将结果集作为 ArrayList
或强类型对象的 List
返回。如果您的存储过程返回了数十万行,此工具的性能将很差,因为它需要存储所有这些行。在这种情况下,您应该编写自己的数据访问方法。
此外,如果您的存储过程返回了数十万行,我建议您考虑实现某种分页机制来减少返回的行数。
历史
- 2008 年 8 月 10 日 -- 原文
- 2008 年 9 月 28 日 -- 添加了检查
null
连接对象的代码。提供了该工具的在线链接