前陣子,我應邀對幾個企業及學校進行一系列的LINQ教育訓練課程,對於LINQ處理物件集(Collection、Array、List)的能力,學員們多半抱持肯定的態度
ORM VS SQL
文/黃忠成
前言
前陣子,我應邀對幾個企業及學校進行一系列的LINQ教育訓練課程,對於LINQ處理物件集(Collection、Array、List)的能力,學員們多半抱持肯定的態度,就長年缺乏資料查詢技術的近代語言來說,這自是當然。你能夠以Set來去除相同的資料,但比不上用Distinct指令來簡潔且易懂,能使用迴圈及運算來Join兩個Array,自然比不上一個Join指令來的輕鬆,這是LINQ的魅力之一。但對於LINQ的最顯眼之處,ORM的處理而言,則恰恰相反,學員無法理解,以往只要下一串SQL指令就可以完成的工作,現在為何要用另一種語法來做,結果又沒有比下一串SQL指令來的輕鬆,科技的發展,程式語言的變革,不是應該要越來越直覺,越發簡單才是嗎?
ORM與傳統SQL語句間的差異
ORM的出現,相信你早已由網路資料、書籍中得知許多,但心中或有疑問,為何要捨SQL指令不用,而去學習一種新的語法,新的資料存取架構?本文嘗試由反向角度出發,探討著ORM為何會出現,SQL指令又有何不妥之處,讓ORM將其隱藏起來。在你看過的資料中,多半會列出一張表,告訴你ORM有何好處,我們就從這張表開始吧。
表1 ORM的優點
1、以物件表現列、以屬性表現欄,以純OOP的概念來操控資料庫。 |
2、隱藏SQL指令,以此達到跨資料庫時,不需修改應用程式的目的。 |
3、以單一語法,操控所有資料庫,減輕學習者的負擔。 |
4、自動產生SQL指令,避免因程式設計師能力不同,寫出效率不彰的應用程式。 |
5、自動產生SQL指令,避免程式設計師對安全性的認知程度不同,寫出不安全的應用程式。 |
OK,現在就這張表,我們反向推回來,表中所列的優點,真的是我們需要的嗎?就第一點來說,以ADO.NET連線模式(SQLConnection、SQLDataReader)來說,資料的呈現是以Result Set(結果集)方式,我們建立連線,取得一個結果集,使用DataReader一列列的讀取資料,要取得某個欄位時,就使用GetString、GetInt32等函式,例如下面的程式碼。
程式1
SqlConnection conn =
new SqlConnection("Data Source=JEFFRAY;Initial Catalog=Northwind;User ID=sa");
conn.Open();
SqlCommand cmd = new SqlCommand("SELECT * FROM Customers WHERE CustomerID LIKE '%V%'", conn);
using(SqlDataReader reader =cmd.ExecuteReader(CommandBehavior.CloseConnection))
{
while(reader.Read())
string s = reader.GetString(reader.GetOrdinal("CustomerID"));
}
new SqlConnection("Data Source=JEFFRAY;Initial Catalog=Northwind;User ID=sa");
conn.Open();
SqlCommand cmd = new SqlCommand("SELECT * FROM Customers WHERE CustomerID LIKE '%V%'", conn);
using(SqlDataReader reader =cmd.ExecuteReader(CommandBehavior.CloseConnection))
{
while(reader.Read())
string s = reader.GetString(reader.GetOrdinal("CustomerID"));
}
這段程式碼為ORM的誕生下了一個肯定的條件,我們知道GetString可以取得CustomerID的欄位值,但我們也無法否認,當CustomerID欄位型態不是String,或是CustomerID的欄位名稱不是CustomerID時,這段程式碼就會拋出例外訊息,直到程式執行的那一刻前,我們都無法肯定這段程式碼一定正確。
不過,這是ORM崇尚者的說法,就一般 SQL使用者而言,很難想像,CustomerID為何不是CustomerID,String為何會變成其它型態的場景,在真實案例中,資料庫的制定與管理是何等重要,改資料庫的規格或是欄位的型態是何等大事,豈可輕易為之。
如果你是這麼想的,那麼我恭喜你,你正處於一個相當幸福的開發團隊中,這裡不會在案子進行期間改動已制定的資料庫規格,因此你不會處於當一個欄位名稱因某人因素需要而調整時,你得啟動小腦袋裡的搜尋引擎,運用已配置給家庭、妻子、兒女而所剩無幾的記憶體來查詢,究竟有那些程式碼用到了這個資料表,然後再耗上一整天來修改這些地方,最糟的是,改完後你仍然不確定是否會有漏網之魚,測試人員會不會蹦現在眼前,氣急的質詢為何以前測過正常的功能,現在又出錯了!
很明顯的,大多數軟體工程師都沒有處在前述的理想環境中是吧?因此,我們有了Typed DataSet(強式型別資料集)。
程式2
SqlConnection conn =
new SqlConnection("Data Source=JEFFRAY;Initial Catalog=Northwind;User ID=sa");
conn.Open();
DataSet1TableAdapters.CustomersTableAdapter adapter =
new ConsoleApplication1.DataSet1TableAdapters.CustomersTableAdapter();
DataSet1.CustomersDataTable table = adapter.GetData();
string s = table[0].CustomerID;
new SqlConnection("Data Source=JEFFRAY;Initial Catalog=Northwind;User ID=sa");
conn.Open();
DataSet1TableAdapters.CustomersTableAdapter adapter =
new ConsoleApplication1.DataSet1TableAdapters.CustomersTableAdapter();
DataSet1.CustomersDataTable table = adapter.GetData();
string s = table[0].CustomerID;
相較於前例,這段程式又有何優點呢?官方說明是,Typed DataSet可以避免你打錯字,也就是下面的情況不會發生。
string s = reader.GetString(reader.GetOrdinal("CustomeID")); //錯誤,但執行時才會發現
string s = table[0].CustomeID; //錯誤,但編譯時就已察覺
string s = table[0].CustomeID; //錯誤,但編譯時就已察覺
也可避免使用錯誤的型別。
int s = reader.GetInt (reader.GetOrdinal("CustomeID")); //錯誤,但執行時才會發現
int s = table[0].CustomeID; //錯誤,但編譯時就已察覺
int s = table[0].CustomeID; //錯誤,但編譯時就已察覺
不過這兩個例子都無法充份證明使用Typed DataSet比傳統寫法好,尤其當資料庫規格改變時,Typed DataSet是否能協助我們調整程式?答案是肯定的,舉例來說,當CustomerID改名為Customer_ID時,以傳統寫法而言,我們需要去改原始碼,但以Typed DataSet而言,至少有兩種選擇可以用,一是直接改動.xsd,將CustomerID欄位名稱改為Customer_ID,此時編譯時,編譯器自會告訴你那裡該調整,而且不會有遺漏!二是直接改變.xsd中,TableAdapter的Insert、Select、Delete、Update等SQL指令,將Customer_ID的Alias指定為CustomerID,這樣就不用修改程式。
如果Typed DataSet可以處理這種情況,那麼再接下來的ORM又因何誕生?話說,雖然Typed DataSet提供了Typed Query(不知道這是啥?呃...查Google吧),但我打賭有些人不會那麼乖巧,為每一個查詢條件建一個Typed Query,尤其在Typed DataSet沒有把Fill函式拿掉的情況下,你的程式中或多或少一定會出現SQL指令,那麼,你並沒有因使用Typed DataSet而從這個泥淖中跳出來。
那ORM解決了此問題了嗎?它成功的將我們從泥淖中救出了嗎?
很明顯的,針對欄位修改議題,多數的ORM如Hibernate、LINQ To SQL、LINQ To Entity Framework,LLIBGEN都允許透過修改Metadata(對應表)、Mapping Class(對應類別)的方式來調整類別與資料表間的對應,有提供專屬SQL模擬語法的ORM,如Hibernate、LINQ To Entity Framework,還能將對應套用到直接下達專屬SQL模擬語法的程式上,你的程式不會與SQL直接相關,自然就不會因直接使用SQL,而導致陷入修改欄位名稱、型別時的困境中。
OK,時到時擔當、沒米才煮蕃薯湯,這是一部份工程師處理事情的方式,反正事情沒發生,所以,舊有方法還是有效的,如果你是這麼想,那麼我承認無法說服你,ORM究竟能幫上你什麼忙。
技術狂熱者的產物?
有些人認為,ORM其實是OOP技術狂熱者的產物,這些人巴不得用物件來統一世界,我不否認這點,因為,就我所觀察所得,無法撇開此原因是ORM現身的原因之一。但所有技術都其來有自,僅有熱情與野心,無法讓其它人認同。
ORM的優點來自於隱藏SQL語句及物件呈現上,雖說隱藏SQL語句,倒不如說是ORM自動產生SQL語句,有些學員聽到這點,就會開始去查看ORM自動產生的SQL語句,然後一一檢視,當他看到一個由ORM產生,沒有效率的SQL語句時,就會抿嘴一笑,說道:這麼沒效率,這有什麼好?
是的,我無法否認這點,所有自動產生的技術,都有其強與弱,ORM強在自動產生SQL指令,將效率維持在一定程度上(試想,不同人針對同一查詢結果,寫出同一種SQL語句的可能性,及同時寫出具效率的SQL語句可能性)。但因自動產生技術的成熟度及對該資料庫的了解度,某些地方效率低落是必然的。因此,我通常對學員提及,高效率並不是ORM標榜的優點,維持一定程度效率才是。但是,在不停止以單一View Port(觀察窗)來看其產生的東西,嘗試著以放大鏡放大其缺點前,我依然無法以"一定程度效率"的說法來說服你,ORM有什麼好。
理想與現實的差距
對SQL高手而言,ORM似乎是一條奇特的路線,他們無法認同,學習LINQ等類SQL語句,比起使用SQL來有什麼優點,畢竟現在的SQL是程式設計師必修的技術,更貼切的說,是常識!以本文提及的問題,SQL高手很輕易的就能以Stored Procedure來處理掉,這是事實!但前提是,這位SQL高手必須是case的領導者,有權力決定,以Stored Procedure來驅動資料。撇開這些不談,就ORM宣稱的跨資料庫特性,真的達得到嗎?就目前的發展而言,我只能說,ORM能加速資料庫變動時的調整腳步,要程式一行不改、又能通過客戶對效能的檢驗,這著實是個很大的問句。但我們也不能忘記,ORM的優點與缺點都來自自動產生SQL語句,這也意味著,自動產生的機制是可以被持續優化的。
來得太早?還是來得太晚?
我聽過許多人提到,ORM來得太早了,很多人根本都還沒準備要從SQL指令,過渡到這個技術上,也沒有必要做這種變革。但我認為,ORM並非來得太早,而是來得太晚,它選在所有人都了解SQL的時代出現,所以,既定印象的存在,否定了這個技術所做出的改動。
認同與採用
OK,我認同你所說的,那我應該採用ORM嗎?別誤會,我只是告訴你ORM與傳統SQL間的差異,及它為何會這麼設計的前因後果,如果真的要我建議的話,那麼請觀察你們團隊成員對本文的看法,很快的,你會得到你應該採用與不採用的決定。因為,如果團隊無法充份了解SQL的不足處、了解ORM的優點與缺點,那麼,冒然採用對專案進行必定是有害無利。
另外,你也必須了解,ORM的程式寫法與傳統SQL語法有一定的差距,學習這東西必須列入專案的時程中,否則,專案寫不完,採用那種技術就沒有意義了。
再者,ORM的效率與傳統SQL有差距,以批次更新來說,在ORM下必須一一建立物件來寫入,這與SQL指令用一行UPDATE改動所有資料列的效率有天差地壤之別,但以此來駁斥ORM之時,也請想想,這種情況有多少?另,ORM多支援Stored Procedure的對應,用這種方式來進行批次更新,是ORM的既定標準手法。