最近看到一篇“CPQuery, 解決拼接SQL的新方法”(http://www.cnblogs.com/fish-li/archive/2012/09/10/CPQuery.html),由於里面的思路基本是錯誤的,很擔心影響園里的初學者。我在帖子里回復了幾次,給博主 fish-li 一點小小的建議,沒有想到,這位老兄說不過,就直接刪除我的回帖,包含別人在貼中對我的發問,如此惡劣行徑,實屬罕見。鄙人不才,不能保證每次發帖回帖都正確,也有在園中說錯話道歉的時候,但從不刪說錯的、道歉的貼/回復。
特另單寫一篇博文,闡述我的觀點,也歡迎大家用踴躍討論。
就事論事,這篇博文的錯誤有以下幾點:
1. 該 CPQuery 嘗試繞開 Ado.net 的 command.Parameters.Add() ,另寫函數來對付 SQL 注入。這是很錯誤的做法。
很多人都以為,對付SQL 注入很容易,只要把單引號處理掉就行了,容易。其實,這種想法是非常錯誤的。
對於類似這樣的代碼:
query = query + " and ProductName like '" + p.ProductName + "'";
轉換字符數據中間的特殊字符,實際上是一件相當困難的事情。PHP 中有個 addslashes 函數,發布多年,漏洞不斷,補丁也不斷:
Addslashes() 漏洞(2004年,%00)
addslashes:會導致SQL注入漏洞(2006年,0xbf27)
http://blog.csdn.net/felio/article/details/1226569
GBK字符集下addslashes函數的注入漏洞及BUG的解決辦法(2011 年,addslashes函數在進行轉義的時候,只對二進制字符串操作二不考慮字符集)
http://www.itlearner.com/article/4824
從上面可以從側面看出其中的難度。
在 SQL 注入(http://msdn.microsoft.com/zh-cn/library/ms161953.aspx) 這篇文章中,提到:
如果可能,拒絕包含以下字符的輸入:
輸入字符 |
在 Transact-SQL 中的含義 |
---|---|
; |
查詢分隔符。 |
' |
字符數據字符串分隔符。 |
-- |
注釋分隔符。 |
/* ... */ |
注釋分隔符。服務器不對 /* 和 */ 之間的注釋進行處理。 |
xp_ |
用於目錄擴展存儲過程的名稱的開頭,如 xp_cmdshell。 |
然而,這僅僅是 SQL Server 所需要處理的特殊字符,如果是其他數據庫,又有別的字符需要處理,難度不是一點點。
如果用 Ado.net ,它所帶的驅動程序,已經替我們做了這些事情,我們只需要調用 cmd.Parameters.Add(p); 即可。
當然了,這篇微軟的文章,還提到,"應檢查所有調用 EXECUTE、EXEC 或 sp_executesql 的代碼 ...在選擇的每個存儲過程中,驗證是否對動態 Transact-SQL 中使用的所有變量都進行了正確處理..."。這個難度就大了。存儲過程中,難以調用 cmd.Parameters.Add(p) 來轉換參數。看來在存儲過程中,處理字符串起來,要格外小心。
嘗試自己寫 SQL 特殊字符處理函數,是一個特別沒有“投資收益比”的事情,很難寫好,就算寫好了,也沒有什么特別的收益,因為數據庫的驅動程序,已經把這個做好了。無論是 C# 還是 Java 的程序員,都不應自己寫 addslashes 函數。
在 Java 中,Apache 有個 http://commons.apache.org/lang/ ,本來提供了一個 StringEscapeUtils.escapeSql() 函數,但是后來主動去掉了這個函數,理由是:
StringEscapeUtils.escapeSql has been removed from the API as it was misleading developers to not use PreparedStatement
也就是說,這類 escapeSql() 函數,誤導開發者不用開發語言本身的 PreparedStatement,是不好的。所以,最好不要寫此類函數。
2. CPQuery 的另一個,據博主 fish-li 所說,“優點”之一,就是拼接 SQL 方便。然而,使用 cmd.Parameters.Add(p) 一樣拼接 SQL 方便:
using (DbCommand cmd = this.mCon.CreateCommand()) { cmd.Transaction = this.mTrans; string sql = "select ... from ... where 1=1 and ... "; if (uploadFlag != null) { sql += " and t." + TtDataVeccTransferBaseDao.COL_UPLOADFLAG + "= ?"; DbParameter p = cmd.CreateParameter(); p.ParameterName = "@COL_UPLOADFLAG"; p.Value = uploadFlag; cmd.Parameters.Add(p); } ... }
以下幾行,可以寫在一個公共函數中:
DbParameter p = cmd.CreateParameter(); p.ParameterName = "@COL_UPLOADFLAG"; p.Value = uploadFlag; cmd.Parameters.Add(p);
這樣最后變成只有兩行:
sql += " and t." + TtDataVeccTransferBaseDao.COL_UPLOADFLAG + "= ?"; addParameterValue(cmd,uploadFlag);
從這兩方面看來,CPQuery 本身出現的意義就有疑問。
一般來說,另寫數據庫訪問組件/庫,如果有以下幾個方面的優勢,是可以考慮的:
a. 代碼自動生成,可以提高開發效率
b. 封裝后更不容易出錯,這也可以做,畢竟很多開發隊伍中,都有新手。
c. 減少代碼量。
d. 提供 Ado.net 或者數據庫驅動沒有的額外功能
比如 Hibernate 的緩存(單表按主鍵查詢,查詢多次,實際只會執行一次,這里是假設數據在短時間內不會被其他人更改,利用緩存提高性能)。又比如,Ado.net 之后,微軟相繼發布 linq to sql, EF, 但是這兩個都只支持 SQL Server ,於是很多人做出類似的、支持其他數據庫的 linq/EF,拿出來賣,也不錯。
從“CPQuery, 解決拼接SQL的新方法” 這篇文章所說,以上幾點,它都沒有做到。因此,這個 CPQuery 是沒有什么用處的。
至於該文提到的幾點,都是錯誤的:
2. 可能會影響性能:每條SQL語句都需要數據庫引擎執行[語句分析]之類的開銷。
3. 影響代碼的可維護性:SQL語句與C#混在一起,想修改SQL就得重新編譯程序,而且二種代碼混在一起,可讀性也不好。
在數據庫系統中,SQL 的解析時間,基本可以忽略不計。相對於 SQL 及結果數據在網絡中傳輸、SQL查詢數據的搜索時間,那么一點點 SQL 的解析時間,根本可以忽略。所謂 SQL 解析時間要優化,純粹是學院派的胡說。
至於 SQL語句與C#混在一起,這個要看個人的寫程序風格了。用 Ado.net 還是用 CPQuery,都有這個問題。因此,這不是 CPQuery 的優點。
用 Ado.net 也可以寫成這樣的函數:
GetUploadData(uploadFlag), 然后把上面的代碼放在里面。這樣僅僅數據庫相關的代碼在一起,其他代碼都在這個 GetUploadData 外面,也沒有什么“SQL語句與C#混在一起”的問題。
至於“想修改SQL就得重新編譯程序”,這是避免不了的。如果從業務邏輯上看,需要把 SQL 中的 > 改成 >= ,確實需要重新編譯程序。用 CPQuery 還是用 Ado.net ,都是如此。
我個人覺得,CPQuery 的作者 fish-li ,對 Ado.net 的不熟悉,才導致了他寫出了 CPQuery 。這本沒有什么。Hibernate 的作者也是不願意手工寫 SQL 才弄出了一個 Java ORM,但是人家有自己的賣點:通過反射來把結果集數據,填充到 java 對象里,不用一個個字段賦值;通過緩存,單表按主鍵查詢,查詢多次,實際只會執行一次,提高性能。這兩個賣點,都是 Java JDBC 本身沒有的。
CPQuery 提供了什么 Ado.net 缺乏的東西么?看不到。
所以說,這個 CPQuery 沒有什么用處。