在企業級應用開發中,常面臨需支援多種資料庫(SQL Server、Oracle、MySQL、PostgreSQL、Access 等)的需求。硬編碼特定資料庫的連線與指令類別(如 SqlConnection、OracleCommand)會導致程式碼耦合度高、難以維護。.NET Framework 提供了 ADO.NET Provider Factory Model(DbProviderFactory),讓我們能撰寫資料庫無關的通用資料存取程式碼。本文提供一個完整、可直接用於生產環境的 DataAccess 基底類別,支援透過 app.config/web.config 切換資料庫提供者,並封裝常用的 ExecuteCommand、ExecuteScalar、ExecuteReader、GetDataSet、GetDataTable、GetDataView 等方法,包含參數化查詢、交易支援與連線池管理。
DbProviderFactory 動態建立 DbConnection、DbCommand、DbDataAdapter、DbParameter。ConfigurationManager.ConnectionStrings 讀取,支援多連線字串切換。try-catch-finally 確保連線正確關閉,支援 using 模式(可擴充實作 IDisposable)。ExecuteCommand(ArrayList sqlList) 批次執行 SQL 並自動交易控制(Commit/Rollback)。using System;
using System.Collections;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Data.Common;
namespace DBUtility
{
///
/// 通用資料庫存取基底類別,支援多資料庫提供者 (SQL Server, Oracle, OleDb, MySQL, PostgreSQL 等)
///
public class DataAccess : IDisposable
{
private string _configString = "ConnectionString"; // 預設連線字串名稱
public DataAccess() { }
public DataAccess(string configString)
{
ConfigString = configString;
}
///
/// 組態檔中連線字串的名稱 (對應 app.config/web.config 的 connectionStrings 區段 name 屬性)
///
public string ConfigString
{
get => _configString;
set => _configString = value ?? "ConnectionString";
}
#region 取得提供者資訊
///
/// 取得資料提供者名稱 (如 System.Data.SqlClient)
///
public string GetProviderName(string configString = null)
{
var settings = GetConnectionStringSettings(configString);
return settings?.ProviderName ?? "System.Data.SqlClient";
}
private ConnectionStringSettings GetConnectionStringSettings(string configString = null)
{
var collection = ConfigurationManager.ConnectionStrings;
if (collection == null || collection.Count == 0)
throw new InvalidOperationException("組態檔中找不到任何連線字串 (connectionStrings 區段為空)。");
string name = string.IsNullOrEmpty(configString) ? ConfigString : configString;
var settings = collection[name];
if (settings == null)
throw new InvalidOperationException($"組態檔中找不到名稱為 '{name}' 的連線字串。");
return settings;
}
#endregion
#region 取得連線字串
public string GetConnectionString(string configString = null)
{
return GetConnectionStringSettings(configString).ConnectionString;
}
#endregion
#region DbProviderFactory 管理
private DbProviderFactory _factoryCache;
private string _cachedProviderName;
///
/// 取得或建立 DbProviderFactory (含快取機制)
///
protected DbProviderFactory GetDbProviderFactory(string configString = null)
{
string providerName = GetProviderName(configString);
if (_factoryCache != null && _cachedProviderName == providerName)
return _factoryCache;
_factoryCache = DbProviderFactories.GetFactory(providerName);
_cachedProviderName = providerName;
return _factoryCache;
}
#endregion
#region 建立連線、命令、適配器、參數
public DbConnection CreateConnection(string configString = null)
{
var factory = GetDbProviderFactory(configString);
var conn = factory.CreateConnection();
conn.ConnectionString = GetConnectionString(configString);
return conn;
}
public DbCommand CreateCommand(string sql, CommandType cmdType = CommandType.Text, DbParameter[] parameters = null, string configString = null)
{
var factory = GetDbProviderFactory(configString);
var cmd = factory.CreateCommand();
cmd.Connection = CreateConnection(configString);
cmd.CommandText = sql;
cmd.CommandType = cmdType;
if (parameters != null && parameters.Length > 0)
cmd.Parameters.AddRange(parameters);
return cmd;
}
public DbDataAdapter CreateAdapter(string sql, CommandType cmdType = CommandType.Text, DbParameter[] parameters = null, string configString = null)
{
var factory = GetDbProviderFactory(configString);
var cmd = factory.CreateCommand();
cmd.Connection = CreateConnection(configString);
cmd.CommandText = sql;
cmd.CommandType = cmdType;
if (parameters != null && parameters.Length > 0)
cmd.Parameters.AddRange(parameters);
var adapter = factory.CreateDataAdapter();
adapter.SelectCommand = cmd;
return adapter;
}
public DbParameter CreateParameter(string parameterName, object value, string configString = null)
{
var factory = GetDbProviderFactory(configString);
var param = factory.CreateParameter();
param.ParameterName = parameterName;
param.Value = value ?? DBNull.Value;
return param;
}
#endregion
#region 執行非查詢 (INSERT/UPDATE/DELETE/DDL)
public int ExecuteCommand(string sql, CommandType cmdType = CommandType.Text, DbParameter[] parameters = null, string configString = null)
{
using (var cmd = CreateCommand(sql, cmdType, parameters, configString))
{
try
{
cmd.Connection.Open();
return cmd.ExecuteNonQuery();
}
catch (Exception ex)
{
throw new DataAccessException($"執行命令失敗: {ex.Message}", ex);
}
}
}
///
/// 批次執行多條 SQL (交易模式),任一失敗全數 Rollback
///
public bool ExecuteCommandBatch(IList sqlList, string configString = null)
{
using (var conn = CreateConnection(configString))
{
conn.Open();
using (var tran = conn.BeginTransaction())
{
try
{
var factory = GetDbProviderFactory(configString);
foreach (string sql in sqlList)
{
using (var cmd = factory.CreateCommand())
{
cmd.Connection = conn;
cmd.Transaction = tran;
cmd.CommandText = sql;
cmd.ExecuteNonQuery();
}
}
tran.Commit();
return true;
}
catch (Exception ex)
{
tran.Rollback();
throw new DataAccessException($"批次執行失敗,已回滾: {ex.Message}", ex);
}
}
}
}
#endregion
#region 執行純量查詢 (單一值)
public object ExecuteScalar(string sql, CommandType cmdType = CommandType.Text, DbParameter[] parameters = null, string configString = null)
{
using (var cmd = CreateCommand(sql, cmdType, parameters, configString))
{
try
{
cmd.Connection.Open();
return cmd.ExecuteScalar();
}
catch (Exception ex)
{
throw new DataAccessException($"執行純量查詢失敗: {ex.Message}", ex);
}
}
}
#endregion
#region 執行查詢回傳 DataReader (唯讀、順向、資料流)
public DbDataReader ExecuteReader(string sql, CommandType cmdType = CommandType.Text, DbParameter[] parameters = null, string configString = null)
{
var cmd = CreateCommand(sql, cmdType, parameters, configString);
try
{
cmd.Connection.Open();
// CommandBehavior.CloseConnection: DataReader 關閉時自動關閉連線
return cmd.ExecuteReader(CommandBehavior.CloseConnection);
}
catch
{
cmd.Connection.Close();
cmd.Dispose();
throw;
}
}
#endregion
#region 執行查詢回傳 DataSet / DataTable / DataView (離線資料集)
public DataSet GetDataSet(string sql, CommandType cmdType = CommandType.Text, DbParameter[] parameters = null, string configString = null)
{
using (var adapter = CreateAdapter(sql, cmdType, parameters, configString))
{
var ds = new DataSet();
adapter.Fill(ds);
return ds;
}
}
public DataSet GetDataSetPaged(string sql, int startIndex, int recordCount, CommandType cmdType = CommandType.Text, DbParameter[] parameters = null, string configString = null)
{
using (var adapter = CreateAdapter(sql, cmdType, parameters, configString))
{
var ds = new DataSet();
adapter.Fill(ds, startIndex, recordCount, "Table");
return ds;
}
}
public DataTable GetDataTable(string sql, CommandType cmdType = CommandType.Text, DbParameter[] parameters = null, string configString = null)
{
return GetDataSet(sql, cmdType, parameters, configString).Tables[0];
}
public DataTable GetDataTablePaged(string sql, int startIndex, int recordCount, CommandType cmdType = CommandType.Text, DbParameter[] parameters = null, string configString = null)
{
return GetDataSetPaged(sql, startIndex, recordCount, cmdType, parameters, configString).Tables[0];
}
public DataTable GetDataTableWithEmptyRows(string sql, int minRows, CommandType cmdType = CommandType.Text, DbParameter[] parameters = null, string configString = null)
{
var dt = GetDataTable(sql, cmdType, parameters, configString);
int need = minRows - dt.Rows.Count;
for (int i = 0; i < need; i++)
dt.Rows.Add(dt.NewRow());
return dt;
}
public DataView GetDataView(string sql, CommandType cmdType = CommandType.Text, DbParameter[] parameters = null, string configString = null)
{
return GetDataSet(sql, cmdType, parameters, configString).Tables[0].DefaultView;
}
#endregion
#region IDisposable 實作 (可選,若衍生類別持有非受控資源)
private bool _disposed = false;
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
_factoryCache = null;
}
_disposed = true;
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
#endregion
}
///
/// 資料存取層自訂例外
///
public class DataAccessException : Exception
{
public DataAccessException(string message, Exception innerException) : base(message, innerException) { }
public DataAccessException(string message) : base(message) { }
}
}
<configuration>
<connectionStrings>
<!-- SQL Server -->
<add name="SqlServerConn"
connectionString="Server=.;Database=MyDB;User Id=sa;Password=123456;"
providerName="System.Data.SqlClient" />
<!-- Oracle (需安裝 Oracle.ManagedDataAccess) -->
<add name="OracleConn"
connectionString="Data Source=ORCL;User Id=scott;Password=tiger;"
providerName="Oracle.ManagedDataAccess.Client" />
<!-- MySQL (需安裝 MySql.Data) -->
<add name="MySqlConn"
connectionString="Server=localhost;Database=mydb;Uid=root;Pwd=123456;"
providerName="MySql.Data.MySqlClient" />
<!-- PostgreSQL (需安裝 Npgsql) -->
<add name="PostgreConn"
connectionString="Host=localhost;Database=mydb;Username=postgres;Password=123456;"
providerName="Npgsql" />
<!-- Access (OleDb) -->
<add name="AccessConn"
connectionString="Provider=Microsoft.ACE.OLEDB.12.0;Data Source=|DataDirectory|\db.accdb;"
providerName="System.Data.OleDb" />
</connectionStrings>
</configuration>
using System;
using System.Data;
using System.Data.Common;
using DBUtility;
class Program
{
static void Main()
{
// 使用預設連線字串 (name="ConnectionString")
var db = new DataAccess("SqlServerConn");
// 1. 參數化查詢 - 防止 SQL Injection
string sql = "SELECT * FROM Users WHERE Age > @Age AND City = @City";
var parameters = new DbParameter[]
{
db.CreateParameter("@Age", 18),
db.CreateParameter("@City", "Taipei")
};
DataTable dt = db.GetDataTable(sql, CommandType.Text, parameters);
Console.WriteLine($"查詢到 {dt.Rows.Count} 筆資料");
foreach (DataRow row in dt.Rows)
Console.WriteLine($"{row["Id"]}\t{row["Name"]}\t{row["Age"]}\t{row["City"]}");
// 2. 執行非查詢 (新增/修改/刪除)
int affected = db.ExecuteCommand(
"INSERT INTO Users (Name, Age, City) VALUES (@Name, @Age, @City)",
CommandType.Text,
new DbParameter[]
{
db.CreateParameter("@Name", "John"),
db.CreateParameter("@Age", 25),
db.CreateParameter("@City", "Taichung")
});
Console.WriteLine($"異動 {affected} 筆資料");
// 3. 純量查詢
object countObj = db.ExecuteScalar("SELECT COUNT(*) FROM Users");
Console.WriteLine($"總筆數:{countObj}");
// 4. 批次交易執行
var sqlBatch = new System.Collections.ArrayList
{
"UPDATE Users SET Age = Age + 1 WHERE City = 'Taipei'",
"UPDATE Users SET LastLogin = GETDATE() WHERE City = 'Taipei'"
};
bool success = db.ExecuteCommandBatch(sqlBatch);
Console.WriteLine($"批次交易 {(success ? "成功" : "失敗")}");
// 5. DataReader 串流讀取 (大量資料、唯讀、高效能)
using (DbDataReader reader = db.ExecuteReader("SELECT Name, Email FROM Users WHERE IsActive = 1"))
{
while (reader.Read())
Console.WriteLine($"{reader["Name"]} - {reader["Email"]}");
}
db.Dispose();
Console.ReadKey();
}
}
| 功能 | 實作方向 |
|---|---|
| 非同步支援 | 新增 ExecuteCommandAsync、GetDataTableAsync 等方法,使用 await cmd.ExecuteNonQueryAsync() 等 |
| 連線重試原則 | 整合 Polly 或 Microsoft.Data.SqlClient 內建重試邏輯 |
| SQL 記錄與效能監控 | 在 CreateCommand 後攔截,記錄 SQL、參數、執行時間 |
| 對應 Entity/DTO | 搭配 Dapper 或自寫 MapToList<T>(DbDataReader) 實現物件映射 |
| 分頁查詢標準化 | 針對不同資料庫方言 (OFFSET/FETCH, ROWNUM, LIMIT) 產生分頁 SQL |
本文提供的 DataAccess 類別完整實作了 ADO.NET Provider Factory 模式,支援多資料庫無縫切換、參數化查詢、交易批次、分頁查詢、DataReader 串流讀取等企業級常用功能。程式碼採用 using 區塊確保資源正確釋放,並提供自訂例外 DataAccessException 統一錯誤處理。開發者只需在組態檔調整連線字串與提供者名稱,即可在不修改業務邏輯的前提下切換資料庫,極大提升系統的可維護性與擴充性。