前言
歡迎關(guān)注dotnet研習(xí)社,今天我們要討論的內(nèi)容是,曾經(jīng)風(fēng)靡一時(shí)的存儲(chǔ)過程
用法。到如今在C#項(xiàng)目調(diào)用Sqlserver的存儲(chǔ)過程,為什么不被認(rèn)為是一個(gè)好的方式?那些老的項(xiàng)目該怎么辦?檢索到的存儲(chǔ)過程相關(guān)內(nèi)容,都是禁止使用,不建議使用的標(biāo)題。那么我們還能再用存儲(chǔ)過程嗎?
?
在許多企業(yè)級(jí)系統(tǒng)或傳統(tǒng)應(yīng)用開發(fā)中,調(diào)用 SQL Server 存儲(chǔ)過程(Stored Procedure, SP)是一個(gè)非常常見的做法。尤其在以數(shù)據(jù)庫(kù)為中心的系統(tǒng)架構(gòu)中,開發(fā)者習(xí)慣將大量邏輯寫在數(shù)據(jù)庫(kù)中,用 C# 去調(diào)用它們完成各種業(yè)務(wù)功能。
但在現(xiàn)代軟件工程中,這種方式卻常常被質(zhì)疑、甚至被認(rèn)為是反模式(Anti-pattern)。我們將結(jié)合架構(gòu)設(shè)計(jì)、可測(cè)試性、可維護(hù)性等多個(gè)維度,分析為什么C#項(xiàng)目中調(diào)用“存儲(chǔ)過程”不是最佳實(shí)踐,以及哪些場(chǎng)景下它依然值得使用。
SQL Server 存儲(chǔ)過程
存儲(chǔ)過程 Procedure 是一組為了完成特定功能的 SQL 語(yǔ)句集合,經(jīng)編譯后存儲(chǔ)在數(shù)據(jù)庫(kù)中,用戶通過指定存儲(chǔ)過程的名稱并給出參數(shù)來(lái)執(zhí)行。
存儲(chǔ)過程中可以包含邏輯控制語(yǔ)句和數(shù)據(jù)操縱語(yǔ)句,它可以接受參數(shù)、輸出參數(shù)、返回單個(gè)或多個(gè)結(jié)果集以及返回值。
由于存儲(chǔ)過程在創(chuàng)建時(shí)即在數(shù)據(jù)庫(kù)服務(wù)器上進(jìn)行了編譯并存儲(chǔ)在數(shù)據(jù)庫(kù)中,所以存儲(chǔ)過程運(yùn)行要比單個(gè)的 SQL 語(yǔ)句塊要快。同時(shí)由于在調(diào)用時(shí)只需用提供存儲(chǔ)過程名和必要的參數(shù)信息,所以在一定程度上也可以減少網(wǎng)絡(luò)流量、網(wǎng)絡(luò)負(fù)擔(dān)。

