C# 與 SQL Server 同步操作完整指南
前言
在現代應用程式開發中,資料庫操作是不可或缺的一環。本文將深入探討如何在 C# 中使用 SQL Server 進行同步操作,涵蓋從基礎連線到進階查詢的各種技術。無論您是剛開始學習資料庫程式設計的新手,還是希望複習基礎知識的資深開發者,本文都能提供實用的參考。
目錄
環境設定與基礎配置
安裝必要的 NuGet 套件
首先,我們需要在專案中安裝必要的 NuGet 套件。對於 SQL Server 操作,主要有兩個選擇:
- System.Data.SqlClient - 傳統方式,適用於 .NET Framework
- Microsoft.Data.SqlClient - 新版推薦,適用於 .NET Core/.NET 5+
在 .NET Core 或 .NET 5+ 專案中,建議使用 Microsoft.Data.SqlClient:
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.1.5" />
使用 Package Manager Console 安裝:
Install-Package Microsoft.Data.SqlClient
或使用 .NET CLI:
dotnet add package Microsoft.Data.SqlClient
專案結構建議
MyProject/
├── Models/
│ ├── Product.cs
│ ├── Order.cs
│ └── Customer.cs
├── Repositories/
│ ├── IProductRepository.cs
│ ├── ProductRepository.cs
│ └── BaseRepository.cs
├── Services/
│ └── DatabaseService.cs
├── Configuration/
│ └── DatabaseConfig.cs
└── Program.cs
建立資料庫連線
連線字串配置
資料庫連線是所有操作的基礎。以下是不同情境下的連線字串範例:
// Windows 驗證
string connectionString = @"Server=.\SQLEXPRESS;Database=MyDatabase;Trusted_Connection=True;";
// SQL Server 驗證
string connectionString = @"Server=localhost;Database=MyDatabase;User Id=sa;Password=YourPassword;";
// 包含其他選項的完整連線字串
string connectionString = @"Server=localhost;Database=MyDatabase;
User Id=sa;Password=YourPassword;
Encrypt=True;TrustServerCertificate=True;
Connection Timeout=30;MultipleActiveResultSets=True;";
連線管理類別
建立一個專門管理資料庫連線的類別:
using Microsoft.Data.SqlClient;
using System;
namespace MyProject.Configuration
{
public class DatabaseConfig
{
private readonly string _connectionString;
public DatabaseConfig(string connectionString)
{
if (string.IsNullOrWhiteSpace(connectionString))
throw new ArgumentException("連線字串不能為空", nameof(connectionString));
_connectionString = connectionString;
}
public SqlConnection CreateConnection()
{
return new SqlConnection(_connectionString);
}
public bool TestConnection()
{
try
{
using (var connection = CreateConnection())
{
connection.Open();
return true;
}
}
catch (SqlException)
{
return false;
}
}
}
}
基本 CRUD 操作
定義資料模型
首先定義我們的資料模型:
namespace MyProject.Models
{
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public decimal Price { get; set; }
public int Quantity { get; set; }
public DateTime CreatedDate { get; set; }
public DateTime? ModifiedDate { get; set; }
}
}
查詢資料(SELECT)
實作查詢操作的完整範例:
using System;
using System.Collections.Generic;
using System.Data;
using Microsoft.Data.SqlClient;
using MyProject.Models;
namespace MyProject.Repositories
{
public class ProductRepository : IProductRepository
{
private readonly string _connectionString;
public ProductRepository(string connectionString)
{
_connectionString = connectionString;
}
// 取得所有產品
public List<Product> GetAllProducts()
{
var products = new List<Product>();
using (var connection = new SqlConnection(_connectionString))
{
connection.Open();
string query = @"SELECT Id, Name, Description, Price, Quantity,
CreatedDate, ModifiedDate
FROM Products
ORDER BY Name";
using (var command = new SqlCommand(query, connection))
{
using (var reader = command.ExecuteReader())
{
while (reader.Read())
{
products.Add(MapReaderToProduct(reader));
}
}
}
}
return products;
}
// 根據 ID 取得單一產品
public Product GetProductById(int id)
{
using (var connection = new SqlConnection(_connectionString))
{
connection.Open();
string query = @"SELECT Id, Name, Description, Price, Quantity,
CreatedDate, ModifiedDate
FROM Products
WHERE Id = @Id";
using (var command = new SqlCommand(query, connection))
{
command.Parameters.AddWithValue("@Id", id);
using (var reader = command.ExecuteReader())
{
if (reader.Read())
{
return MapReaderToProduct(reader);
}
return null;
}
}
}
}
// 搜尋產品
public List<Product> SearchProducts(string searchTerm)
{
var products = new List<Product>();
using (var connection = new SqlConnection(_connectionString))
{
connection.Open();
string query = @"SELECT Id, Name, Description, Price, Quantity,
CreatedDate, ModifiedDate
FROM Products
WHERE Name LIKE @SearchTerm
OR Description LIKE @SearchTerm
ORDER BY Name";
using (var command = new SqlCommand(query, connection))
{
command.Parameters.AddWithValue("@SearchTerm", $"%{searchTerm}%");
using (var reader = command.ExecuteReader())
{
while (reader.Read())
{
products.Add(MapReaderToProduct(reader));
}
}
}
}
return products;
}
// 輔助方法:將 SqlDataReader 對應到 Product 物件
private Product MapReaderToProduct(SqlDataReader reader)
{
return new Product
{
Id = reader.GetInt32(reader.GetOrdinal("Id")),
Name = reader.GetString(reader.GetOrdinal("Name")),
Description = reader.IsDBNull(reader.GetOrdinal("Description"))
? null
: reader.GetString(reader.GetOrdinal("Description")),
Price = reader.GetDecimal(reader.GetOrdinal("Price")),
Quantity = reader.GetInt32(reader.GetOrdinal("Quantity")),
CreatedDate = reader.GetDateTime(reader.GetOrdinal("CreatedDate")),
ModifiedDate = reader.IsDBNull(reader.GetOrdinal("ModifiedDate"))
? null
: reader.GetDateTime(reader.GetOrdinal("ModifiedDate"))
};
}
}
}
新增資料(INSERT)
插入資料時,務必使用參數化查詢以防止 SQL 注入攻擊:
public int AddProduct(Product product)
{
if (product == null)
throw new ArgumentNullException(nameof(product));
using (var connection = new SqlConnection(_connectionString))
{
connection.Open();
string query = @"INSERT INTO Products
(Name, Description, Price, Quantity, CreatedDate)
VALUES
(@Name, @Description, @Price, @Quantity, @CreatedDate);
SELECT SCOPE_IDENTITY();";
using (var command = new SqlCommand(query, connection))
{
command.Parameters.AddWithValue("@Name", product.Name);
command.Parameters.AddWithValue("@Description",
product.Description ?? (object)DBNull.Value);
command.Parameters.AddWithValue("@Price", product.Price);
command.Parameters.AddWithValue("@Quantity", product.Quantity);
command.Parameters.AddWithValue("@CreatedDate", DateTime.Now);
// 取得新增資料的 ID
var newId = Convert.ToInt32(command.ExecuteScalar());
product.Id = newId;
return newId;
}
}
}
// 批次新增多筆資料
public void AddProducts(List<Product> products)
{
using (var connection = new SqlConnection(_connectionString))
{
connection.Open();
using (var transaction = connection.BeginTransaction())
{
try
{
string query = @"INSERT INTO Products
(Name, Description, Price, Quantity, CreatedDate)
VALUES
(@Name, @Description, @Price, @Quantity, @CreatedDate)";
foreach (var product in products)
{
using (var command = new SqlCommand(query, connection, transaction))
{
command.Parameters.AddWithValue("@Name", product.Name);
command.Parameters.AddWithValue("@Description",
product.Description ?? (object)DBNull.Value);
command.Parameters.AddWithValue("@Price", product.Price);
command.Parameters.AddWithValue("@Quantity", product.Quantity);
command.Parameters.AddWithValue("@CreatedDate", DateTime.Now);
command.ExecuteNonQuery();
}
}
transaction.Commit();
}
catch
{
transaction.Rollback();
throw;
}
}
}
}
更新資料(UPDATE)
更新操作同樣需要使用參數化查詢:
public bool UpdateProduct(Product product)
{
if (product == null)
throw new ArgumentNullException(nameof(product));
using (var connection = new SqlConnection(_connectionString))
{
connection.Open();
string query = @"UPDATE Products
SET Name = @Name,
Description = @Description,
Price = @Price,
Quantity = @Quantity,
ModifiedDate = @ModifiedDate
WHERE Id = @Id";
using (var command = new SqlCommand(query, connection))
{
command.Parameters.AddWithValue("@Id", product.Id);
command.Parameters.AddWithValue("@Name", product.Name);
command.Parameters.AddWithValue("@Description",
product.Description ?? (object)DBNull.Value);
command.Parameters.AddWithValue("@Price", product.Price);
command.Parameters.AddWithValue("@Quantity", product.Quantity);
command.Parameters.AddWithValue("@ModifiedDate", DateTime.Now);
int rowsAffected = command.ExecuteNonQuery();
return rowsAffected > 0;
}
}
}
// 部分更新(只更新特定欄位)
public bool UpdateProductPrice(int productId, decimal newPrice)
{
using (var connection = new SqlConnection(_connectionString))
{
connection.Open();
string query = @"UPDATE Products
SET Price = @Price,
ModifiedDate = @ModifiedDate
WHERE Id = @Id";
using (var command = new SqlCommand(query, connection))
{
command.Parameters.AddWithValue("@Id", productId);
command.Parameters.AddWithValue("@Price", newPrice);
command.Parameters.AddWithValue("@ModifiedDate", DateTime.Now);
int rowsAffected = command.ExecuteNonQuery();
return rowsAffected > 0;
}
}
}
刪除資料(DELETE)
刪除操作相對簡單,但要注意處理相關的外鍵約束:
public bool DeleteProduct(int id)
{
using (var connection = new SqlConnection(_connectionString))
{
connection.Open();
// 先檢查是否可以刪除(例如:檢查是否有相關訂單)
string checkQuery = @"SELECT COUNT(*) FROM OrderDetails WHERE ProductId = @Id";
using (var checkCommand = new SqlCommand(checkQuery, connection))
{
checkCommand.Parameters.AddWithValue("@Id", id);
int relatedOrders = (int)checkCommand.ExecuteScalar();
if (relatedOrders > 0)
{
throw new InvalidOperationException($"無法刪除產品,因為有 {relatedOrders} 筆相關訂單");
}
}
// 執行刪除
string deleteQuery = "DELETE FROM Products WHERE Id = @Id";
using (var command = new SqlCommand(deleteQuery, connection))
{
command.Parameters.AddWithValue("@Id", id);
int rowsAffected = command.ExecuteNonQuery();
return rowsAffected > 0;
}
}
}
// 軟刪除(標記為已刪除而非實際刪除)
public bool SoftDeleteProduct(int id)
{
using (var connection = new SqlConnection(_connectionString))
{
connection.Open();
string query = @"UPDATE Products
SET IsDeleted = 1,
DeletedDate = @DeletedDate
WHERE Id = @Id";
using (var command = new SqlCommand(query, connection))
{
command.Parameters.AddWithValue("@Id", id);
command.Parameters.AddWithValue("@DeletedDate", DateTime.Now);
int rowsAffected = command.ExecuteNonQuery();
return rowsAffected > 0;
}
}
}
進階技巧與最佳實踐
使用 SqlDataAdapter 和 DataTable
對於需要離線操作資料的情況,可以使用 SqlDataAdapter 和 DataTable:
public class DataTableExample
{
private readonly string _connectionString;
public DataTableExample(string connectionString)
{
_connectionString = connectionString;
}
// 載入資料到 DataTable
public DataTable GetProductsDataTable()
{
using (var connection = new SqlConnection(_connectionString))
{
string query = "SELECT * FROM Products WHERE IsDeleted = 0";
var adapter = new SqlDataAdapter(query, connection);
var dataTable = new DataTable("Products");
adapter.Fill(dataTable);
return dataTable;
}
}
// 使用 DataTable 進行批次更新
public void UpdateProductsDataTable(DataTable dataTable)
{
using (var connection = new SqlConnection(_connectionString))
{
var adapter = new SqlDataAdapter();
// 設定 SELECT 命令
adapter.SelectCommand = new SqlCommand(
"SELECT * FROM Products", connection);
// 自動產生 INSERT, UPDATE, DELETE 命令
var builder = new SqlCommandBuilder(adapter);
// 執行更新
adapter.Update(dataTable);
}
}
// 進階:自訂更新命令
public void CustomUpdateProductsDataTable(DataTable dataTable)
{
using (var connection = new SqlConnection(_connectionString))
{
var adapter = new SqlDataAdapter();
// SELECT 命令
adapter.SelectCommand = new SqlCommand(
"SELECT Id, Name, Price FROM Products", connection);
// UPDATE 命令
adapter.UpdateCommand = new SqlCommand(
@"UPDATE Products
SET Name = @Name, Price = @Price, ModifiedDate = GETDATE()
WHERE Id = @Id", connection);
adapter.UpdateCommand.Parameters.Add("@Name", SqlDbType.NVarChar, 100, "Name");
adapter.UpdateCommand.Parameters.Add("@Price", SqlDbType.Decimal, 0, "Price");
adapter.UpdateCommand.Parameters.Add("@Id", SqlDbType.Int, 0, "Id");
// 執行更新
adapter.Update(dataTable);
}
}
}
交易處理
當需要執行多個相關聯的資料庫操作時,使用交易可以確保資料的一致性:
public class OrderService
{
private readonly string _connectionString;
public OrderService(string connectionString)
{
_connectionString = connectionString;
}
// 建立訂單(包含訂單主檔和明細)
public int CreateOrder(Order order)
{
using (var connection = new SqlConnection(_connectionString))
{
connection.Open();
SqlTransaction transaction = null;
try
{
transaction = connection.BeginTransaction();
// 1. 插入訂單主檔
string insertOrderQuery = @"
INSERT INTO Orders (CustomerId, OrderDate, TotalAmount)
VALUES (@CustomerId, @OrderDate, @TotalAmount);
SELECT SCOPE_IDENTITY();";
int orderId;
using (var command = new SqlCommand(insertOrderQuery, connection, transaction))
{
command.Parameters.AddWithValue("@CustomerId", order.CustomerId);
command.Parameters.AddWithValue("@OrderDate", order.OrderDate);
command.Parameters.AddWithValue("@TotalAmount", order.TotalAmount);
orderId = Convert.ToInt32(command.ExecuteScalar());
}
// 2. 插入訂單明細並更新庫存
foreach (var detail in order.OrderDetails)
{
// 插入訂單明細
string insertDetailQuery = @"
INSERT INTO OrderDetails (OrderId, ProductId, Quantity, Price)
VALUES (@OrderId, @ProductId, @Quantity, @Price)";
using (var command = new SqlCommand(insertDetailQuery, connection, transaction))
{
command.Parameters.AddWithValue("@OrderId", orderId);
command.Parameters.AddWithValue("@ProductId", detail.ProductId);
command.Parameters.AddWithValue("@Quantity", detail.Quantity);
command.Parameters.AddWithValue("@Price", detail.Price);
command.ExecuteNonQuery();
}
// 更新產品庫存
string updateStockQuery = @"
UPDATE Products
SET Quantity = Quantity - @Quantity
WHERE Id = @ProductId AND Quantity >= @Quantity";
using (var command = new SqlCommand(updateStockQuery, connection, transaction))
{
command.Parameters.AddWithValue("@ProductId", detail.ProductId);
command.Parameters.AddWithValue("@Quantity", detail.Quantity);
int affected = command.ExecuteNonQuery();
if (affected == 0)
{
throw new InvalidOperationException(
$"產品 {detail.ProductId} 庫存不足");
}
}
}
// 3. 記錄交易日誌
string logQuery = @"
INSERT INTO TransactionLog (OrderId, Action, ActionDate)
VALUES (@OrderId, @Action, @ActionDate)";
using (var command = new SqlCommand(logQuery, connection, transaction))
{
command.Parameters.AddWithValue("@OrderId", orderId);
command.Parameters.AddWithValue("@Action", "OrderCreated");
command.Parameters.AddWithValue("@ActionDate", DateTime.Now);
command.ExecuteNonQuery();
}
// 提交交易
transaction.Commit();
return orderId;
}
catch (Exception ex)
{
// 發生錯誤時回滾交易
transaction?.Rollback();
throw new Exception("建立訂單失敗", ex);
}
}
}
// 使用儲存點的複雜交易
public void ComplexTransaction()
{
using (var connection = new SqlConnection(_connectionString))
{
connection.Open();
SqlTransaction transaction = connection.BeginTransaction();
try
{
// 第一個操作
ExecuteCommand("INSERT INTO Table1...", connection, transaction);
// 設定儲存點
transaction.Save("SavePoint1");
try
{
// 第二個操作(可能失敗)
ExecuteCommand("INSERT INTO Table2...", connection, transaction);
}
catch
{
// 只回滾到儲存點
transaction.Rollback("SavePoint1");
}
// 第三個操作
ExecuteCommand("INSERT INTO Table3...", connection, transaction);
transaction.Commit();
}
catch
{
transaction.Rollback();
throw;
}
}
}
private void ExecuteCommand(string query, SqlConnection connection, SqlTransaction transaction)
{
using (var command = new SqlCommand(query, connection, transaction))
{
command.ExecuteNonQuery();
}
}
}
儲存程序的使用
儲存程序可以提高效能並增強安全性:
public class StoredProcedureExample
{
private readonly string _connectionString;
public StoredProcedureExample(string connectionString)
{
_connectionString = connectionString;
}
// 呼叫簡單的儲存程序
public List<Product> GetProductsByCategory(int categoryId)
{
var products = new List<Product>();
using (var connection = new SqlConnection(_connectionString))
{
connection.Open();
using (var command = new SqlCommand("sp_GetProductsByCategory", connection))
{
command.CommandType = CommandType.StoredProcedure;
command.Parameters.AddWithValue("@CategoryId", categoryId);
using (var reader = command.ExecuteReader())
{
while (reader.Read())
{
products.Add(MapReaderToProduct(reader));
}
}
}
}
return products;
}
// 呼叫具有輸出參數的儲存程序
public void GetProductStats(out int totalProducts, out decimal averagePrice)
{
using (var connection = new SqlConnection(_connectionString))
{
connection.Open();
using (var command = new SqlCommand("sp_GetProductStats", connection))
{
command.CommandType = CommandType.StoredProcedure;
// 輸出參數
var totalParam = new SqlParameter("@TotalProducts", SqlDbType.Int)
{
Direction = ParameterDirection.Output
};
var avgParam = new SqlParameter("@AveragePrice", SqlDbType.Decimal)
{
Direction = ParameterDirection.Output,
Precision = 18,
Scale = 2
};
command.Parameters.Add(totalParam);
command.Parameters.Add(avgParam);
command.ExecuteNonQuery();
totalProducts = (int)totalParam.Value;
averagePrice = (decimal)avgParam.Value;
}
}
}
// 呼叫回傳值的儲存程序
public int ExecuteStoredProcWithReturnValue(string procedureName, params SqlParameter[] parameters)
{
using (var connection = new SqlConnection(_connectionString))
{
connection.Open();
using (var command = new SqlCommand(procedureName, connection))
{
command.CommandType = CommandType.StoredProcedure;
// 加入參數
if (parameters != null)
{
command.Parameters.AddRange(parameters);
}
// 加入回傳值參數
var returnParam = new SqlParameter("@ReturnValue", SqlDbType.Int)
{
Direction = ParameterDirection.ReturnValue
};
command.Parameters.Add(returnParam);
command.ExecuteNonQuery();
return (int)returnParam.Value;
}
}
}
private Product MapReaderToProduct(SqlDataReader reader)
{
// 實作省略(同前面的範例)
return new Product();
}
}
對應的儲存程序範例:
-- 簡單查詢儲存程序
CREATE PROCEDURE sp_GetProductsByCategory
@CategoryId INT
AS
BEGIN
SET NOCOUNT ON;
SELECT p.Id, p.Name, p.Description, p.Price, p.Quantity,
p.CreatedDate, p.ModifiedDate
FROM Products p
INNER JOIN ProductCategories pc ON p.Id = pc.ProductId
WHERE pc.CategoryId = @CategoryId
AND p.IsDeleted = 0
ORDER BY p.Name;
END
GO
-- 具有輸出參數的儲存程序
CREATE PROCEDURE sp_GetProductStats
@TotalProducts INT OUTPUT,
@AveragePrice DECIMAL(18,2) OUTPUT
AS
BEGIN
SET NOCOUNT ON;
SELECT @TotalProducts = COUNT(*),
@AveragePrice = AVG(Price)
FROM Products
WHERE IsDeleted = 0;
END
GO
-- 具有回傳值的儲存程序
CREATE PROCEDURE sp_UpdateProductStock
@ProductId INT,
@Quantity INT,
@Operation CHAR(1) -- 'A' for Add, 'R' for Remove
AS
BEGIN
SET NOCOUNT ON;
DECLARE @CurrentStock INT;
-- 取得目前庫存
SELECT @CurrentStock = Quantity
FROM Products
WHERE Id = @ProductId;
-- 檢查產品是否存在
IF @CurrentStock IS NULL
RETURN -1; -- 產品不存在
-- 更新庫存
IF @Operation = 'A'
BEGIN
UPDATE Products
SET Quantity = Quantity + @Quantity,
ModifiedDate = GETDATE()
WHERE Id = @ProductId;
END
ELSE IF @Operation = 'R'
BEGIN
-- 檢查庫存是否足夠
IF @CurrentStock < @Quantity
RETURN -2; -- 庫存不足
UPDATE Products
SET Quantity = Quantity - @Quantity,
ModifiedDate = GETDATE()
WHERE Id = @ProductId;
END
ELSE
RETURN -3; -- 無效的操作
RETURN 0; -- 成功
END
GO
效能優化建議
連線池管理
SQL Server 預設會使用連線池,但我們可以透過連線字串進行調整:
public class ConnectionPoolingExample
{
// 優化的連線字串
private const string OptimizedConnectionString = @"
Server=localhost;
Database=MyDatabase;
User Id=sa;
Password=YourPassword;
Min Pool Size=5;
Max Pool Size=100;
Connect Timeout=30;
Pooling=true;
MultipleActiveResultSets=true;";
// 監控連線池狀態
public void MonitorConnectionPool()
{
using (var connection = new SqlConnection(OptimizedConnectionString))
{
connection.StateChange += (sender, e) =>
{
Console.WriteLine($"連線狀態變更: {e.OriginalState} -> {e.CurrentState}");
};
connection.Open();
// 取得連線池資訊
var poolInfo = connection.RetrieveStatistics();
Console.WriteLine($"連線池統計資訊:");
Console.WriteLine($"- 目前連線數: {poolInfo["NumberOfActiveConnections"]}");
Console.WriteLine($"- 池中連線數: {poolInfo["NumberOfPooledConnections"]}");
Console.WriteLine($"- 非池連線數: {poolInfo["NumberOfNonPooledConnections"]}");
}
}
}
批次操作
當需要插入大量資料時,使用批次操作可以顯著提升效能:
public class BulkOperations
{
private readonly string _connectionString;
public BulkOperations(string connectionString)
{
_connectionString = connectionString;
}
// 使用 SqlBulkCopy 進行大量插入
public void BulkInsertProducts(List<Product> products)
{
using (var connection = new SqlConnection(_connectionString))
{
connection.Open();
using (var bulkCopy = new SqlBulkCopy(connection))
{
// 設定目標資料表
bulkCopy.DestinationTableName = "Products";
// 設定批次大小和逾時時間
bulkCopy.BatchSize = 1000;
bulkCopy.BulkCopyTimeout = 300; // 5 分鐘
// 建立 DataTable
var dataTable = CreateProductDataTable(products);
// 設定欄位對應
bulkCopy.ColumnMappings.Add("Name", "Name");
bulkCopy.ColumnMappings.Add("Description", "Description");
bulkCopy.ColumnMappings.Add("Price", "Price");
bulkCopy.ColumnMappings.Add("Quantity", "Quantity");
bulkCopy.ColumnMappings.Add("CreatedDate", "CreatedDate");
// 事件處理
bulkCopy.SqlRowsCopied += (sender, e) =>
{
Console.WriteLine($"已複製 {e.RowsCopied} 筆資料");
};
// 執行批次插入
bulkCopy.WriteToServer(dataTable);
}
}
}
// 進階:使用 Table-Valued Parameters
public void BulkInsertWithTVP(List<Product> products)
{
using (var connection = new SqlConnection(_connectionString))
{
connection.Open();
// 建立 DataTable
var table = CreateProductDataTable(products);
using (var command = new SqlCommand("sp_BulkInsertProducts", connection))
{
command.CommandType = CommandType.StoredProcedure;
// 使用 Table-Valued Parameter
var parameter = command.Parameters.AddWithValue("@Products", table);
parameter.SqlDbType = SqlDbType.Structured;
parameter.TypeName = "dbo.ProductTableType";
command.ExecuteNonQuery();
}
}
}
// 批次更新
public void BatchUpdateProducts(List<Product> products)
{
const int batchSize = 100;
using (var connection = new SqlConnection(_connectionString))
{
connection.Open();
for (int i = 0; i < products.Count; i += batchSize)
{
var batch = products.Skip(i).Take(batchSize);
var query = BuildBatchUpdateQuery(batch);
using (var command = new SqlCommand(query, connection))
{
AddBatchParameters(command, batch);
command.ExecuteNonQuery();
}
}
}
}
private DataTable CreateProductDataTable(List<Product> products)
{
var dataTable = new DataTable();
// 定義結構
dataTable.Columns.Add("Name", typeof(string));
dataTable.Columns.Add("Description", typeof(string));
dataTable.Columns.Add("Price", typeof(decimal));
dataTable.Columns.Add("Quantity", typeof(int));
dataTable.Columns.Add("CreatedDate", typeof(DateTime));
// 加入資料
foreach (var product in products)
{
dataTable.Rows.Add(
product.Name,
product.Description,
product.Price,
product.Quantity,
DateTime.Now
);
}
return dataTable;
}
private string BuildBatchUpdateQuery(IEnumerable<Product> products)
{
var sb = new StringBuilder();
int index = 0;
foreach (var product in products)
{
sb.AppendLine($@"
UPDATE Products
SET Name = @Name{index},
Price = @Price{index},
Quantity = @Quantity{index},
ModifiedDate = GETDATE()
WHERE Id = @Id{index};");
index++;
}
return sb.ToString();
}
private void AddBatchParameters(SqlCommand command, IEnumerable<Product> products)
{
int index = 0;
foreach (var product in products)
{
command.Parameters.AddWithValue($"@Id{index}", product.Id);
command.Parameters.AddWithValue($"@Name{index}", product.Name);
command.Parameters.AddWithValue($"@Price{index}", product.Price);
command.Parameters.AddWithValue($"@Quantity{index}", product.Quantity);
index++;
}
}
}
對應的 SQL Server 設定:
-- 建立 Table Type
CREATE TYPE dbo.ProductTableType AS TABLE
(
Name NVARCHAR(100),
Description NVARCHAR(500),
Price DECIMAL(18,2),
Quantity INT,
CreatedDate DATETIME
);
GO
-- 使用 Table-Valued Parameter 的儲存程序
CREATE PROCEDURE sp_BulkInsertProducts
@Products dbo.ProductTableType READONLY
AS
BEGIN
SET NOCOUNT ON;
INSERT INTO Products (Name, Description, Price, Quantity, CreatedDate)
SELECT Name, Description, Price, Quantity, CreatedDate
FROM @Products;
END
GO
查詢優化技巧
public class QueryOptimization
{
private readonly string _connectionString;
public QueryOptimization(string connectionString)
{
_connectionString = connectionString;
}
// 使用分頁查詢
public List<Product> GetProductsPaged(int pageNumber, int pageSize, string sortColumn = "Id")
{
var products = new List<Product>();
using (var connection = new SqlConnection(_connectionString))
{
connection.Open();
// 使用 OFFSET-FETCH (SQL Server 2012+)
string query = $@"
SELECT Id, Name, Description, Price, Quantity, CreatedDate, ModifiedDate
FROM Products
WHERE IsDeleted = 0
ORDER BY {sortColumn}
OFFSET @Offset ROWS
FETCH NEXT @PageSize ROWS ONLY";
using (var command = new SqlCommand(query, connection))
{
command.Parameters.AddWithValue("@Offset", (pageNumber - 1) * pageSize);
command.Parameters.AddWithValue("@PageSize", pageSize);
using (var reader = command.ExecuteReader())
{
while (reader.Read())
{
products.Add(MapReaderToProduct(reader));
}
}
}
}
return products;
}
// 使用 NOLOCK 提示(謹慎使用)
public List<Product> GetProductsNoLock()
{
var products = new List<Product>();
using (var connection = new SqlConnection(_connectionString))
{
connection.Open();
// NOLOCK 可能讀取到未提交的資料
string query = @"
SELECT Id, Name, Price, Quantity
FROM Products WITH (NOLOCK)
WHERE IsDeleted = 0";
using (var command = new SqlCommand(query, connection))
{
using (var reader = command.ExecuteReader())
{
while (reader.Read())
{
products.Add(MapReaderToProduct(reader));
}
}
}
}
return products;
}
// 使用索引提示
public Product GetProductByIdWithIndex(int id)
{
using (var connection = new SqlConnection(_connectionString))
{
connection.Open();
// 強制使用特定索引
string query = @"
SELECT Id, Name, Price, Quantity
FROM Products WITH (INDEX(IX_Products_Id))
WHERE Id = @Id";
using (var command = new SqlCommand(query, connection))
{
command.Parameters.AddWithValue("@Id", id);
using (var reader = command.ExecuteReader())
{
if (reader.Read())
{
return MapReaderToProduct(reader);
}
return null;
}
}
}
}
// 執行計畫分析
public void AnalyzeQueryPlan(string query)
{
using (var connection = new SqlConnection(_connectionString))
{
connection.Open();
// 開啟執行計畫
using (var command = new SqlCommand("SET STATISTICS IO ON", connection))
{
command.ExecuteNonQuery();
}
using (var command = new SqlCommand("SET STATISTICS TIME ON", connection))
{
command.ExecuteNonQuery();
}
// 執行查詢
using (var command = new SqlCommand(query, connection))
{
using (var reader = command.ExecuteReader())
{
while (reader.Read())
{
// 讀取資料
}
}
}
// 關閉執行計畫
using (var command = new SqlCommand("SET STATISTICS IO OFF", connection))
{
command.ExecuteNonQuery();
}
using (var command = new SqlCommand("SET STATISTICS TIME OFF", connection))
{
command.ExecuteNonQuery();
}
}
}
private Product MapReaderToProduct(SqlDataReader reader)
{
// 實作省略
return new Product();
}
}
錯誤處理與日誌記錄
完善的錯誤處理機制是穩定應用程式的關鍵:
using System;
using System.Data;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Logging;
public class RobustDatabaseService
{
private readonly ILogger<RobustDatabaseService> _logger;
private readonly string _connectionString;
private readonly int _commandTimeout;
public RobustDatabaseService(
ILogger<RobustDatabaseService> logger,
string connectionString,
int commandTimeout = 30)
{
_logger = logger;
_connectionString = connectionString;
_commandTimeout = commandTimeout;
}
// 通用的執行方法,包含重試邏輯
public T ExecuteWithRetry<T>(
Func<SqlConnection, T> operation,
int maxRetries = 3,
int delayMilliseconds = 1000)
{
int retryCount = 0;
while (retryCount < maxRetries)
{
try
{
using (var connection = new SqlConnection(_connectionString))
{
connection.Open();
_logger.LogDebug("開啟資料庫連線成功");
var result = operation(connection);
_logger.LogDebug("資料庫操作執行成功");
return result;
}
}
catch (SqlException ex) when (IsTransientError(ex))
{
retryCount++;
if (retryCount >= maxRetries)
{
_logger.LogError(ex,
"達到最大重試次數 {MaxRetries},操作失敗",
maxRetries);
throw;
}
int delay = delayMilliseconds * retryCount;
_logger.LogWarning(
"發生暫時性錯誤 {ErrorNumber}: {Message}," +
"將在 {Delay} 毫秒後重試 ({RetryCount}/{MaxRetries})",
ex.Number, ex.Message, delay, retryCount, maxRetries);
System.Threading.Thread.Sleep(delay);
}
catch (SqlException ex)
{
// 非暫時性錯誤,記錄並拋出
LogSqlException(ex);
throw;
}
catch (Exception ex)
{
_logger.LogError(ex, "執行資料庫操作時發生未預期的錯誤");
throw;
}
}
throw new InvalidOperationException("不應該到達這裡");
}
// 判斷是否為暫時性錯誤
private bool IsTransientError(SqlException ex)
{
// SQL Server 暫時性錯誤代碼
int[] transientErrors = new[]
{
1205, // Deadlock
1222, // Lock request timeout
2601, // Duplicate key (在某些情況下可重試)
8645, // Timeout waiting for memory resource
49918, // Cannot process request. Not enough resources
49919, // Cannot process create or update request
49920, // Cannot process request. Too many operations
4060, // Cannot open database
40143, // Connection was terminated
40613, // Database is currently unavailable
40501, // Service is currently busy
40540, // Service has encountered an error
40197, // Service has encountered an error processing request
10929, // Resource ID exceeded limit
10928, // Resource limit reached
10060, // Network error
10054, // Connection forcibly closed
10053, // Connection aborted
233, // Connection initialization error
64 // Connection was successfully established but then failed
};
return Array.Exists(transientErrors, e => e == ex.Number);
}
// 詳細記錄 SQL 異常
private void LogSqlException(SqlException ex)
{
_logger.LogError(ex,
"SQL 錯誤 - 編號: {Number}, 嚴重性: {Class}, " +
"狀態: {State}, 程序: {Procedure}, 行號: {LineNumber}",
ex.Number, ex.Class, ex.State,
ex.Procedure ?? "N/A", ex.LineNumber);
// 記錄所有錯誤
foreach (SqlError error in ex.Errors)
{
_logger.LogError(
"SQL 錯誤詳細: 編號 {Number}, 訊息: {Message}, " +
"來源: {Source}, 程序: {Procedure}",
error.Number, error.Message,
error.Source, error.Procedure ?? "N/A");
}
}
// 安全執行查詢,包含參數記錄
public T ExecuteQuery<T>(
string query,
Func<SqlDataReader, T> mapper,
params SqlParameter[] parameters)
{
return ExecuteWithRetry(connection =>
{
using (var command = new SqlCommand(query, connection))
{
command.CommandTimeout = _commandTimeout;
if (parameters != null && parameters.Length > 0)
{
command.Parameters.AddRange(parameters);
// 記錄查詢和參數(注意不要記錄敏感資料)
_logger.LogDebug("執行查詢: {Query}", query);
foreach (var param in parameters)
{
if (!IsSensitiveParameter(param.ParameterName))
{
_logger.LogDebug("參數 {Name} = {Value}",
param.ParameterName, param.Value);
}
}
}
using (var reader = command.ExecuteReader())
{
return mapper(reader);
}
}
});
}
// 判斷是否為敏感參數
private bool IsSensitiveParameter(string parameterName)
{
string[] sensitiveNames = { "password", "pwd", "secret", "token", "key" };
return Array.Exists(sensitiveNames,
name => parameterName.ToLower().Contains(name));
}
// 執行非查詢命令,回傳受影響的資料列數
public int ExecuteNonQuery(string query, params SqlParameter[] parameters)
{
return ExecuteWithRetry(connection =>
{
using (var command = new SqlCommand(query, connection))
{
command.CommandTimeout = _commandTimeout;
if (parameters != null && parameters.Length > 0)
{
command.Parameters.AddRange(parameters);
}
var affected = command.ExecuteNonQuery();
_logger.LogInformation(
"執行非查詢命令完成,影響 {RowCount} 筆資料",
affected);
return affected;
}
});
}
}
// 自訂例外類別
public class DatabaseOperationException : Exception
{
public string OperationType { get; set; }
public string TableName { get; set; }
public int? ErrorCode { get; set; }
public DatabaseOperationException(
string message,
string operationType,
string tableName,
Exception innerException = null)
: base(message, innerException)
{
OperationType = operationType;
TableName = tableName;
if (innerException is SqlException sqlEx)
{
ErrorCode = sqlEx.Number;
}
}
}
實戰範例:訂單管理系統
讓我們整合前面學到的概念,建立一個完整的訂單管理系統:
// 資料模型
public class Order
{
public int Id { get; set; }
public int CustomerId { get; set; }
public DateTime OrderDate { get; set; }
public decimal TotalAmount { get; set; }
public OrderStatus Status { get; set; }
public List<OrderDetail> OrderDetails { get; set; }
}
public class OrderDetail
{
public int Id { get; set; }
public int OrderId { get; set; }
public int ProductId { get; set; }
public int Quantity { get; set; }
public decimal Price { get; set; }
public decimal Subtotal => Quantity * Price;
}
public enum OrderStatus
{
Pending = 0,
Processing = 1,
Shipped = 2,
Delivered = 3,
Cancelled = 4
}
// Repository 介面
public interface IOrderRepository
{
int CreateOrder(Order order);
Order GetOrderById(int orderId);
List<Order> GetOrdersByCustomer(int customerId);
bool UpdateOrderStatus(int orderId, OrderStatus status);
OrderSummary GetOrderSummary(DateTime startDate, DateTime endDate);
}
// Repository 實作
public class OrderRepository : IOrderRepository
{
private readonly RobustDatabaseService _dbService;
private readonly ILogger<OrderRepository> _logger;
public OrderRepository(
RobustDatabaseService dbService,
ILogger<OrderRepository> logger)
{
_dbService = dbService;
_logger = logger;
}
public int CreateOrder(Order order)
{
return _dbService.ExecuteWithRetry(connection =>
{
using (var transaction = connection.BeginTransaction())
{
try
{
// 插入訂單主檔
int orderId;
string insertOrderQuery = @"
INSERT INTO Orders (CustomerId, OrderDate, TotalAmount, Status)
VALUES (@CustomerId, @OrderDate, @TotalAmount, @Status);
SELECT SCOPE_IDENTITY();";
using (var command = new SqlCommand(insertOrderQuery, connection, transaction))
{
command.Parameters.AddWithValue("@CustomerId", order.CustomerId);
command.Parameters.AddWithValue("@OrderDate", order.OrderDate);
command.Parameters.AddWithValue("@TotalAmount", order.TotalAmount);
command.Parameters.AddWithValue("@Status", (int)order.Status);
orderId = Convert.ToInt32(command.ExecuteScalar());
}
// 插入訂單明細
foreach (var detail in order.OrderDetails)
{
string insertDetailQuery = @"
INSERT INTO OrderDetails
(OrderId, ProductId, Quantity, Price)
VALUES
(@OrderId, @ProductId, @Quantity, @Price)";
using (var command = new SqlCommand(insertDetailQuery, connection, transaction))
{
command.Parameters.AddWithValue("@OrderId", orderId);
command.Parameters.AddWithValue("@ProductId", detail.ProductId);
command.Parameters.AddWithValue("@Quantity", detail.Quantity);
command.Parameters.AddWithValue("@Price", detail.Price);
command.ExecuteNonQuery();
}
// 更新庫存
if (!UpdateProductStock(
connection, transaction, detail.ProductId, detail.Quantity))
{
throw new InvalidOperationException(
$"產品 {detail.ProductId} 庫存不足");
}
}
transaction.Commit();
_logger.LogInformation(
"成功建立訂單 {OrderId},客戶 {CustomerId},金額 {Amount}",
orderId, order.CustomerId, order.TotalAmount);
return orderId;
}
catch (Exception ex)
{
transaction.Rollback();
_logger.LogError(ex, "建立訂單失敗");
throw;
}
}
});
}
public Order GetOrderById(int orderId)
{
return _dbService.ExecuteWithRetry(connection =>
{
Order order = null;
// 查詢訂單主檔
string orderQuery = @"
SELECT Id, CustomerId, OrderDate, TotalAmount, Status
FROM Orders
WHERE Id = @OrderId";
using (var command = new SqlCommand(orderQuery, connection))
{
command.Parameters.AddWithValue("@OrderId", orderId);
using (var reader = command.ExecuteReader())
{
if (reader.Read())
{
order = new Order
{
Id = reader.GetInt32(0),
CustomerId = reader.GetInt32(1),
OrderDate = reader.GetDateTime(2),
TotalAmount = reader.GetDecimal(3),
Status = (OrderStatus)reader.GetInt32(4),
OrderDetails = new List<OrderDetail>()
};
}
}
}
if (order != null)
{
// 查詢訂單明細
string detailQuery = @"
SELECT Id, ProductId, Quantity, Price
FROM OrderDetails
WHERE OrderId = @OrderId";
using (var command = new SqlCommand(detailQuery, connection))
{
command.Parameters.AddWithValue("@OrderId", orderId);
using (var reader = command.ExecuteReader())
{
while (reader.Read())
{
order.OrderDetails.Add(new OrderDetail
{
Id = reader.GetInt32(0),
OrderId = orderId,
ProductId = reader.GetInt32(1),
Quantity = reader.GetInt32(2),
Price = reader.GetDecimal(3)
});
}
}
}
}
return order;
});
}
public List<Order> GetOrdersByCustomer(int customerId)
{
return _dbService.ExecuteQuery(
@"SELECT Id, CustomerId, OrderDate, TotalAmount, Status
FROM Orders
WHERE CustomerId = @CustomerId
ORDER BY OrderDate DESC",
reader =>
{
var orders = new List<Order>();
while (reader.Read())
{
orders.Add(new Order
{
Id = reader.GetInt32(0),
CustomerId = reader.GetInt32(1),
OrderDate = reader.GetDateTime(2),
TotalAmount = reader.GetDecimal(3),
Status = (OrderStatus)reader.GetInt32(4)
});
}
return orders;
},
new SqlParameter("@CustomerId", customerId)
);
}
public bool UpdateOrderStatus(int orderId, OrderStatus status)
{
var affected = _dbService.ExecuteNonQuery(
@"UPDATE Orders
SET Status = @Status, ModifiedDate = GETDATE()
WHERE Id = @OrderId",
new SqlParameter("@OrderId", orderId),
new SqlParameter("@Status", (int)status)
);
return affected > 0;
}
public OrderSummary GetOrderSummary(DateTime startDate, DateTime endDate)
{
return _dbService.ExecuteWithRetry(connection =>
{
using (var command = new SqlCommand("sp_GetOrderSummary", connection))
{
command.CommandType = CommandType.StoredProcedure;
command.Parameters.AddWithValue("@StartDate", startDate);
command.Parameters.AddWithValue("@EndDate", endDate);
using (var reader = command.ExecuteReader())
{
if (reader.Read())
{
return new OrderSummary
{
TotalOrders = reader.GetInt32(0),
TotalRevenue = reader.GetDecimal(1),
AverageOrderValue = reader.GetDecimal(2),
TopSellingProductId = reader.GetInt32(3),
TopSellingProductName = reader.GetString(4)
};
}
return null;
}
}
});
}
private bool UpdateProductStock(
SqlConnection connection,
SqlTransaction transaction,
int productId,
int quantity)
{
string updateQuery = @"
UPDATE Products
SET Quantity = Quantity - @Quantity
WHERE Id = @ProductId AND Quantity >= @Quantity";
using (var command = new SqlCommand(updateQuery, connection, transaction))
{
command.Parameters.AddWithValue("@ProductId", productId);
command.Parameters.AddWithValue("@Quantity", quantity);
return command.ExecuteNonQuery() > 0;
}
}
}
public class OrderSummary
{
public int TotalOrders { get; set; }
public decimal TotalRevenue { get; set; }
public decimal AverageOrderValue { get; set; }
public int TopSellingProductId { get; set; }
public string TopSellingProductName { get; set; }
}
最佳實踐總結
1. 連線管理
- 始終使用
using
語句確保連線正確關閉 - 利用連線池提高效能
- 避免長時間持有連線
2. 安全性
- 永遠使用參數化查詢防止 SQL 注入
- 不要在程式碼中硬編碼連線字串
- 適當處理敏感資料的日誌記錄
3. 效能優化
- 只查詢需要的欄位,避免
SELECT *
- 使用適當的索引
- 考慮使用批次操作處理大量資料
- 實施查詢結果快取策略
4. 錯誤處理
- 實施重試邏輯處理暫時性錯誤
- 提供有意義的錯誤訊息
- 記錄詳細的錯誤資訊以便除錯
5. 程式碼組織
- 使用 Repository 模式分離資料存取邏輯
- 實作介面以提高可測試性
- 保持方法簡潔,每個方法只做一件事
結論
本文詳細介紹了在 C# 中使用 SQL Server 進行同步操作的各種技術和最佳實踐。從基本的 CRUD 操作到進階的交易處理、批次操作和效能優化,這些技術構成了資料庫應用程式開發的基礎。
雖然現代開發越來越傾向使用 ORM 框架如 Entity Framework Core,但理解底層的 ADO.NET 操作仍然非常重要。這不僅有助於解決複雜的效能問題,也能讓您在需要時寫出更高效的資料存取程式碼。
記住,選擇同步或非同步操作應該基於您的應用程式需求。對於桌面應用程式或簡單的批次處理程式,同步操作可能更簡單直接。但對於高並發的 Web 應用程式,考慮使用非同步操作將能提供更好的擴展性。
希望這篇文章能為您的 C# 資料庫程式設計之旅提供實用的參考。持續學習和實踐,您將能夠建立出高效、穩定且易於維護的資料庫應用程式。