DAL.instructions.md
從 Google Drive .github/instructions/DAL.instructions.md 匯入
共用指令內容
使用中---
applyTo: 'App_Code/DAL/**/*.cs'
---
DAL (Data Access Layer) 開發規範
> 文件版本: 1.0.0
> 最後更新: 2026-01-26
> 適用範圍: App_Code/DAL/**/*.cs
---
1. 概述
1.1 層級定位
DAL (資料存取層) 為三層式架構中的最底層,唯一職責是與資料庫進行互動。DAL 必須完全隔離資料庫實作細節,使上層 (BLL) 無需關心底層資料庫類型。
1.2 設計原則
| 原則 | 說明 |
|:-----|:-----|
| 單一職責 | 僅負責 CRUD 操作與資料列映射,禁止包含業務邏輯 |
| 資料庫無關性 | 透過 DbHelperSQL 抽象層操作,禁止直接建立連線 |
| 參數化查詢 | 所有 SQL 必須使用參數化,防止 SQL Injection |
| 資源管理 | 實作 Dispose 模式,確保資源正確釋放 |
1.3 回傳型別策略
DAL 支援兩種 SELECT 回傳模式,依據使用情境選擇:
| 模式 | 回傳型別 | 適用情境 | BLL 端處理 |
|:-----|:---------|:---------|:-----------|
| DTO 映射模式 (主流) | IEnumerable<DtoXxx> | 一般業務 CRUD 查詢;查詢結果需轉 JSON 回傳前端 | BLL 呼叫 .ToList() 取得 List<DtoXxx> |
| DataSet 直通模式 | DataSet | 系統基礎設施查詢 (權限、員工);需跨層 DataRow 直接操作;報表 / Excel 匯出用途 | BLL 自行遍歷 DataRow 做映射或直接操作 |
> 決策原則:優先使用 DTO 映射模式。僅在資料結構不固定、多用途查詢、或系統基礎設施類別時使用 DataSet 直通模式。
---
2. 類別結構規範
2.1 命名規則
| 項目 | 規則 | 範例 |
|:-----|:-----|:-----|
| 類別名稱 | Dbo{實體名稱} 或 Dbo{實體名稱}{資料庫類型} | DboEmployee, DboEmployeeMSSQL |
| 資料表常數 | TableName (internal const) | internal const String TableName = "employee"; |
| 方法名稱 | 動詞 + 名詞,使用 PascalCase | SelectById, Insert, Update, Delete |
2.2 必要繼承
所有 DAL 類別必須繼承自 CHMC.DB 命名空間下的 DbHelperSQL:
// Sybase ASE 資料庫
using CHMC.DB.AseClient;
public class DboEmployee : DbHelperSQL { }
// Microsoft SQL Server
using CHMC.DB.SqlClient;
internal partial class DboEmployeeMSSQL : DbHelperSQL { }
2.3 標準類別結構
using System;
using System.Collections.Generic;
using System.Data;
using System.Text;
using CHMC;
using CHMC.DB.AseClient; // 或 CHMC.DB.SqlClient
using Sybase.Data.AseClient; // 或 System.Data.SqlClient
/// <summary>
/// [資料表描述] 資料存取類別
/// </summary>
public class Dbo{EntityName} : DbHelperSQL
{
#region ========== 常數與欄位 ==========
/// <summary>
/// 資料表名稱
/// </summary>
internal const String TableName = "{table_name}";
// 私有欄位 - 統一宣告於此
private DataSet dsReturn = null;
private StringBuilder sbSQLCriteria = null;
private List<AseParameter> param = null; // 或 List<SqlParameter>
private AseParameter[] parameters = null; // 或 SqlParameter[]
private String strSQL = String.Empty;
private String strSQLCriteria = String.Empty;
#endregion
#region ========== 建構子 ==========
/// <summary>
/// 預設建構子
/// </summary>
internal Dbo{EntityName}()
{ }
/// <summary>
/// 建構子 - 接收連線字串
/// </summary>
/// <param name="connectionString">資料庫連線字串</param>
internal Dbo{EntityName}(String connectionString)
: base(connectionString)
{ }
/// <summary>
/// 建構子 - 接收交易物件 (用於跨表交易)
/// </summary>
/// <param name="tran">交易物件</param>
internal Dbo{EntityName}(DbTransaction tran)
: base(tran)
{ }
#endregion
#region ========== 解構與資源釋放 ==========
/// <summary>
/// 解構子
/// </summary>
~Dbo{EntityName}()
{
Dispose(false);
}
/// <summary>
/// 釋放資源
/// </summary>
/// <remarks>
/// ⚠ 重要:此方法使用 <c>new</c> 隱藏父類別方法(非 override),
/// 因此 BLL 層必須用 try/finally + 明確呼叫 dal.Dispose(),
/// 禁止用 using 語句(using 透過 IDisposable 介面只呼叫父類別 Dispose,
/// 子類別的 DataSet / param 清理邏輯永遠不會執行)。
/// </remarks>
public new void Dispose()
{
base.Dispose();
if (dsReturn != null)
{
dsReturn.Dispose();
dsReturn = null;
}
if (sbSQLCriteria != null)
{
sbSQLCriteria.Clear();
sbSQLCriteria = null;
}
if (param != null)
{
param.Clear();
param = null;
}
parameters = null;
}
#endregion
#region ========== 公開方法 ==========
// CRUD 方法實作於此區塊
#endregion
#region ========== 私有方法 ==========
// 輔助方法實作於此區塊
#endregion
}
---
3. 方法實作規範
3.1 DbHelperSQL 標準方法
> ⛔ 嚴重警告:DbHelperSQL 的方法不接受省略參數的呼叫。以下簽章是唯一合法的呼叫方式,少傳或多傳任何參數都會導致 CS1501 / CS7036 編譯錯誤。
| 方法 | 完整方法簽章 | 用途 | 回傳值 |
|:-----|:-----|:-----|:-----|
| FillDataSet | base.FillDataSet(string strQuery, CommandType execCommandType, IDataParameter[] parameters, string tableName) | SELECT 查詢 | DataSet |
| ExecuteNonQuery | base.ExecuteNonQuery(string strQuery, CommandType execCommandType, IDataParameter[] parameters) | INSERT / UPDATE / DELETE | int (影響筆數) |
| ExecuteScalar | base.ExecuteScalar(string strQuery, CommandType execCommandType, IDataParameter[] parameters) | 取得單一值 (COUNT, MAX 等) | object |
呼叫範例速查:
// ✅ SELECT 無參數查詢 — parameters 傳 null,tableName 傳 TableName
dsReturn = base.FillDataSet(sbSQLCriteria.ToString(), CommandType.Text, null, TableName);
// ✅ SELECT 有參數查詢
dsReturn = base.FillDataSet(sbSQLCriteria.ToString(), CommandType.Text, parameters, TableName);
// ✅ INSERT / UPDATE / DELETE
return base.ExecuteNonQuery(sbSQLCriteria.ToString(), CommandType.Text, parameters);
❌ 以下呼叫方式全部禁止(會導致編譯錯誤):
// ❌ 只傳 1 個參數 → CS1501 錯誤
base.FillDataSet(sbSQLCriteria.ToString());
// ❌ 只傳 2 個參數 (sql + parameters) → CS1501 錯誤
base.FillDataSet(sbSQLCriteria.ToString(), parameters);
// ❌ 只傳 2 個參數 (sql + parameters) → CS7036 錯誤
base.ExecuteNonQuery(sbSQLCriteria.ToString(), parameters);
3.2 SELECT 方法範本 — DTO 映射模式 (主流)
> 此模式透過 IEnumerable<T> + yield return 延遲列舉,搭配 DTO 的 GetSingleData(DataRow) 方法完成資料列映射。此為專案中最常使用的查詢回傳模式。
#### 範本 A:IEnumerable + yield return — 動態條件查詢
/// <summary>
/// 查詢連絡記錄 (動態條件)
/// </summary>
/// <param name="dto">查詢條件 DTO</param>
/// <returns>符合條件的記錄列舉</returns>
internal IEnumerable<DtoContactRecord> RetrieveContactRecord(DtoContactRecord dto)
{
sbSQLCriteria = new StringBuilder();
param = new List<AseParameter>();
// 動態組合 WHERE 條件 (前置 AND 風格)
if (!String.IsNullOrEmpty(dto.Serial_num))
{
sbSQLCriteria.Append(" AND serial_num = @serial_num");
param.Add(new AseParameter("@serial_num", AseDbType.Char));
param[param.Count - 1].Value = dto.Serial_num;
}
if (!String.IsNullOrEmpty(dto.Citizen_id))
{
sbSQLCriteria.Append(" AND citizen_id = @citizen_id");
param.Add(new AseParameter("@citizen_id", AseDbType.VarChar));
param[param.Count - 1].Value = dto.Citizen_id;
}
// 處理 WHERE 子句 (移除開頭的 AND)
if (sbSQLCriteria.Length > 0)
{
strSQLCriteria = " WHERE " + sbSQLCriteria.ToString().Substring(4);
strSQLCriteria += " ORDER BY sort_order ";
}
parameters = param.ToArray();
strSQL = "SELECT col1, col2, col3 FROM " + TableName + strSQLCriteria;
// ✅ FillDataSet 完整 4 參數呼叫
dsReturn = base.FillDataSet(strSQL, CommandType.Text, parameters, TableName);
// ✅ Sybase CP850 編碼轉換 (中文欄位必加)
Utility.CP850TransUnicode(dsReturn, TableName);
// ✅ 透過 DTO.GetSingleData() 映射並 yield return
foreach (DataRow dr in dsReturn.Tables[TableName].Rows)
{
DtoContactRecord item = new DtoContactRecord();
item.GetSingleData(dr);
yield return item;
}
}
#### 範本 B:IEnumerable + yield return — 固定條件查詢
/// <summary>
/// 依主鍵查詢單筆資料
/// </summary>
/// <param name="id">主鍵值</param>
/// <returns>查詢結果列舉 (0 或 1 筆)</returns>
internal IEnumerable<DtoEntity> RetrieveById(String id)
{
sbSQLCriteria = new StringBuilder();
param = new List<AseParameter>();
sbSQLCriteria.Append(" AND id = @Id");
param.Add(new AseParameter("@Id", AseDbType.VarChar));
param[param.Count - 1].Value = id;
if (sbSQLCriteria.Length > 0)
{
strSQLCriteria = " WHERE " + sbSQLCriteria.ToString().Substring(4);
}
parameters = param.ToArray();
strSQL = "SELECT col1, col2, col3 FROM " + TableName + strSQLCriteria;
dsReturn = base.FillDataSet(strSQL, CommandType.Text, parameters, TableName);
Utility.CP850TransUnicode(dsReturn, TableName);
foreach (DataRow dr in dsReturn.Tables[TableName].Rows)
{
DtoEntity item = new DtoEntity();
item.GetSingleData(dr);
yield return item;
}
}
> 重要:使用 yield return 的方法不可包覆 try-catch,因為 yield return 不允許出現在 try-catch 區塊中。例外處理由 BLL 層的 try-catch 負責捕獲。
---
3.3 SELECT 方法範本 — DataSet 直通模式
> 此模式適用於系統基礎設施查詢 (權限、員工)、報表匯出、或查詢結構不固定的場景。
#### 範本 A:無參數查詢(使用 StringBuilder.Append 風格)
/// <summary>
/// 取得所有上架中的資料(前台用)
/// </summary>
/// <returns>查詢結果 DataSet</returns>
internal DataSet SelectActive()
{
sbSQLCriteria = new StringBuilder();
sbSQLCriteria.Append("SELECT col1, col2, col3 ");
sbSQLCriteria.AppendFormat("FROM {0} ", TableName);
sbSQLCriteria.Append("WHERE is_active = 'Y' ");
sbSQLCriteria.Append("ORDER BY sort_order ASC ");
// ✅ 無參數時 parameters 傳 null,最後一個參數必須傳 TableName
dsReturn = base.FillDataSet(sbSQLCriteria.ToString(), CommandType.Text, null, TableName);
return dsReturn;
}
#### 範本 B:有參數查詢(使用 StringBuilder.Append 風格)
/// <summary>
/// 依主鍵查詢單筆資料
/// </summary>
/// <param name="id">主鍵值</param>
/// <returns>查詢結果 DataSet,失敗時回傳 null</returns>
internal DataSet SelectById(Int32 id)
{
sbSQLCriteria = new StringBuilder();
sbSQLCriteria.Append("SELECT col1, col2, col3 ");
sbSQLCriteria.AppendFormat("FROM {0} ", TableName);
sbSQLCriteria.Append("WHERE id = @Id ");
param = new List<AseParameter>();
param.Add(new AseParameter("@Id", AseDbType.Integer) { Value = id });
parameters = param.ToArray();
// ✅ 完整 4 參數呼叫
dsReturn = base.FillDataSet(sbSQLCriteria.ToString(), CommandType.Text, parameters, TableName);
return dsReturn;
}
#### 範本 C:傳統 strSQL 字串串接風格(亦合法)
internal DataSet SelectById(String id)
{
sbSQLCriteria = new StringBuilder();
param = new List<AseParameter>();
// 組合 WHERE 條件
sbSQLCriteria.Append(" AND column_name = @ParamName");
param.Add(new AseParameter("@ParamName", AseDbType.VarChar));
param[param.Count - 1].Value = id;
// 處理 WHERE 子句 (移除開頭的 AND)
if (sbSQLCriteria.Length > 0)
{
strSQLCriteria = " WHERE " + sbSQLCriteria.ToString().Substring(5);
}
parameters = param.ToArray();
strSQL = "SELECT column1, column2, column3 FROM " + TableName + strSQLCriteria;
try
{
dsReturn = base.FillDataSet(strSQL, CommandType.Text, parameters, TableName);
return dsReturn;
}
catch (Exception ex)
{
Log.WriteLog(ex);
return null;
}
}
3.4 INSERT 方法範本
/// <summary>
/// 新增一筆資料
/// </summary>
/// <param name="dto">資料傳輸物件</param>
/// <returns>成功回傳 true,失敗回傳 false</returns>
internal Boolean Insert(DtoEntity dto)
{
param = new List<AseParameter>();
param.Add(new AseParameter("@Column1", AseDbType.VarChar));
param[param.Count - 1].Value = dto.Column1;
param.Add(new AseParameter("@Column2", AseDbType.Integer));
param[param.Count - 1].Value = dto.Column2;
parameters = param.ToArray();
strSQL = "INSERT INTO " + TableName + " (column1, column2) "
+ "VALUES (@Column1, @Column2)";
try
{
return base.ExecuteNonQuery(strSQL, CommandType.Text, parameters) > 0;
}
catch (Exception ex)
{
Log.WriteLog(ex);
return false;
}
}
3.5 UPDATE 方法範本
/// <summary>
/// 更新一筆資料
/// </summary>
/// <param name="dto">資料傳輸物件</param>
/// <returns>成功回傳 true,失敗回傳 false</returns>
internal Boolean Update(DtoEntity dto)
{
param = new List<AseParameter>();
// SET 欄位
param.Add(new AseParameter("@Column1", AseDbType.VarChar));
param[param.Count - 1].Value = dto.Column1;
// WHERE 條件 (主鍵)
param.Add(new AseParameter("@Id", AseDbType.Integer));
param[param.Count - 1].Value = dto.Id;
parameters = param.ToArray();
strSQL = "UPDATE " + TableName + " SET column1 = @Column1 WHERE id = @Id";
try
{
return base.ExecuteNonQuery(strSQL, CommandType.Text, parameters) > 0;
}
catch (Exception ex)
{
Log.WriteLog(ex);
return false;
}
}
3.6 DELETE 方法範本
/// <summary>
/// 刪除一筆資料
/// </summary>
/// <param name="id">主鍵值</param>
/// <returns>成功回傳 true,失敗回傳 false</returns>
internal Boolean Delete(Int32 id)
{
param = new List<AseParameter>();
param.Add(new AseParameter("@Id", AseDbType.Integer));
param[param.Count - 1].Value = id;
parameters = param.ToArray();
strSQL = "DELETE FROM " + TableName + " WHERE id = @Id";
try
{
return base.ExecuteNonQuery(strSQL, CommandType.Text, parameters) > 0;
}
catch (Exception ex)
{
Log.WriteLog(ex);
return false;
}
}
---
4. 錯誤處理規範
4.1 例外處理原則
| 規則 | 說明 |
|:-----|:-----|
| 必須捕獲 | 所有對 base 方法的呼叫必須包覆於 try-catch |
| 必須記錄 | 使用 Log.WriteLog(ex) 記錄所有例外 |
| 安全回傳 | 查詢失敗回傳 null,異動失敗回傳 false 或 -1 |
| 禁止吞掉 | 禁止空的 catch 區塊 |
4.2 錯誤處理範例
internal DataSet SelectAll()
{
strSQL = "SELECT * FROM " + TableName;
try
{
dsReturn = base.FillDataSet(strSQL, CommandType.Text, null, TableName);
return dsReturn;
}
catch (Exception ex)
{
// ✅ 正確:記錄錯誤並回傳安全值
Log.WriteLog(ex);
return null;
}
}
---
5. 禁止事項 (Anti-Patterns)
5.1 🚫 禁止手動建立資料庫連線
// ❌ 絕對禁止
using (AseConnection conn = new AseConnection(connectionString))
{
conn.Open();
AseCommand cmd = new AseCommand(sql, conn);
// ...
}
// ✅ 必須使用 base 方法
dsReturn = base.FillDataSet(strSQL, CommandType.Text, parameters, TableName);
5.2 🚫 禁止在 DAL 中包含業務邏輯
// ❌ 禁止:業務邏輯應在 BLL
internal Boolean InsertIfValid(DtoUser user)
{
if (user.Age < 18) // 這是業務規則,不應在 DAL
return false;
// ...
}
// ✅ 正確:DAL 只負責資料操作
internal Boolean Insert(DtoUser user)
{
// 純粹的 INSERT 操作
}
5.3 🚫 禁止字串串接 SQL 參數
// ❌ 嚴重安全漏洞 - SQL Injection
strSQL = "SELECT * FROM users WHERE id = '" + userId + "'";
// ✅ 必須使用參數化查詢
param.Add(new AseParameter("@UserId", AseDbType.VarChar));
param[param.Count - 1].Value = userId;
strSQL = "SELECT * FROM users WHERE id = @UserId";
5.4 🚫 禁止在 DAL 中回傳 List<Dto>
> DAL 允許透過 IEnumerable<T> + yield return 逐列映射 DTO(主流模式),但禁止在 DAL 內自行將整個 DataSet 轉換為 List<Dto> 後回傳。List<T> 的實體化由 BLL 呼叫 .ToList() 負責。
// ❌ 禁止:在 DAL 內部 new List 並回傳
internal List<DtoUser> SelectAllUsers()
{
List<DtoUser> list = new List<DtoUser>();
// ... 手動逐列 Add 到 list
return list;
}
// ✅ 正確(主流):使用 yield return 回傳 IEnumerable
internal IEnumerable<DtoUser> SelectAllUsers()
{
// ... FillDataSet + CP850 轉換
foreach (DataRow dr in dsReturn.Tables[TableName].Rows)
{
DtoUser item = new DtoUser();
item.GetSingleData(dr);
yield return item;
}
}
// ✅ 正確(基礎設施):直接回傳 DataSet
internal DataSet SelectAllUsers()
{
// ...
return dsReturn;
}
---
6. SQL 查詢設計規範
6.1 必要限制
| 規則 | 說明 |
|:-----|:-----|
| 二段式命名 | 使用 表名.欄位名 或 別名.欄位名 格式;有 JOIN 關係時,欄位必須指明所屬的表 |
| WHERE 條件排序 | WHERE 條件設計應將索引欄位值擺前頭,辨識度越高越前 |
6.2 效能建議
| 建議 | 說明 |
|:-----|:-----|
| 索引欄位避免函式 | 避免在索引欄位上使用函式或計算 |
| 避免前置萬用字元 | 避免在 LIKE 條件前使用萬用字元(如 LIKE '%keyword') |
6.3 SQL 查詢範例
// ❌ 禁止:JOIN 時未指明欄位所屬表
strSQL = "SELECT order_id, customer_name FROM orders o JOIN customers c ON o.customer_id = c.id";
// ✅ 正確:二段式命名、表別名指明欄位來源
strSQL = "SELECT o.order_id, o.order_date, c.customer_name "
+ "FROM orders o "
+ "JOIN customers c ON o.customer_id = c.id "
+ "WHERE o.status = @Status";
---
7. 交易處理規範
7.1 交易設計原則
| 規則 | 說明 |
|:-----|:-----|
| 最小範圍 | 僅在需要更新時才開始交易,減少資源佔用時間 |
| 異常 Rollback | 應用程式端必須做好例外處理,確保異常時執行 Rollback |
| 避免大交易 | 禁止在單一交易中包含大範圍操作 |
| 限制 JOIN | 避免在交易中使用過多 JOIN |
| 分層設計 | 將資料庫操作分為上層邏輯與底層存取,提升效率 |
7.2 交易處理範例
// ✅ 正確:使用 DbTransaction 進行跨表交易
DbTransaction tran = null;
try
{
tran = base.BeginTransaction();
DboOrder dboOrder = new DboOrder(tran);
DboOrderDetail dboDetail = new DboOrderDetail(tran);
dboOrder.Insert(orderDto);
dboDetail.Insert(detailDto);
tran.Commit();
}
catch (Exception ex)
{
if (tran != null)
tran.Rollback();
Log.WriteLog(ex);
throw;
}
---
8. 編碼轉換處理
8.1 Sybase CP850 編碼處理
從 Sybase 讀取中文資料時,需進行 CP850 → Unicode 轉換:
internal DataSet SelectById(String id)
{
// ... 查詢邏輯
dsReturn = base.FillDataSet(strSQL, CommandType.Text, parameters, TableName);
// 處理 Sybase 編碼轉換
Utility.CP850TransUnicode(dsReturn, TableName);
return dsReturn;
}
---
9. 存取修飾詞規範
| 成員類型 | 修飾詞 | 說明 |
|:---------|:-------|:-----|
| 類別 | public 或 internal | 依可見範圍需求決定 |
| TableName 常數 | internal const | 允許 BLL 存取表名 |
| 建構子 | internal | 僅允許同組件存取 |
| CRUD 方法 | internal | 僅允許 BLL 呼叫 |
| 私有欄位 | private | 類別內部使用 |
---
10. 程式碼審查檢核表
在提交程式碼前,請確認以下項目:
- [ ] 類別繼承自
DbHelperSQL - [ ] 類別名稱符合
Dbo{EntityName}格式 - [ ] 提供接收
connectionString的建構子 - [ ] 實作
Dispose()方法並釋放所有資源 - [ ] 所有 SQL 使用參數化查詢
- [ ] 所有
base方法呼叫包覆於try-catch - [ ]
catch區塊使用Log.WriteLog(ex)記錄錯誤 - [ ] 方法回傳值符合規範 (失敗時回傳
null/false/-1) - [ ] 無手動建立資料庫連線的程式碼
- [ ] 無業務邏輯程式碼
- [ ] 所有公開方法皆有 XML 註解
---
11. ⚠️ SQL 欄位名稱必須與 data-model.md 完全一致
> 教訓來源:2026-04 實際踩坑
> ewcs_policies_m 的標題欄位是 policy_name,但 DAL 寫成 title → 執行時 Invalid column name 'title'。
> ewcs_financial_rpts_m 無 sort_order、is_active 欄位,但 DAL 包含這兩欄 → INSERT/UPDATE 報錯。
11.1 強制執行原則
撰寫任何 SELECT / INSERT / UPDATE SQL 之前,必須先對照
specs/002-ewcs-welfare-portal/data-model.md 確認每個欄位的實際名稱與是否存在。
11.2 本專案常見陷阱(非通用名稱的欄位)
| 資料表 | ⚠️ 正確欄位名稱 | ❌ 容易誤用的錯誤名稱 |
|:---|:---|:---|
| ewcs_policies_m | policy_name | title, name |
| ewcs_download_forms_m | form_name | title, name |
| ewcs_financial_rpts_m | report_name | title, name |
| ewcs_regulations_m | reg_title | title |
11.3 本專案欄位「不存在」的常見陷阱
| 資料表 | 不存在的欄位(禁止使用) |
|:---|:---|
| ewcs_regulations_m | sort_order |
| ewcs_financial_rpts_m | sort_order, is_active |
| ewcs_album_photos_m | is_active |
11.4 NOT NULL 欄位必須包含於 INSERT
> 若 DB 欄位宣告為 NOT NULL 且無 DEFAULT 值,INSERT 時不提供該欄位會導致「Attempt to insert NULL value into column」錯誤。
| 資料表 | 必含於 INSERT 的 NOT NULL 欄位 |
|:---|:---|
| ewcs_regulations_m | update_dt(需傳入日期或使用 GETDATE()) |
| ewcs_download_forms_m | update_dt(需傳入日期或使用 GETDATE()) |
11.5 撰寫新 DAL 前的欄位核對清單
- [ ] SELECT 清單中的每個欄位名稱與 data-model.md 完全一致
- [ ] INSERT 包含所有 NOT NULL 欄位(包括
update_dt等輔助時間欄) - [ ] UPDATE SET 子句使用正確欄位名稱
- [ ]
DtoXxx.GetSingleData()中dr["欄位名"]與實際 DB 欄位名稱一致 - [ ] 不使用 data-model.md 中未列出的欄位