SQL與資料庫設計

Object-Relational Mapping (ORM): 一種在物件導向程式碼(如 C#)與關聯式資料庫之間自動轉換資料的技術,讓開發者能以操作物件的方式來存取資料庫。ORM 就像一位翻譯官,將程式碼中的物件操作(例如 product.Name = “新名稱”)翻譯成對應的 SQL 指令(UPDATE Products SET Name = ‘新名稱’ WHERE Id = 1)。

EF Core: 可以使用 C# 物件執行 CRUD處理資料轉換變更追蹤 (Change Tracking)資料庫遷移 (Migrations)延遲載入 (Lazy Loading)

# 全域安裝 EF Core 工具。

dotnet tool install –global dotnet-ef

# 安裝 EF Core SQLite 和工具包

dotnet add package Microsoft.EntityFrameworkCore.Sqlite
dotnet add package Microsoft.EntityFrameworkCore.Tools

DbContext: EF Core 的核心類別,是應用程式與資料庫溝通的橋樑,通常放置在 Models/ApplicationDbContext.cs 。

  • DbSet<T> : 分別對應資料庫中的資料表。DbContext 會追蹤所有從資料庫查詢出來的物件,當您修改這些物件後,只需呼叫 

  • SaveChanges() : 當修改物件後,只需呼叫 SaveChanges() DbContext 就會自動生成對應的 SQL 指令來更新資料庫,確保資料的一致性。

用 DbContext 處理 CRUD 操作

C 新增:使用 Add 方法插入新記錄。填入實體實例後,呼叫 SaveChanges 方法將其儲存到資料庫中。

var newProduct = new Product { Name = “Laptop”, Price = 1200.99m };

  • m 標記為 decimal 類型

context.Products.Add(newProduct);

context.SaveChanges();

R 讀取: Find 、 FirstOrDefault ToList 等方法允許從資料庫中檢索資料(無論是單一還是多個記錄)。

var allProducts = context.Products.ToList();

Console.WriteLine(“All Products:”);

allProducts.ForEach(p => Console.WriteLine($”{p.Id}: {p.Name} – ${p.Price}”));

U 更新:使用 Find 或 ToList 取得記錄 ,根據需要修改,然後套用 Update SaveChanges 來反映變更。

var singleProduct = context.Products.Find(1);

Console.WriteLine($”Product Found: {singleProduct.Name} – ${singleProduct.Price}”);

singleProduct.Price = 1100.99m;    // 更新數據

context.SaveChanges();

D 刪除:使用 Find 或 ToList 檢索記錄 ,將其傳遞給 Remove,然後呼叫 SaveChanges 將其從資料庫中刪除。

context.Products.Remove(singleProduct);

context.SaveChanges();

第一範式 (1NF):原子性(Atomic)

  • 核心要求: 第一範式要求資料表中的每個欄位都必須是原子性的,也就是說,欄位裡的值不能再被分割,也不能包含多個值或一個列表。這能確保數據的最小顆粒度

  • ❌ 錯誤範例(不符合 1NF): 在一個名為 客戶資料 的表格中,有一個 客戶_電話 欄位儲存了多個電話號碼,例如 101, 王小明, 0912-3456, 0987-6543。由於一個欄位裡有兩個電話號碼,這違反了原子性。

  • ✅ 正確範例(符合 1NF): 要修正這個問題,有兩種常見的做法。

    • 方法一是將欄位拆分成多個獨立的欄位,例如:客戶_電話_主要客戶_電話_備用

    • 方法二是將電話號碼拆分到一個獨立的 客戶_電話 資料表。這樣,客戶 ID 為 101 的客戶就會佔據兩行紀錄,每行只包含一個電話號碼,實現了數據的原子性。

第二範式(2NF):完全依賴主鍵

  • 核心要求: 第二範式適用於使用複合主鍵的資料表。它要求所有的非主鍵欄位都必須完全依賴於整個複合主鍵,不能只依賴於主鍵中的一部分。這一步的目的是消除部分依賴

  • ❌ 錯誤範例(不符合 2NF): 假設有一個 訂單明細 表格,其複合主鍵是 (訂單 ID, 產品 ID)。表格中包含了 數量產品名稱產品單價 等欄位。

    • 錯誤發生在 產品名稱產品單價 欄位,因為它們只依賴於 產品 ID,而與 訂單 ID 無關。這導致同一個產品的名稱和單價資訊在不同的訂單中被重複儲存。

  • ✅ 正確範例(符合 2NF): 要符合 2NF,必須將只依賴於 產品 ID 的欄位(即 產品名稱產品單價拆分到一個獨立的 產品 資料表

    • 原來的 訂單明細 表格只保留 訂單 ID產品 ID數量。通過這種拆分,我們消除了重複的產品資訊,提高了數據的一致性。

第三範式(3NF):消除傳遞依賴 (Transitive Dependency)

    • 核心要求: 第三範式要求非主鍵欄位不能依賴於另一個非主鍵欄位。這確保了表中的所有資訊都與其主鍵直接相關,進一步減少了數據的冗餘和維護的複雜性。

    • ❌ 錯誤範例(不符合 3NF): 假設有一個 員工資料 表格,主鍵是 員工 ID。表格中包含了 員工姓名部門 ID部門名稱

      • 這裡的錯誤是 部門名稱 依賴於 部門 ID,而 部門 ID 是一個非主鍵欄位。

      • 這是一個 傳遞依賴員工 ID部門 ID部門名稱,這導致如果某個部門(例如 ID 為 D10)的名稱需要修改,你必須修改所有屬於 D10 部門的員工記錄。

    • ✅ 正確範例(符合 3NF): 要符合 3NF,必須將具有傳遞依賴關係的欄位(部門 ID部門名稱拆分到一個獨立的 部門 資料表

      • 修正後的 員工資料 表格只保留 員工 ID員工姓名部門 ID(作為外來鍵)。

      • 這樣,當部門名稱需要變動時,只需要在獨立的 部門 表格中修改一次即可,徹底消除了更新異常。

ACID 原則: 確保資料庫交易的可靠性,是關聯式資料庫的基石。

  • 原子性 (Atomicity): 交易被視為一個不可分割的工作單元。例如,銀行轉帳包含「從 A 帳戶扣款」和「向 B 帳戶存款」兩個操作,這兩個操作必須要麼同時成功,要麼同時失敗回滾。
  • 一致性 (Consistency): 交易前後,資料庫的完整性約束不能被破壞。例如,轉帳前後,所有帳戶的總金額應保持不變。
  • 隔離性 (Isolation): 在多個交易並行執行時,每個交易都感覺不到其他交易的存在。系統透過鎖定機制來實現,確保交易之間互不干擾。
  • 耐久性 (Durability): 一旦交易被成功提交 (Commit),其結果就是永久性的。即使發生系統斷電或崩潰,資料也不會遺失,通常是透過交易日誌 (Transaction Log) 來保證。

並行控制 (Concurrency Control): 解決多使用者同時存取資料時可能發生的問題。不同的隔離級別提供了在資料準確性與系統效能之間的權衡。

  • Read Uncommitted: 效能最好,但允許一個交易讀取另一個尚未提交的交易所做的修改,可能導致髒讀 (Dirty Read)
  • Read Committed: 大多數資料庫的預設級別。它保證一個交易只能讀取已經提交的變更,避免了髒讀。但如果在同一個交易中兩次讀取同一筆資料,中間若有其他交易修改了它,會讀到不同的結果,稱為不可重複讀取 (Non-Repeatable Read)
  • Repeatable Read: 保證在同一個交易中多次讀取同一筆資料時,結果總是一致的。但它無法阻止其他交易插入符合查詢條件的新資料,導致幻讀 (Phantom Read)
  • Serializable: 最嚴格的級別,透過鎖定整個查詢範圍來防止任何形式的並行問題,確保資料的絕對一致性,但會嚴重影響系統的並發效能。

索引是提升查詢速度的關鍵,其本質是用額外的儲存空間來換取更快的讀取時間,但會降低寫入操作的效能,因為每次寫入都需要同步更新索引。

聚集索引 (Clustered Index):

  • 常用於 : ID (主鍵/自增長)、OrderDate (日期欄位)
  • 特性: 資料表的物理儲存順序與索引鍵值的邏輯順序完全一致。就像一本按姓氏排序的電話簿,資料本身就是按順序存放的。
  • 限制: 由於物理順序只能有一種,一個資料表只能有一個聚集索引,通常設定在主鍵上。
  • 用途: 提供最快的資料查找速度,特別適合範圍查詢(例如,查詢某個日期區間內的所有訂單)。

非聚集索引 (Nonclustered Index):

  • 常用於 : LastName、Email、ZipCode、Status。
  • 特性: 建立一個獨立的排序結構(通常是 B-Tree),該結構包含索引鍵值以及一個指向實際資料行的指標(或聚集索引的鍵值)。就像書本最後的索引頁,它告訴你某個關鍵字在哪一頁,你需要根據頁碼再翻到對應位置。
  • 限制: 一個資料表可以有多個非聚集索引,以滿足不同的查詢需求。
  • 用途: 為不同的查詢條件提供多種快速查找途徑,而不改變資料的物理順序。

索引使用注意事項:

  • 避免過度索引 (Over-Indexing),每個額外的索引都會在 INSERTUPDATEDELETE 時帶來額外開銷。
  • 定期清理未使用的索引 (Unused Indexes),這些索引只會白白佔用儲存空間並拖累寫入效能。
  • 善用複合索引 (Compound Index),即在多個欄位上建立單一索引,以優化涉及這些欄位的複雜查詢。
  • 透過分析查詢計畫 (Query Plan) 來驗證索引是否被資料庫引擎有效利用,並進行相應調整。

將常用的 SQL 邏輯封裝在資料庫中,可以被應用程式重複呼叫,帶來多重效益。

優點:

  • 提升效能: 預存程序在首次執行時會被編譯並快取執行計畫,後續呼叫時能直接重用,避免了重複的 SQL 解析與優化過程。
  • 強化安全: 可以只授予使用者執行特定程序的權限,而不讓他們直接存取底層資料表,有效防止未經授權的資料操作與 SQL 注入攻擊。
  • 減少網路流量: 對於複雜的操作,應用程式只需傳送一個簡短的呼叫指令,而不是一長串的 SQL 腳本,大幅降低了客戶端與伺服器之間的網路傳輸量。
  • 模組化與維護性: 將業務邏輯集中在資料庫中管理,當邏輯需要變更時,只需修改預存程序,而無需重新部署所有應用程式。

純量函數 (Scalar-Valued Functions): 返回一個單一值(如數字、字串),常被用在 SELECT 語句的欄位列表或 WHERE 子句中。

— 假設我們有一個 ProductPrice 表格
— 目的:根據原始價格計算 8 折後的價格

CREATE FUNCTION CalculateDiscountPrice
(
    @OriginalPrice DECIMAL(10, 2) — 輸入參數:原始價格
)
RETURNS DECIMAL(10, 2) AS  BEGIN       
 — 返回值:折扣後的單一價格
   DECLARE @DiscountPrice DECIMAL(10, 2);
   — 計算折扣價格 (8 折)
   SET @DiscountPrice = @OriginalPrice * 0.80;
   RETURN @DiscountPrice;
END;
GO

# 接收@OriginalPrice然後回傳計算後的@DiscountPrice數值

— 如何使用 (效果:在 SELECT 語句中像內建函數一樣使用)
SELECT
   ProductID,Price AS OriginalPrice,dbo.CalculateDiscountPrice(Price) AS DiscountPrice 
FROM
    Products;

# dbo.CalculateDiscountPrice(Price):

  • 用途: 在 SELECT 語句中,你直接呼叫了你剛剛建立的函數。
  • dbo.: 在 SQL Server 中,dbo 是預設的資料庫擁有者 (Database Owner) 結構描述 (Schema),呼叫自定義函數時通常需要加上它。
  • Price: 這是傳入函數的參數。在這裡,它使用了 Products 表格中每一行的 Price 欄位的值作為輸入。

資料表值函數 (Table-Valued Functions): 返回一個完整的資料表(結果集),可以像一般資料表一樣在 FROM 子句中被查詢或連接。

— 目的:根據部門 ID 篩選出該部門的所有員工

CREATE FUNCTION GetEmployeesByDepartment
(
   @DepartmentID INT  — 輸入參數:部門 ID
)

RETURNS TABLE  AS  RETURN   — 返回值:一個資料表 (結果集)

(
    — 這裡的查詢結果就是這個函數返回的資料表
   SELECT 
       EmployeeID , FirstName , LastName,Salary
   FROM 
       Employees
   WHERE 
       DepartmentID = @DepartmentID
);

GO
— 如何使用 (效果:在 FROM 語句中像資料表一樣使用)
SELECT
   FirstName, Salary
FROM
    dbo.GetEmployeesByDepartment(101); 

— 將函數視為一個資料表來源

  • 參數化查詢: 將使用者輸入嚴格視為資料,而非可執行的程式碼。這是防範 SQL 注入最有效且必須採用的首選方法。
  • 輸入驗證: 在應用程式層面對所有使用者輸入進行嚴格的白名單驗證,限制其類型、格式與長度,作為額外的防線。
  • 預存程序: 將 SQL 邏輯封裝在資料庫中,並限制應用程式只能透過參數化方式呼叫這些程序,進一步隔離了程式碼與資料。
  • 身份驗證 (Authentication): 透過強密碼策略、多因素驗證 (MFA) 及單一登入 (SSO) 等機制,嚴格驗證「使用者是誰」。
  • 授權 (Authorization): 遵循最小特權原則 (Principle of Least Privilege),確保每個使用者僅被授予完成其工作所必需的最小權限。常透過基於角色的存取控制 (RBAC) 來系統化地管理權限。
  • 加密: 對靜態資料 (Data at Rest)(儲存在硬碟上的資料)和傳輸中資料 (Data in Transit)(在網路中傳輸的資料)進行加密,確保即使資料被竊取也無法讀取。
  • 防火牆: 設定嚴格的網路存取規則,限制僅允許來自可信任 IP 或應用程式伺服器的連線,阻擋未經授權的外部存取。
  • 定期備份: 制定完善的備份策略(如完整、差異、交易日誌備份),並將備份加密後儲存在安全的異地,確保在災難發生時能可靠地復原資料。
  • 持續監控與日誌: 啟用資料庫稽核功能,不間斷地監控所有資料庫活動。日誌記錄可以捕捉到異常或未經授權的行為,為即時威脅偵測與事後追蹤提供依據。

發佈留言