# DbAccess 大幅改版:簡化資料存取與交易管理

>BeeNET 於 v3.4.0 在資料存取層進行了大幅重構,過去的 `DbCommandHelper` 與 `SysDb` 等輔助類別,現在都統整為單一的 `DbAccess` 類別。
>開發者不再需要手動管理 `DbConnection`、`DbCommand` 與 `DbDataReader`,即可輕鬆完成資料操作。此外,`DbAccess` 也支援批次命令與交易,以及外部連線的整合,讓使用更安全、彈性。本文整理了改版重點與實際使用方式。
- NuGet:<https://www.nuget.org/packages/Bee.Db>
- GitHub:<https://github.com/jeff377/bee-library>
---
## 1️⃣ 改版重點總覽
- **單一入口 `DbAccess`**:所有資料存取皆透過 `DbAccess`,降低耦合度並減少學習成本。
- **自動連線管理**:內部模式下自動建立/釋放 `DbConnection`、`DbCommand` 與 `DbDataReader`。
- **同步/非同步 API**:所有主要操作皆提供 Sync/Async 版本。
- **交易與批次**:
- `ExecuteBatch*` 可一鍵包交易;任一命令失敗即回滾,並拋出包含**失敗索引**的例外。
- 單筆命令也可透過 `Execute(spec, DbTransaction)` / `ExecuteAsync(spec, DbTransaction)` 參與外部交易。
- **外部連線模式**:可與自訂交易範圍整合。
---
## 2️⃣ `DbAccess` API 概觀
### 1. 建構子(兩種模式)
```csharp
// 內部連線模式:以 databaseId 解析 Provider 與 ConnectionString
var db1 = new DbAccess("HR");
// 外部連線模式:連線與交易生命週期由外部管理
using (var conn = new SqlConnection(connString))
{
conn.Open();
var db2 = new DbAccess(conn); // 外部連線型態與 Provider 對應由框架管理
}
```
### 2. 單筆命令
```csharp
// 同步
DbCommandResult result = db1.Execute(spec);
// 非同步
DbCommandResult resultAsync = await db1.ExecuteAsync(spec, cancellationToken);
```
### 3. 批次命令與交易
```csharp
var batch = new DbBatchSpec
{
UseTransaction = true,
IsolationLevel = IsolationLevel.ReadCommitted, // 可選
Commands =
{
new DbCommandSpec(DbCommandKind.NonQuery, "INSERT INTO Department(Name) VALUES({0})", "R&D"),
new DbCommandSpec(DbCommandKind.NonQuery, "INSERT INTO Employee(Name, DepartmentId) VALUES({0}, {1})", "Alice", 5)
}
};
// 同步
DbBatchResult batchResult = db1.ExecuteBatch(batch);
// 非同步
DbBatchResult batchResultAsync = await db1.ExecuteBatchAsync(batch, cancellationToken);
```
> 若批次第 `i` 筆失敗:框架會嘗試回滾並拋出
> `InvalidOperationException($"Failed to execute batch: Command at index {i} failed.", innerException)`,便於快速定位。
### 4. 指定外部交易的單筆命令
```csharp
using (var conn = new SqlConnection(connString))
{
conn.Open();
using (var tran = conn.BeginTransaction())
{
var db = new DbAccess(conn);
db.Execute(spec1, tran); // 同步
await db.ExecuteAsync(spec2, tran, token); // 非同步
tran.Commit();
}
}
```
> ⚠️ 注意:若使用外部交易 (`DbTransaction`),請僅呼叫 `Execute` / `ExecuteAsync`,不應再呼叫 `ExecuteBatch`。
### 5. 串流查詢與物件映射
```csharp
// Query:直接回傳 List<T>
var list = db1.Query<Employee>(spec);
// QueryAsync:直接回傳 List<T>
var listAsync = await db1.QueryAsync<Employee>(spec, cancellationToken);
```
### 6. DataTable 回寫
```csharp
var spec = new DataTableUpdateSpec
{
DataTable = table,
InsertCommand = insertSpec,
UpdateCommand = updateSpec,
DeleteCommand = deleteSpec,
UseTransaction = true,
IsolationLevel = IsolationLevel.ReadCommitted // 可選
};
var db = new DbAccess("HR");
int affected = db.UpdateDataTable(spec);
```
---
## 3️⃣ `DbCommandSpec` 說明
`DbCommandSpec` 是資料庫命令的中介類別:你只需描述「要做什麼」(SQL + 參數 + 命令種類),它就會在執行前依照 DatabaseType 自動轉換成正確的 DbCommand 與 DbParameter。
- **兩種參數模式**
- **位置參數**:`{0}`, `{1}`, …(短小、直覺)
- **具名參數**:`{name}`, `{hiredOn}`, …(欄位多時更不易放錯)
- **依 DatabaseType 套用參數前綴**
你在 SQL 中寫 `{name}` 或 `{0}`,實際執行時會自動轉換為正確的參數名稱與 `DbParameter`。
- SQL Server:`@name`
- Oracle:`:name`
- MySQL:`?name`
- **Stored Procedure 直接傳遞**
當 `CommandType = StoredProcedure` 時,`CommandText` 不做參數占位符解析,直接用 SP 名稱與參數集合建立命令。
- **Timeout 規則**
`CommandTimeout <= 0` 以預設 30 秒;若超過全域上限則自動套用上限。
- **安全性**
不論位置或具名參數,都不會做字串拼接;在 `CreateCommand()` 期間會轉成 `DbParameter`,可有效防止 SQL Injection。
---
## 4️⃣ `DbCommandSpec` 使用範例
### 1. 查詢(位置參數)
```csharp
string sql = @"
SELECT EmployeeId,
Name,
HiredOn
FROM Employee
WHERE EmployeeId = {0}";
var command = new DbCommandSpec(DbCommandKind.DataTable, sql, 1001);
var db = new DbAccess("HR");
var employees = db.Query<Employee>(command); // List<Employee>
```
### 2. 查詢單一值(位置參數)
```csharp
var spec = new DbCommandSpec(
DbCommandKind.Scalar,
"SELECT COUNT(*) FROM Employee WHERE DepartmentId = {0}",
5
);
var db = new DbAccess("HR");
var result = db.Execute(spec);
int count = Convert.ToInt32(result.Scalar);
```
### 3. 新增資料(位置參數 + 逾時,多欄位)
```csharp
var spec = new DbCommandSpec(
DbCommandKind.NonQuery,
"INSERT INTO Department(Name, CreatedOn, IsActive) VALUES({0}, {1}, {2})",
"R&D", DateTime.Now, true
)
{
CommandTimeout = 60 // <=0 使用預設 30 秒;超過全域上限則套用上限(由 setter 處理)
};
var db = new DbAccess("HR");
DbCommandResult r = db.Execute(spec);
Console.WriteLine($"Rows affected: {r.RowsAffected}");
```
### 4. 更新資料(具名參數,多欄位更新 + 條件)
```csharp
var spec = new DbCommandSpec(
DbCommandKind.NonQuery,
"UPDATE Employee SET Name = {name}, HiredOn = {hiredOn} WHERE EmployeeId = {employeeId}",
new Dictionary<string, object>
{
["name"] = "Alice",
["hiredOn"] = new DateTime(2024, 1, 1),
["employeeId"] = 1001
}
);
var db = new DbAccess("HR");
db.Execute(spec);
```
### 5. Stored Procedure(具名參數示範)
```csharp
var spec = new DbCommandSpec(
DbCommandKind.NonQuery,
"usp_UpdateSalary",
new Dictionary<string, object>
{
["employeeId"] = 1001,
["delta"] = 1000m
}
)
{
CommandType = CommandType.StoredProcedure
};
var db = new DbAccess("HR");
db.Execute(spec);
```
---
## 5️⃣ 總結
透過這次的改版,`DbAccess` 已成為 BeeNET 中**唯一且安全**的資料存取入口:
- 開發者可以只用單一類別完成大部分資料庫操作。
- 連線與命令資源交由框架管理,降低資源洩漏風險。
- 批次命令與交易更簡潔,失敗會自動回滾並回報失敗位置。
- 可依需求選擇內部或外部連線模式,靈活整合其他框架。
開發者現在可以更專注於 SQL 與業務邏輯本身,讓資料存取更直觀、更可靠。
---
**📢 歡迎轉載,請註明出處**
**📬 歡迎追蹤我的技術筆記與實戰經驗分享**
[Facebook](https://www.facebook.com/profile.php?id=61574839666569) | [HackMD](https://hackmd.io/@jeff377) | [GitHub](https://github.com/jeff377) | [NuGet](https://www.nuget.org/profiles/jeff377)