非最佳實(shí)踐的理由
? 一、職責(zé)分離:應(yīng)用邏輯不應(yīng)“藏”在數(shù)據(jù)庫(kù)中
現(xiàn)代應(yīng)用倡導(dǎo)清晰的分層架構(gòu)(例如三層架構(gòu)、DDD、Clean Architecture)。每一層都有明確職責(zé):
- ? 表示層(UI):負(fù)責(zé)用戶交互;
- ? 業(yè)務(wù)邏輯層(Domain/Application):實(shí)現(xiàn)業(yè)務(wù)規(guī)則;
- ? 數(shù)據(jù)訪問層(Infrastructure):負(fù)責(zé)數(shù)據(jù)持久化;
將業(yè)務(wù)邏輯寫入數(shù)據(jù)庫(kù)中的存儲(chǔ)過程,會(huì)讓職責(zé)劃分變得模糊:
- ? 一部分業(yè)務(wù)規(guī)則在 C# 中;
- ? 另一部分藏在數(shù)據(jù)庫(kù)的 SP 中;
- ? 日后維護(hù)時(shí),你不再知道“訂單審核邏輯”究竟是寫在代碼里,還是藏在某個(gè) SP 里。
這不僅違背了關(guān)注點(diǎn)分離(Separation of Concerns)
的原則,還讓維護(hù)人員疲于奔命。
? 二、存儲(chǔ)過程難以測(cè)試與調(diào)試
對(duì)比一下:
- ? C# 方法:可以使用 xUnit、NUnit、Moq 進(jìn)行精細(xì)化單元測(cè)試;
- ? 存儲(chǔ)過程:只能通過寫 SQL 腳本人工測(cè)試,或者執(zhí)行后看數(shù)據(jù)庫(kù)結(jié)果。
存儲(chǔ)過程屬于“黑盒邏輯”:
這對(duì)現(xiàn)代軟件開發(fā)流程中的 自動(dòng)化測(cè)試、持續(xù)集成(CI)和持續(xù)交付(CD) 是一個(gè)巨大障礙。
? 三、可維護(hù)性差,版本控制麻煩
一個(gè)存儲(chǔ)過程的生命周期和源代碼不是一回事:
- ? C# 項(xiàng)目源碼:用 Git 版本管理,有變更記錄;
- ? 存儲(chǔ)過程:往往直接部署到數(shù)據(jù)庫(kù),甚至沒人備份過修改前的版本。
很多團(tuán)隊(duì)缺乏對(duì)數(shù)據(jù)庫(kù)對(duì)象的版本管理,一旦某人改了 SP 出錯(cuò)了,回滾都無(wú)從談起。
此外,存儲(chǔ)過程的調(diào)試和定位問題極其痛苦,沒有 IntelliSense、沒有類型提示、無(wú)法跳轉(zhuǎn)引用,維護(hù)成本遠(yuǎn)高于普通 C# 代碼。
? 四、數(shù)據(jù)庫(kù)耦合嚴(yán)重,降低系統(tǒng)可移植性
如果業(yè)務(wù)邏輯大量依賴 T-SQL 寫的存儲(chǔ)過程,那基本和 SQL Server“綁死”了。遷移到 PostgreSQL、MySQL、Oracle?可能要重寫一大堆邏輯,甚至整個(gè)系統(tǒng)的架構(gòu)。
現(xiàn)代開發(fā)傾向于“盡量避免對(duì)具體技術(shù)棧的深度耦合”,ORM(如 EF Core、Dapper)正是這種趨勢(shì)的體現(xiàn)。
? 五、安全與性能問題
- ? 安全性問題:如果存儲(chǔ)過程權(quán)限配置不當(dāng),可能讓用戶執(zhí)行敏感操作;
- ? SQL 注入問題:雖然 SP 能緩解注入風(fēng)險(xiǎn),但不當(dāng)拼接參數(shù)依然可能出錯(cuò);
- ? 性能問題:SP 執(zhí)行效率并非一定高于應(yīng)用層邏輯處理,尤其是在業(yè)務(wù)邏輯復(fù)雜、需要頻繁變更的場(chǎng)景下。
那么,存儲(chǔ)過程還能用嗎?
當(dāng)然能。在以下場(chǎng)景中使用存儲(chǔ)過程是合理甚至推薦的:
? 什么時(shí)候使用存儲(chǔ)過程是“合理”的?
- 1. 批量數(shù)據(jù)處理
比如大量插入、更新、數(shù)據(jù)清洗等操作,在數(shù)據(jù)庫(kù)內(nèi)執(zhí)行效率更高。 - 2. 已有遺留系統(tǒng)
如果數(shù)據(jù)庫(kù)中已有大量穩(wěn)定的存儲(chǔ)過程邏輯,不建議全部遷移,可以做適配層。 - 3. 需要數(shù)據(jù)庫(kù)級(jí)別的訪問控制
有些系統(tǒng)需要通過 SP 封裝所有操作,外部只能調(diào)用已授權(quán)的存儲(chǔ)過程,這是合理的安全策略。 - 4. 執(zhí)行計(jì)劃重用
SP 通常擁有緩存執(zhí)行計(jì)劃的能力,某些高頻查詢可以利用這一特性提升性能。
? 更推薦的替代方案
| |
| |
| 使用 View / Function(僅作為查詢層) |
| |
| 使用遷移腳本(如 EF Core Migration、DbUp) |
| 使用 Mock Repository,避免依賴數(shù)據(jù)庫(kù) |
總結(jié)
“能寫在代碼里的邏輯,就不要藏在數(shù)據(jù)庫(kù)里。”
這是很多資深架構(gòu)師的共識(shí)。C# 與 SQL Server 的存儲(chǔ)過程配合雖然很常見,但在現(xiàn)代架構(gòu)中,更推薦將 業(yè)務(wù)邏輯回歸到應(yīng)用層,數(shù)據(jù)庫(kù)應(yīng)作為“持久化工具”而非“業(yè)務(wù)大腦”。
當(dāng)然,存儲(chǔ)過程并非“一無(wú)是處”,只要在合適的場(chǎng)景使用,依然可以成為你系統(tǒng)的利器。但濫用,則會(huì)讓你的代碼和數(shù)據(jù)庫(kù)一同失控。