Pages

2015-05-27

提供自訂ORM物件的直覺式語法

從ADO.NET/DataTable跨入Data Entity時代之初,我選擇了微軟EF+LINQ架構,然而EF v6.1.3在對SQL字串執行的函式雖夠用但語法不夠好,因此我使用自己的Helper Class來執行SQL指令,並映射成POCO物件,如此進可攻退可守。

先來看看EF6下提供的SQL字串執行語法:

// 使用params順序參數
string sql = "INSERT INTO [t] (id,name) VALUES ({0}, {1})";
// 它會轉成@p0, @p1等Parameter避免SQL Injection
dbContext.Database.SqlQuery<POCO>(sql, 123, "abc");


// 若要使用具名DB參數,就得使用:
sql = "INSERT INTO [t] (id,name) VALUES (@id, @name)";
DbParameter[] param = new DbParameter[]
{
new DbParameter("@id", 123),
new DbParameter("@name", "abc")
};
dbContext.Database.SqlQuery<POCO>(sql, param);

其中傳入的POCO類別,必須和SQL裏SELECT出來的欄位數要完全一致才不會出錯,這要求很不合理,畢竟查詢時能共用大POCO宣告類別才會方便。雖然這些用完即丟的POCO類別有產生器可產生,但垃圾一多就不好管理。為此,我仿照Dapper元件的直覺式語法優點,使用匿名類別來產生具名DbParameter參數,並支援讀入欄位數不一致的POCO物件裏,甚至可以dynamic型別來傳回查詢資料集合。

SqlConnection con = new SqlConnection("ConnectionString");
Database db = new Database(con, true); // Try to open connection.

string sql = "SELECT * FROM [t] WHERE id = @id AND name = @name";
var param = new
{
id = 123,
name = "abc"
};

// IEnumerable<POCO>
var result1 = db.QueryEntity(sql, param);
//db.QueryEntity(cmd); // 可執行自己的DbCommand

result1 = from r in result1
where r.id > 2 && r.id < 5
select r;

foreach (var item in result1)
{
int id = item.id;
string name = item.name;
}
實作了dynamic動態類別傳入,直接得到dynamic的資料集合,省去了用完即拋的POCO類別的宣告手續。
// IEnumerable<dynamic>
var result2 = db.QueryDynamic(sql, param, buffered: false);
//db.QueryDynamic(cmd);

foreach (dynamic item in result2)
{
int id = item.id; // Dynamic free coding.
string name = item.name;
}

以上我所包裝的DB Helper類別,全是Dapper也能做到的功能,但Dapper不能使用DbCommand,而且對於底層DbParameter參數無法再修改其值,當大量執行SQL修改指令時,Dapper底層必得重新綁定DB參數,徒耗效能。所以我利用Refection實作了類似Dapper般使用Anonymous Class來宣右DbParameter參數,並增加對既有DbCommand的支援及優化。


為什麼我寧可自己實作整套的ORM Helper Class,卻不直接修改調整Dapper的開源程式碼呢?其實是個人能力有限,實在看不懂它在寫什麼東西。雖然Dapper號稱高效能,其實我們查詢取回的資料筆數都不大,使用自訂的ORM映射方式並不會導致效能太差,最重要是自己的物件自己做主,簡單又好用。

2015-05-23

LINQ裏的Single與First差別比較

剛開始接觸LINQ的人會對常用的四大函式Single, SingleOrDefault, First, 及FirstOrDefault之差別感到疑惑,下圖是比較表:

LINQ Single vs SingleOrDefault vs First vs FirstOrDefault - Technical Overload
http://www.technicaloverload.com/linq-single-vs-singleordefault-vs-first-vs-firstordefault/

Method Comparsion

當遇到查詢某ItemId資料時,一般人會統一用FirstOrDefacut()來通吃,避掉Exception的發生。然而有些程序的重點是在於查到該Entity後的連續操作,若一開始的Entity找不到是致命錯誤,應該讓它發生Exception才對,這時候就得用SingleXXX()了。

至於XXXOrDefault()主要是當該筆資料找不到時,能得到null值。詳細的用法可以參加該網址下方的使用範例,善用這類的Lambda表達式,可以比from…select語法更精簡。

2015-05-20

EF的DbConnection自動開關控制

Jon Gallant: Do I always have to call Dispose() on my DbContext objects? Nope http://blog.jongallant.com/2012/10/do-i-have-to-call-dispose-on-dbcontext.html

The default behavior of DbContext is that the underlying connection is automatically opened any time is needed and closed when it is no longer needed. E.g. when you execute a query and iterate over query results using “foreach”, the call to IEnumerable<T>.GetEnumerator() will cause the connection to be opened, and when later there are no more results available, “foreach” will take care of calling Dispose on the enumerator, which will close the connection. In a similar way, a call to DbContext.SaveChanges() will open the connection before sending changes to the database and will close it before returning.

Given this default behavior, in many real-world cases it is harmless to leave the context without disposing it and just rely on garbage collection.

這是一位來自微軟EF資深研發工程師Diego Vega的回應,他說EF下的DbConnection會隨著存取資料自動開啟DB連線,也會隨著Dispose()明確地關閉連線。雖然EF的GC資源回收機制也會呼叫其Dispose()函式來關閉DB連線,但觸發時機不一定,所以他建議明確呼叫DbContext.Dispose()來關閉DB連線是比較好的方式,這也是為何EF的Sample Code總是用using()的主因。

