# DbAccess 大幅改版:簡化資料存取與交易管理 ![dbaccess-v2](https://hackmd.io/_uploads/ry1etw_qge.png) >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)