存储过程(Stored Procedure)是数据库编程中的重要组成部分,它提供了一种在数据库服务器端预编译和执行SQL语句的高效方式。在C#应用程序中调用存储过程可以带来性能优势、安全提升和业务逻辑封装等好处。本文将全面介绍C#中调用存储过程的各种方法和技术。
一、存储过程基础
1.1 存储过程的优势
- 性能优化:预编译执行计划,减少网络传输
- 安全性:减少SQL注入风险,实现权限控制
- 代码复用:多个应用共享同一业务逻辑
- 维护便利:修改存储过程无需重新部署应用
1.2 基本调用流程
- 创建数据库连接
- 配置SqlCommand对象
- 添加输入/输出参数
- 执行存储过程
- 处理返回结果
二、基础调用方法
2.1 使用ADO.NET调用
using System.Data;
using System.Data.SqlClient;
public DataTable GetEmployeeData(int departmentId)
{
DataTable resultTable = new DataTable();
using (SqlConnection connection = new SqlConnection(connectionString))
{
// 创建Command对象并指定为存储过程
using (SqlCommand command = new SqlCommand("usp_GetEmployeesByDepartment", connection))
{
command.CommandType = CommandType.StoredProcedure;
// 添加输入参数
command.Parameters.AddWithValue("@DepartmentID", departmentId);
// 添加输出参数(可选)
command.Parameters.Add("@EmployeeCount", SqlDbType.Int).Direction = ParameterDirection.Output;
connection.Open();
// 执行存储过程
using (SqlDataReader reader = command.ExecuteReader())
{
resultTable.Load(reader);
}
// 获取输出参数值
int employeeCount = Convert.ToInt32(command.Parameters["@EmployeeCount"].Value);
Console.WriteLine($"总员工数: {employeeCount}");
}
}
return resultTable;
}
2.2 参数类型详解
参数方向 | 描述 | 示例 |
---|---|---|
ParameterDirection.Input | 输入参数(默认) | command.Parameters.AddWithValue("@param", value) |
ParameterDirection.Output | 输出参数 | command.Parameters.Add("@param", SqlDbType.Int).Direction = ParameterDirection.Output |
ParameterDirection.InputOutput | 输入输出参数 | 同上,设置Direction为InputOutput |
ParameterDirection.ReturnValue | 返回值参数 | 第一个参数通常为返回值参数 |
三、高级调用技术
3.1 处理复杂参数
表值参数(Table-Valued Parameters)
// 创建DataTable作为表值参数
DataTable employeeTable = new DataTable();
employeeTable.Columns.Add("EmployeeID", typeof(int));
employeeTable.Columns.Add("Name", typeof(string));
// 添加数据行...
// 配置SqlCommand
using (SqlCommand command = new SqlCommand("usp_BulkUpdateEmployees", connection))
{
command.CommandType = CommandType.StoredProcedure;
SqlParameter param = command.Parameters.AddWithValue("@EmployeeList", employeeTable);
param.SqlDbType = SqlDbType.Structured;
param.TypeName = "dbo.EmployeeTableType"; // 数据库中定义的表类型
connection.Open();
command.ExecuteNonQuery();
}
自定义类型参数
// 使用JSON传递复杂对象
var employeeData = new {
Name = "张三",
Details = new {
Position = "开发工程师",
Level = "Senior"
}
};
string jsonParam = JsonConvert.SerializeObject(employeeData);
using (SqlCommand command = new SqlCommand("usp_ProcessEmployeeData", connection))
{
command.CommandType = CommandType.StoredProcedure;
command.Parameters.AddWithValue("@JsonData", jsonParam);
// 执行...
}
3.2 事务处理
using (SqlConnection connection = new SqlConnection(connectionString))
{
connection.Open();
SqlTransaction transaction = connection.BeginTransaction();
try
{
using (SqlCommand command = new SqlCommand("usp_UpdateEmployeeSalary", connection, transaction))
{
command.CommandType = CommandType.StoredProcedure;
command.Parameters.AddWithValue("@EmployeeID", 1001);
command.Parameters.AddWithValue("@SalaryIncrease", 500);
command.ExecuteNonQuery();
}
// 可以继续调用其他存储过程...
transaction.Commit();
}
catch
{
transaction.Rollback();
throw;
}
}
四、使用ORM框架调用存储过程
4.1 Entity Framework Core
// 返回实体列表
var employees = context.Employees
.FromSqlRaw("EXEC usp_GetEmployeesByDepartment @DepartmentID={0}", departmentId)
.ToList();
// 执行无返回的存储过程
context.Database.ExecuteSqlRaw("EXEC usp_UpdateEmployeeStatus @EmployeeID={0}, @NewStatus={1}",
employeeId, newStatus);
// 使用输出参数
var param = new SqlParameter("@ResultCode", SqlDbType.Int)
{
Direction = ParameterDirection.Output
};
context.Database.ExecuteSqlRaw("EXEC usp_ComplexOperation @Param1={0}, @ResultCode={1} OUT",
param1, param);
int resultCode = (int)param.Value;
4.2 Dapper调用存储过程
using Dapper;
// 简单调用
var employees = connection.Query<Employee>(
"usp_GetEmployeesByDepartment",
new { DepartmentID = departmentId },
commandType: CommandType.StoredProcedure);
// 使用输出参数
var parameters = new DynamicParameters();
parameters.Add("@DepartmentID", departmentId);
parameters.Add("@EmployeeCount", dbType: DbType.Int32, direction: ParameterDirection.Output);
connection.Execute(
"usp_GetDepartmentInfo",
parameters,
commandType: CommandType.StoredProcedure);
int employeeCount = parameters.Get<int>("@EmployeeCount");
五、性能优化技巧
5.1 参数嗅探问题解决
// 使用OPTION(RECOMPILE)提示
using (SqlCommand command = new SqlCommand(
"EXEC usp_GetSalesData @StartDate, @EndDate OPTION(RECOMPILE)",
connection))
{
// 参数设置...
}
// 或使用本地变量"屏蔽"参数
ALTER PROCEDURE usp_GetSalesData
@StartDate DATETIME,
@EndDate DATETIME
AS
BEGIN
DECLARE @LocalStartDate DATETIME = @StartDate
DECLARE @LocalEndDate DATETIME = @EndDate
SELECT * FROM Sales
WHERE SaleDate BETWEEN @LocalStartDate AND @LocalEndDate
END
5.2 异步调用
// 异步执行存储过程
public async Task<List<Employee>> GetEmployeesAsync(int departmentId)
{
using (SqlConnection connection = new SqlConnection(connectionString))
{
await connection.OpenAsync();
return (await connection.QueryAsync<Employee>(
"usp_GetEmployeesByDepartment",
new { DepartmentID = departmentId },
commandType: CommandType.StoredProcedure)).ToList();
}
}
5.3 连接池优化
// 连接字符串中配置连接池
string connectionString =
"Server=myServer;Database=myDB;Integrated Security=True;" +
"Max Pool Size=100;Min Pool Size=10;Connection Timeout=30;";
六、安全最佳实践
6.1 参数化调用
// 绝对不要这样拼接SQL
string unsafeSql = $"EXEC usp_GetUser '{userInput}'";
// 总是使用参数化查询
using (SqlCommand command = new SqlCommand("usp_GetUser", connection))
{
command.CommandType = CommandType.StoredProcedure;
command.Parameters.AddWithValue("@Username", userInput);
// 执行...
}
6.2 权限最小化
-- 为应用创建专用数据库用户
CREATE LOGIN AppUser WITH PASSWORD = 'ComplexP@ssw0rd!';
CREATE USER AppUser FOR LOGIN AppUser;
-- 只授予执行特定存储过程的权限
GRANT EXECUTE ON usp_GetEmployees TO AppUser;
GRANT EXECUTE ON usp_UpdateEmployee TO AppUser;
6.3 敏感数据保护
// 使用SqlParameter的敏感参数特性
SqlParameter param = new SqlParameter("@Password", SqlDbType.NVarChar, 128)
{
Value = encryptedPassword,
IsSensitive = true // 防止在日志中记录
};
command.Parameters.Add(param);
七、实际应用场景
7.1 分页查询存储过程
// 调用分页存储过程
public PagedResult<Employee> GetEmployeesPaged(int pageIndex, int pageSize)
{
var parameters = new DynamicParameters();
parameters.Add("@PageIndex", pageIndex);
parameters.Add("@PageSize", pageSize);
parameters.Add("@TotalCount", dbType: DbType.Int32, direction: ParameterDirection.Output);
using (var connection = new SqlConnection(connectionString))
{
var employees = connection.Query<Employee>(
"usp_GetEmployeesPaged",
parameters,
commandType: CommandType.StoredProcedure).ToList();
int totalCount = parameters.Get<int>("@TotalCount");
return new PagedResult<Employee>(employees, pageIndex, pageSize, totalCount);
}
}
7.2 批量数据处理
// 批量更新员工状态
public void BulkUpdateEmployeeStatus(List<int> employeeIds, string newStatus)
{
DataTable idTable = new DataTable();
idTable.Columns.Add("ID", typeof(int));
foreach (var id in employeeIds)
{
idTable.Rows.Add(id);
}
using (SqlConnection connection = new SqlConnection(connectionString))
using (SqlCommand command = new SqlCommand("usp_BulkUpdateEmployeeStatus", connection))
{
command.CommandType = CommandType.StoredProcedure;
SqlParameter param = command.Parameters.AddWithValue("@IDList", idTable);
param.SqlDbType = SqlDbType.Structured;
param.TypeName = "dbo.IntIDList";
command.Parameters.AddWithValue("@NewStatus", newStatus);
connection.Open();
command.ExecuteNonQuery();
}
}
八、调试与问题排查
8.1 捕获存储过程错误
try
{
using (SqlCommand command = new SqlCommand("usp_ComplexOperation", connection))
{
command.CommandType = CommandType.StoredProcedure;
// 设置参数...
connection.Open();
command.ExecuteNonQuery();
}
}
catch (SqlException ex)
{
// 处理特定错误代码
if (ex.Number == 50000) // 用户定义的错误
{
Console.WriteLine("业务逻辑错误: " + ex.Message);
}
else
{
Console.WriteLine($"数据库错误 #{ex.Number}: {ex.Message}");
}
// 访问错误集合
foreach (SqlError error in ex.Errors)
{
Console.WriteLine($"严重度 {error.Class}, 状态 {error.State}: {error.Message}");
}
}
8.2 性能分析
-- 在SQL Server中查看存储过程执行统计
SELECT
OBJECT_NAME(st.objectid) AS ProcedureName,
qs.execution_count,
qs.total_elapsed_time/1000 AS total_elapsed_time_ms,
qs.total_logical_reads,
qs.last_execution_time
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) st
WHERE st.objectid IS NOT NULL
ORDER BY qs.total_elapsed_time DESC;
结语
通过本文的介绍,您应该已经掌握了C#中调用存储过程的核心技术,包括:
- 基础调用方法:使用ADO.NET的基本模式
- 高级技术:表值参数、事务处理、输出参数
- ORM集成:EF Core和Dapper中的存储过程调用
- 性能优化:参数嗅探处理、异步调用
- 安全实践:参数化查询、权限控制
在实际项目中使用存储过程时,建议:
- 将业务逻辑合理地分配在应用层和数据库层
- 为存储过程编写清晰的文档说明
- 建立统一的错误处理机制
- 定期审查存储过程的性能
存储过程作为数据库编程的强大工具,在正确使用时可以显著提升应用程序的性能和安全性。希望本指南能帮助您在C#项目中高效、安全地使用存储过程。