換句話說,若你的DB Helper Class是吃EF底下的DbConnection物件,其一開始ConnectionState是Closed關閉的,因此記得判斷一下State開啟它,並在最後呼叫DbContext.Dispose()來明確關閉DbConnection。

2015-05-13

在Google Blogger高亮顯示程式碼

為了在Google Blogger上顯示高亮美化的程式碼片段,使用SyntaxHighlighter這js套件來達到以下的效果。

HL Demo

由於文章顯示是在Google Blogger上,設定的教學圖文步驟請看此文章: 「How do I add syntax highlighting to my Blogger blog?」,原來Blogger的html header是在範本(Template)裏設定:

Blogger Template

依教學文章設定很簡單,若不想黑色背景的色調,把「shThemeEmacs.css」換成「shThemeDefault.css」即可。

2015-05-09

導入EF之初期策略

是否你仍在使用ADO.NET+DataTable方式存取資料庫,心裏卻很想導入EF (Entity Framework v6.x)架構,但在看了不少LINQ及EF的範例碼後,覺得語法雖簡鍊,但對其細節掌握度不好呢?其實你的直覺是對的,Entity有它的好處,但要完全取代SQL,現階段是做不到的。

以下圖為例,你若走一般書上教的編號1路線,若你對C# Lambda及LINQ相關物件集合不是很了解的話,很容易陷入效能危機。新手程式員為了求結果,一直用Fluent Syntax一直串接集合,當資料量一大,效能就變極差。如同一位好的程式員在讀取UI樹狀結構資料時,他懂得怎麼下簡單的SQL取回最少資料量,再於Client端用程式作結構上的映射,而不是直接在DB裏下一大串子查詢及遞迴SQL的過程。

EF to Entity

LINQ在SQL Expression的自動轉換上,無法作到像傳統SQL那麼精確,例如你要把某日期欄位在更新資料時寫入DB主機時間,EF是沒辦法直接用GETDATE()的,就算真要做到這功能,也是至DB端取得時間變數再查詢寫入的,雖然能達到目的,但這種耗損效能的行為不可取。很多人接觸EF之初,就以為可以不用再寫SQL指令,其實EF的精神在於Entity資料集合上,而非100%要用LINQ取代SQL。

至目前EF v6.x版,DataTable是可以完全被取代掉的,使用IEnumerable<T>來代替,這在程式撰寫上會得到很多好處。但LINQ to SQL這部分的語法轉換無法完全取代,所以還是得用DbContext.Database.ExecuteSqlCommand()來執行傳統SQL字串。這部分和傳統ADO.NET+DbParameter使用方式一樣,其實這部分可以直接使用自己寫的DbHelper來讀入EF下的DbConnection來實現。主要原因是EF目前執行SQL指令的函式,其傳回的Entity類別必須與Select欄位數完全一致,也無法直接傳回dynamic集合,在使用上的語法不夠直覺、簡單。

SQL Profiler

我的導入方式是,在完全理解LINQ及Lambda函式設計之後,確認它在底層的實作沒包藏太多耗損效能垃圾,然後去了解IEnumerable<T>集合,以Entity的理念重新改寫我的DbHelper物件。內容包括實作資料庫ORM讀取及匿名類別、映射Reflection、動態dynamic等新的方式,讓我的DbHelper可以用Entity的概念去讀取接收資料,並產生與EF同結果的集合資料。當用新思維走過一圈後再回頭看EF的架構,更能透視它函式實作原理,達到去繁入簡的學習。

如上圖編號3,當遇到LINQ無法完全轉成我預期的SQL指令時,我就會切入自己的DbHelper產生預期的資料結果。我不是迴避LINQ,只是因為更了解它的限制及效能缺點(SQL Profiler一直開著),使用更直覺、更高效能的方式去解決問題而己。

2015-05-06

使用PE工具快速安裝Win7

若使用傳統方式,我們得先將Win7安裝ISO檔製成開機USB碟,再使用它一步一步傳統安裝,而這個新方式是讓你可以在一個U碟裏存放各種Windows系統的ISO檔,快速安裝而避開了傳統安裝步驟畫面。請使用「通用PE工具箱V5.5(無廣告修改版)」USB碟開機進入PE系統(你也可以使用VM虛擬系統+ PE ISO檔模擬、學習)。

  1. 使用「DiskGenius分割神器或PartAssist分區助手」工具,將目標磁區先建立好,記得作4K對齊增加磁碟效能,並設定可開機Active及格式化為NTFS。
    Create Partition
  2. 使用UltraISO建立虛擬光碟,並把Win7安裝ISO檔載入。
    Virtual Image 
  3. 開啟「WinNTSetup安裝器」工具,編號1選定該虛擬光碟下的/sources/install.wim,編號2-3選定目標安裝磁區,開始安裝。若遇到「0x91錯誤: 目錄不是空的」,代表要先把目標磁區格式化清空才行。
    WinNTSetup
  4. 寫入wim映象文件後,拔除U碟,重新使用該C槽開機,它會啟動Windows 7介面,選擇要登入的的User帳號密碼,就直接能進入開始使用Win7系統了。上個步驟寫入映像檔文件,就是完全取代傳統的安裝進度畫面,因此速度超快。
  5. 同理可推,只要你的U碟存有Win2000/XP/2003/Vista/Win7/8/10/2008/2012等ISO檔,就可以利用此方法快速安裝。