7/9/2010 9:44:52 PM

[RUN! PC] 2010 七月號 - 結合檔案及資料庫的交易處理

Microsoft.NET | RUNPC | Transactional NTFS | 技術隨筆 | 我的作品 | 物件導向 | 作業系統

 

IMG_3972

再次感謝編輯大人 :D,Transactional NTFS  #2 也刊出來了!

這篇是延續上一篇,進一步的介紹 TxF  如何與 TransactionScope 互動,讓你可以結合檔案系統及資料庫的異動,變成單一交易的技巧。

由於TxF還沒有正式在.NET Framework裡支援,所以這篇最後也介紹了 AlphaFS,可以簡化應用時的障礙。AlphaFS 是一套想要取代 System.IO.* 的類別庫,它支援了許多 NTFS 的進階功能 (像是 VSS、HardLink 等) 的功能,而 TxF 也在範圍內,透過它就不用再像 上一篇 一樣,辛苦的用 P/Invoke 了。

 

這篇提到的範例程式可以在這裡下載。有任何意見也歡迎在這裡留話給我 :D



5/5/2010 1:51:00 AM

[RUN! PC] 2010 五月號 - TxF讓檔案系統也能達到交易控制

Microsoft.NET | RUNPC | Transactional NTFS | 技術隨筆 | 我的作品 | 物件導向 | 作業系統

 

image 

五月號就刊出來,還真有點意外 :D,這次稿件趕不及,晚了幾天才交出去,編輯大人還是讓我上五月號啦,真是感謝 :D

之前執行緒的系列,是打算把各種應用執行緒的演算法都介紹一下,寫了五篇就沒靈感了。實際寫CODE的技巧倒是很多可以介紹,不過 .NET FX 4.0 出來之後,這些鎖碎的 coding 技巧又大幅簡化了,除非有特別的演算法需要 (如同之前那五篇 :D),否則還自己拿 Thread 物件硬幹已經沒什麼意義了,所以就換另一個我有興趣的主題 - Transactional NTFS 來寫。

這系列第一篇出爐了,主要就是先介紹它的觀念及如何入門,國內這類資訊還不多,我就野人獻曝寫了一篇,試著寫看看了。較鎖碎的實作技巧 (如 P/Invoke) 我會直接貼 BLOG,而較完整的概念及實作探討等等就會整理成文章拿來投稿了。

再次感謝各位支持啦,底下有範例程式跟一些參考資源的 LINK,需要的歡迎取用 :D

 

範例程式:

  1. Visual Studio 2008 Project (C#) File: TransactionDemo.zip

 

參考資訊:

  1. AlphaFS: Brining Advanced Windows FileSystem Support to .NET
    http://alphafs.codeplex.com/
  2. MSDN magazine (July 2007): Enhance Your Apps With File System Transactions
    http://msdn.microsoft.com/en-us/magazine/cc163388.aspx
  3. B# .NET BLOG: Windows Vista - Introducing TxF In C#
    Part 1: Transacted File Delete
    Part 2: Using System.Transactions and the DTC
    Part 3: CreateFileTransacted Demo
  4. Code Project: Windows Vista TxF / TxR
    http://www.codeproject.com/KB/vista/KTM.aspx
  5. BLOG: Because we can
    http://blogs.msdn.com/because_we_can/archive/2005/05/18/419809.aspx
    Discussion and explanation relating to the Transactional NTFS feature coming in Longhorn, plus any other interesting anecdotes...
  6. Performance Consoderations for Transactional NTFS
    http://msdn.microsoft.com/en-us/library/ee240893(VS.85).aspx
  7. When to Use Transactional NTFS
    http://msdn.microsoft.com/en-us/library/aa365738(VS.85).aspx


4/5/2010 3:31:00 AM

[RUN! PC] 2010 四月號 - 生產者vs消費者– 執行緒的供需問題

Microsoft.NET | RUNPC | Threading | 技術隨筆 | 我的作品 | 物件導向

image 

 

隔了足足一年,趁著過年,生出第五篇了 :D,再次感謝編輯賞光啦,願意刊登我寫的題材...

這篇 [生產者/消費者] 其實是延續上一篇 [生產線模式] 而來的,生產線模式,是利用 PIPE 的方式,把工作切成各個階段同時進行。而生產者消費者,則是去探討兩個階段之間如何做好進度的協調。這篇除了講講處理的原則之外,也實作了BlockQueue來簡化這個問題。

其實更漂亮的用法,是我在 MSDN Magazine 上看到的 BlockingStream, 直接把它作成 System.IO.Stream 的衍生類別。像那些包裝成 Stream 的資料處理 (像是壓縮、加密、或是 Socket 這類),都很適合。不過一般情況下不是所有應用都能套上 Stream 的模式,因此就有了把它包裝成 Queue 的念頭,也就是這篇寫的 BlockQueue 的應用。

相關的東西其實過去也寫了幾篇,只不過 BLOG 寫的都比較瑣碎,都是針對特定主題較深入的討論,而投稿的文章就會比較完整的介紹原理及作法等等,細節就沒辦法兼顧了。有興趣的讀者可以參考一下我部落格的相關文章 :D   這期的範例程式可以在底下下載,相關的 LINK 也列在底下。

再次謝謝各位支持啦 :D


MSDN Magazine 閱讀心得: Stream Pipeline
RUN!PC 精選文章 - 生產線模式的多執行緒應用
[RUN! PC] 2008 十一月號 生產線模式的多執行緒應用
生產者 vs 消費者 - BlockQueue 實作



3/23/2010 2:16:52 AM

[TxF] #2. 先作功課 - 熟悉 P/Invoke 及 Win32 檔案處理...

C# | Microsoft.NET | MSDN | TxF | P/Invoke

 

其實這篇是多寫的,因為前一篇提到的 Transactional NTFS 官方只提供 Win32 API 而已,不提供包裝好的 managed code 用的 library... 因此現階段想始用它,P/Invoke 是逃不掉的... 這篇就先來複習一下,想要在 C# 裡呼叫 unmanaged code 該怎麼用吧。這邊的例子為了配合後面幾篇,就同樣的以檔案處理為例。

這篇我不想去長篇大論的討論 P/Invoke 那堆規則及語法,也不想去討論那堆 Marshal 的觀念等等... 想學好 P/Invoke 就別看我這篇了,應該去 MSDN 看... 這篇我只想交待一下該如何配合 windows api 來作檔案處理而已。因為這些是往後要用到 Transactional NTFS 必要的技巧,TxF 新的 API 都是跟 win32 標準檔案處理的 API 一一對應的,弄懂了如何用 win32 api 操作檔案,你大概就學會八成的 TxF 了... 想用 TxF ...  熟悉點 P/Invoke 是應該的...

 

先從最單純的 MoveFile 開始吧,它沒有扯到啥 pointer 或是 handle ... 算是最單純的例子.. 先來看看這 API 原生的樣子:

BOOL WINAPI MoveFile( __in LPCTSTR lpExistingFileName, __in LPCTSTR lpNewFileName );

 

 

這 API 做啥事就不用多說了,把 lpExistingFileName 這檔案搬到 lpNewFileName 去... 搬成功還是失敗,就用 BOOL 值把結果傳回來。這時就要透過 P/Invoke 的用法,想辦法產生一個可以對應到 unmanaged code 的 C# function ... 我先把宣告方式貼出來,再來解試 code ...

P/Invoke: MoveFile[copy code]
   1:  [DllImport("kernel32.dll")]
   2:  static extern bool MoveFile(string lpExistingFileName, string lpNewFileName);

 

寫過 C / C++ 的人,大概對 extern 這個 keyword 不陌生吧? extern 這修飾字代表這個 function 是外來的,C# 正好拿來用在這裡。外來的 DLL 都是 function 型態,所以一定是 static, 沒有綁著任何一個物件... 而標在上面的 Attribute: DllImport, 則是標上這個 function 是來自那個 DLL。

在這個例子裡,這樣就足夠了,直接在程式裡試著寫一段 C# (managed code) 來試看看結果吧:

P/Invoke Sample #1. MoveFile[copy code]
   1:  public class PInvokeTest
   2:  {
   3:      [DllImport("kernel32.dll")]
   4:      static extern bool MoveFile(string lpExistingFileName, string lpNewFileName);
   5:      public static void Main(string[] args)
   6:      {
   7:          string srcFileName = @"C:\file1.txt";
   8:          string dstFileName = @"C:\file2.txt";
   9:          Console.Write("move file: from [{0}] to [{1}] ... ", srcFileName, dstFileName);
  10:          if (MoveFile(srcFileName, dstFileName) == true)
  11:          {
  12:              Console.WriteLine("OK!");
  13:          }
  14:          else
  15:          {
  16:              Console.WriteLine("FAIL!");
  17:          }
  18:      }
  19:  }

 

程式執行前,看一下 C:\ 的 DIR *.TXT 指令執行結果:

image

沒錯,有 c:\file1.txt 這檔案... 接著來執行範例程式:

image

執行成功。再重新看一下 C:\ 的 DIR *.TXT 指令執行結果:

image

看來程式很順利的呼叫了 win32 api 裡定義的 MoveFile( ... ) ... 這種範例有點不入流,要處理檔案總不可能只有這樣吧? 接著我們再來看看需要 Open File 加上讀寫檔案內容的應用。

Windows 是個以 HANDLE 為主的作業系統,一般在寫 C / C++ 程式,都是以指標(POINTER)的方式在處理資料,但是在 windows 裡,作業系統提供的資料用的指標,則特別以 "HANDLE" 來稱呼,它比一般的 POINTER 來說多了一些管理的動作。因此你開啟的檔案,或是建立的視窗,通通都以 HANDLE 來稱呼,而不是單純的用 POINTER (雖然它也是個 POINTER 啦)。接下來的例子就來看看 HANDLE 的應用。

[copy code]
   1:  public class PInvokeTest2
   2:  {
   3:      [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
   4:      public static extern IntPtr CreateFile(
   5:             string lpFileName,
   6:             uint dwDesiredAccess,
   7:             uint dwShareMode,
   8:             IntPtr SecurityAttributes,
   9:             uint dwCreationDisposition,
  10:             uint dwFlagsAndAttributes,
  11:             IntPtr hTemplateFile
  12:             );
  13:      [DllImport("kernel32.dll", SetLastError = true)]
  14:      static extern bool CloseHandle(IntPtr hObject);
  15:      public static void Main(string[] args)
  16:      {
  17:          IntPtr pFile = CreateFile(
  18:              @"c:\file1.txt",
  19:              0x80000000,
  20:              0x00000001,
  21:              IntPtr.Zero,
  22:              3,
  23:              0,
  24:              IntPtr.Zero);
  25:          Stream fs = new FileStream(pFile, FileAccess.Read);
  26:          TextReader tr = new StreamReader(fs);
  27:          Console.WriteLine(tr.ReadToEnd());
  28:          tr.Close();
  29:          fs.Close();
  30:          CloseHandle(pFile);
  31:      }
  32:  }

 

這個例子裡,很 "神奇" 的把 unmanaged code 拿到的指標 (IntPtr), 丟給 System.IO.* 底下的 FileStream 來用,竟然還能成功的開啟檔案並且把資料讀出來... 沒錯,MS寫的東西本來就都是同一家人,System.IO 那堆東西就是這樣包出來的。 這段範例程式除了看起來複雜一點之外,跟那堆要查文件才知道是啥意思的 flags 之外,其它都很簡單,不過就 Open File, 然後讀出內容,接著 Close ...

較特別的是,在 P/Invoke 的世界裡,都用 struct System.IntPtr 這個型別,來代表 unmanaged 世界裡常用到的 POINTER.. 藉著這個型別,我們就可以把兩個世界的橋樑給搭起來。不過當你執行這個範例時,編譯器會給你一個很礙眼的警告:

 

'System.IO.FileStream.FileStream(System.IntPtr, System.IO.FileAccess)' is obsolete: 'This constructor has been deprecated.  Please use new FileStream(SafeFileHandle handle, FileAccess access) instead.  http://go.microsoft.com/fwlink/?linkid=14202'  

 

沒錯,這是老用法了,一如用指標有數不盡的缺點,新的語言都想盡辦法扔掉它 (POINTER: 我是無辜的...),連包裝過的 IntPtr 也不例外。在 .NET Framework 2.0 裡就不再建議你用這個建構式了,請改用 SafeFileHandle .... 既然在 windows 的世界裡,一切系統的資源都是以 HANDLE 來處理,自然的在 managed code 裡也有對應的型別,它就是 System.Runtime.InteropServices.SafeHandle 。接著再來看看改用 SafeHandle 的版本:

[copy code]
   1:  public class PInvokeTest3
   2:  {
   3:      [DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
   4:      static extern SafeFileHandle CreateFile(
   5:          string fileName,
   6:          uint fileAccess,
   7:          uint fileShare,
   8:          IntPtr securityAttributes,
   9:          uint creationDisposition,
  10:          uint flags,
  11:          IntPtr template);
  12:      public static void Main(string[] args)
  13:      {
  14:          SafeFileHandle pFile = CreateFile(
  15:              @"c:\file1.txt",
  16:              0x80000000,
  17:              0x00000001,
  18:              IntPtr.Zero,
  19:              3,
  20:              0,
  21:              IntPtr.Zero);
  22:          Stream fs = new FileStream(pFile, FileAccess.Read);
  23:          TextReader tr = new StreamReader(fs);
  24:          Console.WriteLine(tr.ReadToEnd());
  25:          tr.Close();
  26:          fs.Close();
  27:          pFile.Close();
  28:      }
  29:  }

 

執行的結果,當然也是順利的把文字檔內容印到 CONSOLE 裡了,我就不再多貼,直接看程式碼。

其實這裡有點偷吃步,用的是 SafeFileHandle, 而不是 SafeHandle。雖然兩者有繼承關係啦。不過後面我就都把它當成 SafeHandle 看待。這個版本的程式,除了把 IntPtr 換成 SafeFileHandle 之外,及最後的 CloseHandle( pFile ) 改成直接呼叫 SafeHandle.Close( ) 之外,沒有太大的不同了。有啦,把 IntPtr 跟 CloseHandle 兩者包裝在同一個 SafeHandler 裡是安全的多,至少 SafeHandle 實作了 IDispose 的介面,在適當的情況下,它至少會自動的被呼叫 Dispose( ) 回收資源...

 

看到這裡,以前老搞不清楚的 FileStream 為什麼有幾個怪怪的 constructor, 自從研究了 TxF 之後,意外的晃然大悟... 算是額外的收穫吧! 原來就是要配合 P/Invoke 使用...

其實說穿了,呼叫 win32 api 大概就這幾招了,幾種 unmanaged code 裡用到的型別,指標都可以對應之後,程式寫起來就簡單了。這篇我特意漏掉一部份,就是各種用 uint 來當作 flags 的型別,沒有轉到對應的 enum 列舉型別,我是覺的這是 option 啦,畢竟查查文件就有,多列上來我得多打好多字... 篇幅有限 XD

這幾個例子如果都看懂後,那麼 TxF 就沒有障礙了! 接下來就看下回分解了 :D    下回會看到第一個 "交易式" 的檔案處理範例! 沒灌 windows vista / 2008 / 7 / 2008 R2 的讀者門,快去準備吧!

 

參考資訊:

  1. 有用的網站: PINVOKE.NET
    http://www.pinvoke.net/index.aspx

    這網站幫你整理好了各種 win32 api 該如何宣告它的 C# signature, 以供 p/Invoke 使用。如果你不熟它的語法,這網站可以幫你不少忙。另外它也提供了一些方便的工具,像是 winform 的查詢工具,或是 visual studio 用的 add-ins
  2. MSDN 的 API 說明: MoveFile
    http://msdn.microsoft.com/en-us/library/aa365239(VS.85).aspx
  3. 什麼是 P/Invoke ? Wiki 的說明:
    http://en.wikipedia.org/wiki/Pinvoke
  4. Safe Handle & Critical Finalization
    (實在是看不懂的中譯... "安全控制代碼和關鍵結束" ???)
    http://msdn.microsoft.com/zh-tw/library/fh21e17c(v=VS.90).aspx


3/18/2010 12:06:00 AM

[TxF] #1. 初探 Transactional NTFS

C# | Microsoft.NET | MSDN | SQL | 作業系統 | TxF | Transactional NTFS

其實想用 TxF (Transactional NTFS, 交易式的 NTFS) 已經很久了,不過老是被一些雜事卡著,到過年期間才有空好好研究一下。這篇主要是介紹而已,就不講太多 Code, 先以瞭解 TxF 是什麼,及如何善用它等等方面為主。詳細的用法,就等後面幾篇吧!

Transactional NTFS, 中文是 "交易式NTFS",或是常見到的縮寫 "TxF”,早期的相關文章也有人寫 "TxFS”。這是在 Windows Vista / 2008 推出時,首次正式提供的功能。雖然它叫作 Transactional “NTFS”, 實際上它並不是一個新的檔案系統,而是一組新的 API (跟原有的檔案處理 API 幾乎是一對一的對應),  支援你用交易的方式操作檔案。一起推出的還有 Transactional Registry (TxR),一樣是有對應的 windows API,只不過它處理的對像是 windows registry,不是檔案...。

用這種方式處理檔案的讀寫動作,有種很神奇的感覺,過去都只在資料庫裡有機會這樣用,現在檔案的處理也可以了。配合像是 DTC 這類交易協調器的支援,甚至可以把檔案的處理及資料庫的處理,通通都包裝成一個交易來進行,一但任何一個環節失敗,都可以回復到最初的狀態,感覺好像是在用 DB,而不是寫檔案... 目前官方並沒有推出 managed code 的含式庫,現在要用只有幾種選擇:

  1. 直接用 C / C++ 呼叫 win32 api
  2. 用 P/Invoke,在 C# 裡呼叫 win32 api
  3. 找那些別人 (非官方) 包裝好的 .net class library ..

這些用起來都有點不踏實,畢竟用 P/Invoke 不是長久之計,總覺的遲早會被替換掉。不過即使如此,這項還是掩蓋不了這技術的價值。我貼一段自己寫的 sample code,讓還沒用過的人體會一下,寫檔案還支援交易處理的 "爽度" ...

 

[copy code]
   1:  // 建立 KTM transaction object
   2:  IntPtr transaction = CreateTransaction(
   3:      IntPtr.Zero,
   4:      IntPtr.Zero,
   5:      0, 0, 0, 0,
   6:      null);
   7:  string[] files = new string[] {
   8:      @"c:\file1.txt",
   9:      @"c:\file2.txt",
  10:      @"c:\file3.txt"};
  11:  try
  12:  {
  13:      foreach (string file in files)
  14:      {
  15:          // 使用支援交易的 delete file API
  16:          if (DeleteFileTransactedW(file, transaction) == false)
  17:          {
  18:              // 刪除失敗
  19:              throw new InvalidOperationException();
  20:          }
  21:      }
  22:      // 認可交易
  23:      CommitTransaction(transaction);
  24:  }
  25:  catch (Exception ex)
  26:  {
  27:      // 還原交易
  28:      RollbackTransaction(transaction);
  29:  }
  30:  CloseHandle(transaction);

 

範例裡用到的幾個 method, 像是 CreateTransaction( ), DeleteFileTransactedW( ), CommitTransaction( ), RollbackTransaction( ) ... 等等,都是透過 P/Invoke 的方式呼叫的 win32 api... 除了用的型別不如 pure .net class library 般直覺之外,這樣的 code 也已經很簡單了,短短卅行就可以搞定...

雖然這樣的 code 實在不大合我胃口,但是它畢竟是個堪用的方案... 對於 code 有潔癖的,可以考慮其它的用法。前面是最基本的 API call,如果你不滿意,MS自家的技術 DTC (Distributed Transaction Coordinator) 當然也支援 TxF。DTC 可以提供額外的好處,就是允許你做分散式的交易管理。意思是你配合 DTC,就可以把 Local File I/O 跟 database access 整合在同一個交易範圍內。

這邊的 sample code 我就不貼了,在 managed code 裡去呼叫到 COM 的那堆介面 (啥 QueryInterface 的) 實在跟 .NET programming 的 style 有點格格不入... 在 C# 的世界裡,應該用 TransactionScope 才對。在 MS 的世界裡,TxF + TxR + DB 都可以是 TransactionScope 內的一部份。這部份的 Sample Code 我一樣先不貼了,不然貼一堆 code 又沒篇幅說明,感覺很混...

其實,MS 該做的都做了,唯一缺的就是它竟然沒正式的併入 .NET Framework 內的一員... 如果 TxF 真的是你想用的東西,倒是有個 OpenSource Project 可以考慮一下: AlphaFS, 它的目標是能替換掉 namespace System.IO.*, 所以很多你常用的 class library, 它都有對等一樣用法的版本,當然它提供了更多的功能及改善... 其中 TxF 的支援就在內,你想用 TxF 來開發軟體的話,這是個不錯的選擇...

總之,這篇只是個開始,目的是想先 "預覽" 一下 TxF 的能耐,及未來它配 DTC / TransactionScope 後,能怎麼應用它的方式,還有其它可用的相關資源。接下來我會陸續整理一些相關的研究心得.. (別太期待,大概一兩週生一篇就很偷笑了 XD),下回見 !

 

 

參考資訊:

  1. AlphaFS: Brining Advanced Windows FileSystem Support to .NET
    http://alphafs.codeplex.com/
  2. MSDN magazine (July 2007): Enhance Your Apps With File System Transactions
    http://msdn.microsoft.com/en-us/magazine/cc163388.aspx
  3. B# .NET BLOG: Windows Vista - Introducing TxF In C#
    Part 1: Transacted File Delete
    Part 2: Using System.Transactions and the DTC
    Part 3: CreateFileTransacted Demo
  4. Code Project: Windows Vista TxF / TxR
    http://www.codeproject.com/KB/vista/KTM.aspx
  5. BLOG: Because we can
    http://blogs.msdn.com/because_we_can/archive/2005/05/18/419809.aspx
    Discussion and explanation relating to the Transactional NTFS feature coming in Longhorn, plus any other interesting anecdotes...
  6. Performance Consoderations for Transactional NTFS
    http://msdn.microsoft.com/en-us/library/ee240893(VS.85).aspx
  7. When to Use Transactional NTFS
    http://msdn.microsoft.com/en-us/library/aa365738(VS.85).aspx


12/19/2009 11:47:05 PM

[設計案例] 清除Cache物件 #2. Create Custom CacheDependency

543 | C# | CS | Microsoft.NET | MSDN | 小技巧 | 技術隨筆 | 物件導向

上一篇廢話了這麼多,其實重點只有一個,我這次打算利用 CacheDependency 的機制,只要一聲令下,我想移除的 cache item 就會因為 CacheDependency 的關係自動失效,而不用很辛苦的拿著 cache key 一個一個移除。

我的想法是用 tags 的概念,建立起一套靠某個 tag 就能對應到一組 cache item,然後將它移除。開始之前先來想像一下 code 寫好長什麼樣子:

透過 tags 來控制 cache items 的範例程式[copy code]
   1:      static void Main(string[] args)
   2:      {
   3:          string[] urls = new string[] {
   4:              "http://columns.chicken-house.net/",
   5:              // 共 50 組網址... 略
   6:          };
   7:          foreach (string url in urls)
   8:          {
   9:              DownloadData(new Uri(url));
  10:          }
  11:          Console.ReadLine();
  12:          TaggingCacheDependency.DependencyDispose("funp.com");
  13:          Console.ReadLine();
  14:      }
  15:      private static void Info(string key, object value, CacheItemRemovedReason reason)
  16:      {
  17:              Console.WriteLine("Remove: {0}", key);
  18:      }
  19:      private static byte[] DownloadData(Uri sourceURL)
  20:      {
  21:          byte[] buffer = (byte[])HttpRuntime.Cache[sourceURL.ToString()];
  22:          if (buffer == null)
  23:          {
  24:              // 直接到指定網址下載。略...
  25:              buffer = null;
  26:              HttpRuntime.Cache.Add(
  27:                  sourceURL.ToString(),
  28:                  buffer,
  29:                  new TaggingCacheDependency(sourceURL.Host, sourceURL.Scheme),
  30:                  Cache.NoAbsoluteExpiration,
  31:                  TimeSpan.FromSeconds(600),
  32:                  CacheItemPriority.NotRemovable,
  33:                  Info);
  34:          }
  35:          return buffer;
  36:      }
  37:  }

 

 

這段 sample code 做的事很簡單,程式準備了 50 個網址清單,用 for-loop 一個一個下載。下載的 method: DownloadData(Uri sourceURL) 會先檢查 cache 是否已經有資料,沒有才真正下載 (不過下載的細節不是本篇要講的,所以就直接略過了...)。

而主程式的最後一行,則是想要把指定網站 ( funp.com ) 下載的所有資料,都從 cache 移除。為了方便觀看程式結果,我特地加上了 callback method, 當 cache item 被移除時, 會在畫面顯示資訊:

image

由執行結果來看,果然被移出 cache 的都是來在 funp.com 的網址... 接著來看看程式碼中出現的 TaggingCacheDependecny 是怎麼實作的。相關的 code 如下:

TaggingCacheDependency 的實作[copy code]
   1:  public class TaggingCacheDependency : CacheDependency
   2:  {
   3:      private static Dictionary<string, List<TaggingCacheDependency>> _lists = new Dictionary<string, List<TaggingCacheDependency>>();
   4:      public TaggingCacheDependency(params string[] tags)
   5:      {
   6:          foreach (string tag in tags)
   7:          {
   8:              if (_lists.ContainsKey(tag) == false)
   9:              {
  10:                  _lists.Add(tag, new List<TaggingCacheDependency>());
  11:              }
  12:              _lists[tag].Add(this);
  13:          }
  14:          this.SetUtcLastModified(DateTime.MinValue);
  15:          this.FinishInit();
  16:      }
  17:      public static void DependencyDispose(string tag)
  18:      {
  19:          if (_lists.ContainsKey(tag) == true)
  20:          {
  21:              foreach (TaggingCacheDependency tcd in _lists[tag])
  22:              {
  23:                  tcd.NotifyDependencyChanged(null, EventArgs.Empty);
  24:              }
  25:              _lists[tag].Clear();
  26:              _lists.Remove(tag);
  27:          }
  28:      }
  29:  }

 

30行不到... 其實程式很簡單,TaggingCacheDependency 繼承自 CacheDependency, 額外宣告一個靜態的 Dictionary<string, List<TaggingCacheDependency>> 來處理各個標簽及 TaggingCacheDependency 的關係,剩下的就沒什麼了。呼叫 DependencyDispose( ) 就可以通知 .NET Cache 機制,將相關的 cache item 移除。

用法很簡單,當你要把任何物件放進 cache 時,只要用 TaggingCacheDependency 物件來標示它的 tag:

把物件加進 Cache, 配上 TaggingCacheDependency ...[copy code]
   1:  HttpRuntime.Cache.Add(
   2:      sourceURL.ToString(),
   3:      buffer,
   4:      new TaggingCacheDependency(sourceURL.Host, sourceURL.Scheme),
   5:      Cache.NoAbsoluteExpiration,
   6:      TimeSpan.FromSeconds(600),
   7:      CacheItemPriority.NotRemovable,
   8:      Info);

在這個例子裡 (line 4), 直接在 TaggingCacheDependency 物件的 constructor 上直接標上 tags, 在此例是直接把網址的 hostname, scheme 兩個部份當作 tag, 未來就可以依照這兩種資訊直接讓 cache 裡的相關物件失效。

而要下令讓 Cache 內有標上某個 tag 的 cache item 失效,只要這行:

 

將標為 "funp.com" 的 cache item 設為失效的 cache item[copy code]
   1:  TaggingCacheDependency.DependencyDispose("funp.com");

 

結果就會如同上面的程式範例一樣,還留在 cache 的該網址下載資料,在這一瞬間通通都會被清掉...

 

用這種方式,是不是比拿到 key 再去呼叫 Cache.Remove( key ) 的方式簡單多了呢? 同時也能夠更快速的處理複雜的移除機制。其實運用 tagging 的方式只是一例,需要的話你也可以設計合適的 CacheDependency 類別。

以下是本篇文章的兩個附加參考檔案:

Download File - URL清單



12/19/2009 4:29:29 AM

[設計案例] 清除Cache物件 #1. 問題與作法

ASP.NET | C# | CS | Microsoft.NET | MSDN | 小技巧 | 技術隨筆 | 物件導向

每次心裡有什麼好點子想寫出來時,第一關就卡在想不出個好標題... 想來想去的標題,怎麼看就是既不顯眼又不聳動... 果然是個老實的工程師性格 =_= ...  這次要講的,是 .NET HttpRuntime 裡提供的 Cache 物件的操作心得。這個東西我想不用我多作介紹,大家都用到爛掉了吧? 不過好用歸好用,有個老問題其實一直困擾著我很久了...

" 我該怎麼手動的把某個物件從 cache 裡移除? "

老實說,這問題蠻沒水準的... 老叫別人要翻 MSDN,我自己怎麼沒翻? 不不... 容我花點篇幅先說明一下問題。Cache物件,是個典型的 Dictionary 型態的應用 (雖然它沒有 implement interface: IDictionary… ), 透過 key 就可以拿到 cached item. 要從 cache 裡移除某個 item, 簡單的很,只要用 Remove 這個 method, 一行就搞定了:

從 key 移除指定的 cache item[copy code]
   1:  HttpRuntime.Cache.Remove(“cache-key”);

別小看這一行,實作起來障礙還不少。首先,你得額外去記著 cache key 的值。當你要移除的 cache item 有多個的時後,或是移除的 items 之間的關係有點複雜時,這些 code 就不怎麼漂亮了。下一個問題是:

" 我該如何得知所有存在 Cache 內的 keys 有那些? "

這個問題單純的多,那些把 intelligent sense 當購物網站的人 (平常不看文件,只會按下 . 然後挑個順眼 method 來用的人),可能這次就碰壁了... Cache 物件不像一般的 Dictionary 一樣,有提供 Keys 這樣的 property ... 它藏在 GetEnumerator 這 method 內,它會把所有的 keys 給巡一遍,你需要所有的 keys 的話,可以這樣用:

跑過 cache 裡每一個 key[copy code]
   1:  foreach (string key in HttpRuntime.Cache) { 
   2:      // … 
   3:  }

不過這樣的風險也是蠻高的,誰曉得你拿到 key 後的下一秒,這個 cache item 還在不在 cache 內?

 

 

 

--------------------------------------------------------------

本文正式開始! 哈哈,前面那一段只是廢話 + 碎碎唸,現在才是正題。前面想表達的只是,因為 cache 的不確定性 (資料隨時都會被 remove), 操作起來變的要格外小心, 即使它用起來像一般的 Dictionary 一樣。

我舉個案例,來說明我應用 cache 的情況。假如我想實作一個簡單的 web browser, 透過網路下載資源是很慢的動作,每種 browser 都會有某種程度的 cache 機制。我們就拿 Cache 物件替代 IE 的 "temporary internet files” 目錄吧。這時很簡單,只要用 URL 當作 KEY,下載的 content 就當物件塞進去就好...

不過事情沒那麼簡單。如果程式運作了一陣子,我想提供使用者手動清除 "部份" cache 的功能的話,那該怎麼辦? 我舉幾種情況:

  1. 從 cache 裡刪除所有從某個特定網站 (如: columns.chicken-house.net) 下載的資料
  2. 從 cache 裡刪除所有特定類型的資料 (如: content-type 為 image/jpeg 的圖檔)
  3. 從 cache 裡刪除所有透過特定 protocol (如: https) 下載的資料

這樣的要求應該不算過份吧? 用前面提到的兩種作法,你會想哭吧 XD .. 用這些基礎,你大概只能選這幾種作法 (各位網友有好作法也記得提供一下):

  1. 自己另外管理所有下載過的 URL, 用盡各種適合的資料結構,讓你可以順利的挑出這些 match 的 key, 然後移除它。

    缺點: 都作這麼多,你乾脆自己重寫個 cache 機制好了... 何況時間一久,你管理的 key, 那些對應的資料搞不好老早就通通從 cache 裡清掉了...
  2. 聰明一點,用 regular expression … 從 GetEnumerator( ) 一筆一筆過濾出要移除的 URL, 然後清掉它...

    缺點: 這作法只會檢查還留在 cache 內的 URL,不過這樣的 cache 隨便也有成千上萬個,每次都要 looping 掃一次實在不怎麼好看... 有違處女座有潔癖的個性...

 

這些方法 code 寫起來實在不怎麼漂亮,我就不寫 sample code 了,請各位自行想像一下寫起來的樣子。抱歉,如果你用的正好是上面的作法... 那請多包含... :D   這些都是 workable 的作法,但是看起來就是沒什麼設計感;程式可以動,不過就效能、簡潔、可讀性、美感來看,就是覺的不夠精緻 @@。跟朋友討論到這個問題時,我想到一個爛主意...

" 用蠢方法,這些 cache item 先分好類,每一類去關聯一個檔案,設 CacheDependency … 要清掉時去 touch 一下這個檔案,一整組的物件就會自動被清出 cache 了…。”

老實說,我覺的這是個既聰明又愚蠢的作法。聰明的是它很漂亮的解決我要如何移除某一群 item 的問題...,愚蠢的是這種單純程式內可以解決的事,竟然要繞到外面不必要的 file system I/O 動作... 而這通常是最慢的...

 

--

咳,寫太晚,實際的程式碼明天待續...



9/24/2009 2:42:00 AM

[設計案例] 生命遊戲 #5, 中場休息

543 | CS | Microsoft.NET | 技術隨筆 | 物件導向

在繼續下去之前,先來講一下,我希望讓這個 "生命遊戲" 程式,發展到什麼程度吧。其實前面四篇都還只是基礎入門,跟準備動作而已,接下來才開始會有些有趣的。

我希望這系列文章寫完後,這個程式要能扮演一個真正可運作的 Matrix … 沒錯,就是像電影駭客任務裡的母體一樣,這個程式會變成 Matrix 的主要架構,而各式各樣的 "生物" 可以在這個虛擬世界裡生活。為了讓它真的跑的動,所以前幾篇提到的效能問題,執行緒問題,就不能不考慮。為了讓這個虛擬世界能更擬真一點,它至少要是個依時間驅動的模式,而不是像 "生命遊戲" 最早定義的回合制,因此這個問題也要在基本架構裡解決掉。

這些 "生物" 那裡來? 當然是大家來開發 :D 最終目標是要把這 Matrix 建起來,讓各位的生物可以放進來互相較勁一番... 因此先替這些程式 (生物) 抽像化,定義好它跟世界,及跟其它生物之間互動的規格 (就是下篇 "抽像化 / 多型" 要說的) 就是必要的工作之一了。替生命定義好抽像化介面之後,就可以開始衍生出各種不同的生命型態,一起加入這個虛擬世界,因此繼承、多型的技術就派上用場了。

生命是會演化的,當世界上真的演化出一種新的生命型態時,整個世界可以 "安裝" 好新的生命型態,然後全部存檔,重新啟動嗎? 當然不行... 因此如何 "動態" 的加入新的生命型態,如何不停止 GameHost 的前提下,由新的 Assembly 載入 Class (再下篇要說明的 "動態載入"),也是必需克服的技術之一。

這幾個階段及目標,就是我這一系列文章想要做到的。聽起來好像很有趣,可是卻又沒什麼實際的用途... Orz, 沒辦法,我就是喜歡寫這類要動點腦筋的程式,即使畫面一點都不炫也沒關係... 平常工作就不大有機會寫這種程式了,加上現在又只剩一張嘴...。

如果順利的發展,我倒是有個打算,這個 GameHost 成形之後,我打算定些基本的規則,比如土地上會有一定的機率及規則,長出草 (食物) 來。而這世界有各種不同的生物 (EX: 羊),需要靠這世界上的資源維持生命。到時大家可以把自己創造的 "羊" 一起放到這個世界內,看看執行了一陣子之後,誰設計的 "品種" 比較好,最後可以一代一代的繁衍下來...。

想的很美好,不過我不像 darkthread 可以替最後優勝的造物者提供獎品.. Orz.. 未來的設計藍圖就先規劃到這裡。在繼續下去之前,我把程式重新整理了一下,有興趣的人可以下載回去。這份程式碼跟 #4 的功能結構是一樣的,只不過整個架構都作過重整,變數等命名也調整過了,是為了往後說明相關物件技術時,不會被這些從 #1 ~ #4 改的支離破碎的程式碼干擾...

嗯,講了一堆廢話,結論就是: 敬請期待續集 :D  哈哈...

下載重整過的程式碼:



9/19/2009 3:58:22 PM

[設計案例] 生命遊戲 #4, 有效率的使用執行緒

Microsoft.NET | 543 | C# | Threading | 我的作品 | 技術隨筆 | 物件導向

原本這篇不講執行緒,要直接跳到 OOP 多型的應用... 不過看一看 #3 自己寫的程式,實在有點看不下去... 30x30 的大小,程式跑起來就看到 903 條執行緒在那邊跑... 而看一下 CPU usage, 只有 5% 不到... 這實在不是很好看的實作範例,如果這是線上遊戲的 SERVER 程式,裡面的每個人,每個怪物等等都用一條專用的執行緒在控制他的行為的話,我看這遊戲不用太多人玩,SERVER 就掛掉了吧! 因此要繼續更貼近實際的生命模擬遊戲前,我們先來解決效能的問題,所以多安插了這篇進來 :D

前一篇 (#3) 的主題是把生命的進行,從被動的在固定時間被喚醒 (callback) 的作法,改成主動的在指定時間執行 (execute)。 想也知道,現實世界的生物都是 "主動" 的,後面的作法比較符合 OOP 的 "模擬世界,加以處理" 的精神。但是,一個小程式就吃掉 900 條執行緒,是有點過頭了。不知道還有沒有人記得,我騙到獎品的這個程式... 很另類的用 yield return 來解決類似問題的作法... 藉著 compiler 很雞婆的把單一流程翻成數段可以切開執行的邏輯...,正好拿來利用一下,替我們把一連串連續的邏輯切段,以便利用多執行緒來處理。我的想法是這樣,原程式是用個迴圈,作完該作的事,就休息 (sleep) 一段時間。而新的寫法,我打算用 yield return new TimeSpan(…) 來取代 Thread.Sleep(…)。每個 Cell內部的程式結構修改不大,不過對於 GameHost 就是個挑戰了... 來看看修改前及修改後的程式碼:

用 yield return TimeSpan 來取代 Thread.Sleep( ) 的作法[copy code]
   1:  // 修改前
   2:  // 使用 Thread.Sleep( ) 來控制時間
   3:  public void WholeLife(object state)
   4:  {
   5:      int generation = (int)state;
   6:      for (int index = 0; index < generation; index++)
   7:      {
   8:          this.OnNextStateChange();
   9:          Thread.Sleep(_rnd.Next(950, 1050));
  10:      }
  11:  }
  12:  //
  13:  //
  14:  //
  15:  // 修改後
  16:  // 使用 yield return new TimeSpan( ) 來控制時間
  17:  public IEnumerable<TimeSpan> WholeLife(object state)
  18:  {
  19:      int generation = (int)state;
  20:      for (int index = 0; index < generation; index++)
  21:      {
  22:          this.OnNextStateChange();
  23:          yield return TimeSpan.FromMilliseconds(_rnd.Next(950, 1050));
  24:      }
  25:      yield break;
  26:  }

 

別想的太美,只改這樣,程式是不會動的... 修改過之後,麻煩的地方會在 GameHost. 因為整個 GameHost 的邏輯都反過來了。原本是 GameHost 只要放著那九百條執行緒自生自滅,它只要不斷的刷新畫面就好了。現在它則得用 foreach(…) 去詢問:

"大爺,這次您要休息多久?"

接到 yield return 傳回的 TimeSpan 物件 (代表它要休息多久後,繼續下一個動作) 後,經過這段時間,GameHost 就要再去叫醒 cell, 然後再詢問一次:

"大爺,這次您要休息多久?"

關鍵就在於 GameHost 如何能透過少量的 thread 來伺後這些大爺,而不是像 #3 的程式一樣,每個大爺都用一條專屬的 thread… 要共用執行緒,就要先想辦法把工作切碎,這是基本法則。如果你希望你的生命遊戲程式不只是作業的話,那麼效能跟即時回應的問題是必需要考慮的。在動手改寫 GameHost 程式之前,先來分析一下改寫的目標有那些:

目標是要達到像 #2 範例一樣的效果,但是要用更有效率的方式。

目標很清楚,再來就看看有什麼手段可以用了。第一個是過量的執行緒,應該要想辦法改用執行緒集區。因為 #2 用了高達 900 條執行緒,不過整體 CPU USAGE 不到 5%,大部份的執行緒都在閒置狀態。如果能想辦法把這些運算丟到執行緒集區,由集區動態管理會有效率的多。

第二,就是把原本的 Thread.Sleep(ts) 改成 yield return ts 後,原本每個 thread 自己睡覺的機制,就要改成 cell 各自回報 game host 它想要睡多久,然後由 game host 統一在時間到時叫醒它。由於一次有多個 cell 同時在運作,因此我們需要一個簡單的排程器,作法像這樣:

  1. 建立一個時間表,依照時間順序,把每個 cell 預計要被叫醒的時間標上去。
  2. 時間到了之後,就去呼叫該 cell 的 OnNextStateChangeEx(),同時取得該 cell 下次要喚醒的時間,再標到時間表上
  3. GameHost就不斷的替每個 cell 重複 (1) (2) 的動作..
  4. 同時另外用一條獨立的執行緒,作畫面更新的動作。

嗯,要處理的方式越來越清楚了。剩下的是 "時間表" 要用什麼型式來表現? 我的選擇是,我希望它是個 ToDo List, 會幫我排好時間,我只要把工作標上時間,丟進 ToDo List, 然後 ToDo List 只要能忠實的回報給我還有沒有排定的工作? 如果有,下一個要處理的工作是那一個? 什麼時後處理?

它的用法只有丟工作進去,跟拿工作出來,因此我設計它的公開介面長這個樣子:

CellToDoList 的類別設計[copy code]
   1:  public class CellToDoList
   2:  {
   3:      public void AddCell(Cell cell) {...}
   4:      public Cell GetNextCell() {...}
   5:      public Cell CheckNextCell() {...}
   6:      public int Count {get;}
   7:  }

 

裡面的實作,我就不多說了。我是把它當成 QUEUE 在設計,唯一的差別是,放進 QUEUE 的東西會先經過排序,因此不見得是 "First In First Out" 這種典型的貯列,而是會以 Cell 上標示的時間為準,依序 Out …。實作起來很簡單,用現成的 SortedList 當內部的儲存方式,加上基本的 lock 機制來確保它是 thread safe 的就夠了。

好,這些雞絲都準備好之後,就可以來打造我們的新版 GameHost 了。來看看 Code:

改用 ThreadPool / CellToDoList 的新版 GameHost:[copy code]
   1:  static CellToDoList _cq;
   2:  static void _YieldReturnGameHost(string[] args)
   3:  {
   4:      int worldSizeX = 30;
   5:      int worldSizeY = 30;
   6:      World realworld = new World(worldSizeX, worldSizeY);
   7:      _cq = new CellToDoList();
   8:      // init threads for each cell
   9:      for (int positionX = 0; positionX < worldSizeX; positionX++)
  10:      {
  11:          for (int positionY = 0; positionY < worldSizeY; positionY++)
  12:          {
  13:              Cell cell = realworld.GetCell(positionX, positionY);
  14:              cell.OnNextStateChangeEx();
  15:              _cq.AddCell(cell);
  16:          }
  17:      }
  18:      // 啟動定期更新畫面的執行緒
  19:      Thread t = new Thread(RefreshScreen);
  20:      t.Start(realworld);
  21:      while (_cq.Count > 0)
  22:      {
  23:          Cell item = _cq.GetNextCell();
  24:          if (item.NextWakeUpTime > DateTime.Now)
  25:          {
  26:              // 時間還沒到,發呆一下等到時間到為止
  27:              Thread.Sleep(item.NextWakeUpTime - DateTime.Now);
  28:          }
  29:          ThreadPool.QueueUserWorkItem(RunCellNextStateChange, item);
  30:      }
  31:  }
  32:  private static void RunCellNextStateChange(object state)
  33:  {
  34:      Cell item = state as Cell;
  35:      TimeSpan? ts = item.OnNextStateChangeEx();
  36:      if (ts != null) _cq.AddCell(item);
  37:  }
  38:  private static void RefreshScreen(object state)
  39:  {
  40:      while (true)
  41:      {
  42:          Thread.Sleep(500);
  43:          (state as World).ShowMaps("");
  44:      }
  45:  }

 

GameHost 的工作很明確,一開始 (line 18 ~ 20) 就把更新畫面的動作完全交給另一個執行緒,之後就專心處理 ToDoList 內的工作了。

接著後面的 while loop (line 21 ~ 30) 則是很單純的從 ToDoList 裡取出下一個要要動作的 Cell, 如果時間還沒到就 Sleep 等一下它。執行完後會再詢問下一次是什麼時後,同時再把他加到 ToDoList 內等待下一次輪到他時繼續。

這次的程式我沒有設定停止的條件,因此你會看到程式會不斷的執行下去。程式執行起來,結果跟 #3 沒什麼不同,畫面上的每個細胞會照著題目的規則生長或死亡,不同的是 #3 的 Game Host 需要用到 903 條執行緒,而這版的 Game Host 只要 9 條執行緒...

image

 

其實,以這樣的範例題,我大可以不用顧慮到效能的問題,不過就是示範程式怎麼寫嘛。不過,我的目標如果只是訂在怎麼寫這練習題,大可以 GOOGLE 一下就有一堆作業解答了 :D。我的目標是要展示一下,該如何開發這樣的 GameHost ? 這樣的程式,是大部份的遊戲的基礎,尤其是像線上遊戲或是 facebook 這類互動遊戲的基礎。有了像樣的 Game Host 之後,接下來就把目標放在如何建立多樣的生物,一起放在這世界裡面生活了。接下來就會大量運用到 OOP 的特點 (對,就是上一篇預告的...) 繼承及多型。

有沒有人覺的,這種程式越寫越像 Matrix (就是駭客任務裡的 "母體") 了? 裡面活著的東西其實都在我的掌控之下... =_= 哈哈... 未完待續,請期待續集 :D。

--
範例程式:



9/15/2009 2:26:31 AM

[設計案例] 生命遊戲#3, 時序的控制

Microsoft.NET | C# | Threading | 作業系統 | 技術隨筆 | 物件導向

原本的範例,其實有些盲點,不知各位有沒看到? 一樣的起始狀態,一樣的遊戲規則,你不一定會得到一樣的結果。為什麼? 因為這會跟你程式 SCAN 的順序有關。怎麼說? 因為到目前為只,整個遊戲就好像下棋一樣,是 "回合制",我下完了換你... 一路一直輪下去。

這時先下後下就會影響結果了。現實世界的生命不是這樣的啊... 不知有沒有人玩過早期的太空戰士 (Final Fantasy) 系列遊戲? 當年 FF 有個很重要的突破,就是把 RPG 從傳統的 "回合制" 改成即時戰鬥... 每個人都有個倒數的碼錶,數到 0 你就可以發動下一次的攻擊... 這樣才接近現實世界啊。套用到我們的生命遊戲,這次我們想作的改變,就是把程式改成這種模式。

因此來調整一下規則,每個細胞每隔 1000ms 後會進到下一個狀態。不過生命總是沒有完全一樣的,因此每個細胞進到下一個狀態的時間差,都會有 10% 的誤差 (也就是 950ms ~ 1050ms 之間的時間都有可能)。其它規責則維持不變,來看看程式該怎麼改寫。

這種 "即時制",是比較合乎現實的情況的,如果未來你想發展到像 facebook 上的那些小遊戲,或是其它線上遊戲一樣的話, "回合制" 是決對行不通的... 這時,我們可以想像,每個細胞都有自己的執行緒,每換過一次狀態後就 Sleep() 一段時間,醒來再換到下一次狀態... 一直到指定的世代 (generation) 到達為止。

來看一下改版過的程式。我們先不動原本的 Cell, 只追加一個 method: WholeLife( ), 呼叫後就會一直更新這個細胞的狀態,直到它結束為止 (不是死掉喔,是 generation 到達)。而整個世界的所有細胞,都是獨立的個體,都有個專屬的執行緒在運作...。這時 Game Host 就得換個方式來讓這些細胞過日子 (執行),同時 Game Host 好像有個人造衛星一樣,不斷的在上空拍照來更新畫面,而完全不影響這些細胞的生命進行。

來看一下改寫過的 Cell 追加的 method:

Cell 追加的 method[copy code]
   1:  public void WholeLife(object state)
   2:  {
   3:      int generation = (int)state;
   4:      for (int index = 0; index < generation; index++)
   5:      {
   6:          this.OnNextStateChange();
   7:          Thread.Sleep(_rnd.Next(950, 1050));
   8:      }
   9:  }

 

改變不大,只是多個簡單的迴圈,跟 sleep 來控制時間而已。再來看看 Game Host 要怎麼改:

多執行緒版本的 Game Host[copy code]
   1:  static void Main(string[] args)
   2:  {
   3:      int worldSizeX = 30;
   4:      int worldSizeY = 30;
   5:      int maxGenerationCount = 100;
   6:      World realworld = new World(worldSizeX, worldSizeY);
   7:      // init threads for each cell
   8:      List<Thread> threads = new List<Thread>();
   9:      for (int positionX = 0; positionX < worldSizeX; positionX++)
  10:      {
  11:          for (int positionY = 0; positionY < worldSizeY; positionY++)
  12:          {
  13:              Cell cell = realworld.GetCell(positionX, positionY);
  14:              Thread t = new Thread(cell.WholeLife);
  15:              threads.Add(t);
  16:              t.Start(maxGenerationCount);
  17:          }
  18:      }
  19:      // reflesh maps
  20:      do
  21:      {
  22:          realworld.ShowMaps("");
  23:          Thread.Sleep(100);
  24:      } while (IsAllThreadStopped(threads) == false);
  25:      // wait all thread exit.
  26:      foreach (Thread t in threads) t.Join();
  27:  }
  28:  private static bool IsAllThreadStopped(List<Thread> threads)
  29:  {
  30:      foreach (Thread t in threads)
  31:      {
  32:          if (t.ThreadState != ThreadState.Stopped) return false;
  33:      }
  34:      return true;
  35:  }

 

其實這卅幾行 code, 大都花在控制執行緒上面,有興趣的讀者可以翻翻我之前寫的那系列文章,我就不多作說明了。調整之後,這個世界變的更不可測了,一樣的起始環境,連上帝 (在這模擬世界裡,我就是上帝 XD) 都無法預測下一秒會發生什麼事...

image

 

感覺就好像看電視一樣。畫面不斷的在閃動,而畫面裡的細胞會不規責的跳動,不像上一版程式一樣,每刷一次就變一次那樣的枯燥無聊。如果畫面呈現的地方再多用點心思,就可以弄的像卡通一樣,每個細胞都各自用自己的步調在活著...

到這裡,如何? 應該沒有人把作業寫到這個樣子了吧 XD (就說別抄我的程式去交作業了)。不適當的利用執行緒,也做的到類似的結果。不過,你花費的代價會很大,因為你的程式得自己來做 context switch (這些是 OS + thread scheduler 會幫你解決掉的,只要你曉得要用 thread)。

接下來下一篇,我們再繼續調整這世界的遊戲規則,加入更多元素進去,看看程式會變怎樣? 多執行緒解決時間的問題了,再來我們要用繼承及多型,讓不同的生命可以在同一個世界下共同生活...  ((待續))

 



9/14/2009 1:32:02 AM

[設計案例] 生命遊戲#2, OOP版的範例程式

Microsoft.NET | C# | 我的作品 | 技術隨筆 | 物件導向

還好,第一版的程式沒有難產。這版的目的很簡單,就是把題目實作出來,同時我會盡量套用物件導向的理念去設計程式的結構,而不是只把結果算出來而已。其實我一直覺的,這類生命模擬的程式,是非常適合用OOPL來實作的範例,大概OOPL所有強調的特性 (封裝、繼承、多型、動態聯結... 等等) 都用的到,算是完美的應用範例題吧!

不過很奇怪的,我特地 GOOGLE 了一下,不知 OOPL 高手都不屑寫這種範例還是怎樣,找到的範例程式,不管用什麼語言 (C/C++/Java/C#都有) 寫的,清一色都很沒有物件導向的 fu ... 好吧,只好自己來寫一個。

第一步,一定是先看看你的程式,分析出需要那些類別/物件,及它們之間的關係。比較正規的作法就是 UML 的 UseCase 了。不過這範例其實不大,我就直接跳到 Class Diagram 了 (因為VS2008剛好有現成的...)... 主要的類別有兩個: World (世界) 及 Cell (細胞)。

World 就是給 Cell 生活的空間,我們只訂義一個有限大小的二維空間,就一個 M x N 的棋盤這樣。而 Cell 則是一個細胞,描述單一一個細胞本身,在各種不同的條件下會有什麼反應。先貼一下 class diagram:

 

image  
圖1. class diagram (World & Cell)

老實說,這張圖還蠻乏善可陳的,World對外公開的介面,大概包含了幾個主要功能,就是取得指定座標的 Cell (GetCell), 及把目前的整個 World 狀態印出來 (ShowMaps) 的 method 而已。而 Cell 的公開介面,不外乎是它目前是活著還是死的,還有它的建構式,及呼叫後會把狀態轉移到下一次狀態的 method。

其它都是 World / Cell 互相溝通用,或是 Init 用的 Method / Prop, 就不多作介紹。先來看看主程式,扮演上帝的你,如何讓這堆單細胞生物,在你的世界裡活起來:

Game Of Life 主程式[copy code]
   1:  static void Main(string[] args)
   2:  {
   3:      int worldSizeX = 30;
   4:      int worldSizeY = 30;
   5:      int maxGenerationCount = 100;
   6:      World realworld = new World(worldSizeX, worldSizeY);
   7:      for (int generation = 1; generation <= maxGenerationCount; generation++)
   8:      {
   9:          realworld.ShowMaps(string.Format("Generation: {0}", generation));
  10:          Thread.Sleep(1000);
  11:          for (int positionX = 0; positionX < worldSizeX; positionX++)
  12:          {
  13:              for (int positionY = 0; positionY < worldSizeY; positionY++)
  14:              {
  15:                  // do day pass
  16:                  Cell cell = realworld.GetCell(positionX, positionY) as Cell;
  17:                  cell.OnNextStateChange();
  18:              }
  19:          }
  20:      }
  21:  }

 

主程式我還沒把不相干的動作刪掉,也才廿一行... line 1 ~ 5 只是初始值,line 6 建立整個世界,之後就每跑完一個世代 (generation) 就休息一秒鍾,繼續下一次進化。這樣隨著時間的過去,畫面上會一直更新整個世界的狀態... 直到只定的次數到了為止。

 

class World 的部份就沒什麼特別的,就只是把一個二維陣列包裝一下而已。直接貼 Code 就混過去吧 XD,一樣沒有刪掉程式碼,原 CODE 照貼:

class World 的程式碼[copy code]
   1:  public class World
   2:  {
   3:      private int SizeX = 0;
   4:      private int SizeY = 0;
   5:      private Cell[,] _map;
   6:      public World(int maxPosX, int maxPosY)
   7:      {
   8:          this._map = new Cell[maxPosX, maxPosY];
   9:          this.SizeX = maxPosX;
  10:          this.SizeY = maxPosY;
  11:          for (int posX = 0; posX < maxPosX; posX++)
  12:          {
  13:              for (int posY = 0; posY < maxPosY; posY++)
  14:              {
  15:                  this._map[posX, posY] = new Cell(this, posX, posY);
  16:              }
  17:          }
  18:      }
  19:      internal void PutOn(Cell item, int posX, int posY)
  20:      {
  21:          if (this._map[posX, posY] == null)
  22:          {
  23:              this._map[posX, posY] = item;
  24:              item.PosX = posX;
  25:              item.PosY = posY;
  26:          }
  27:          else
  28:          {
  29:              throw new ArgumentException();
  30:          }
  31:      }
  32:      public Cell GetCell(int posX, int posY)
  33:      {
  34:          if (posX >= this.SizeX) return null;
  35:          if (posY >= this.SizeY) return null;
  36:          if (posX < 0) return null;
  37:          if (posY < 0) return null;
  38:          return this._map[posX, posY];
  39:      }
  40:      public void ShowMaps(string title)
  41:      {
  42:          Console.Title = title;
  43:          Console.SetWindowSize(this.SizeX * 2, this.SizeY);
  44:          Console.SetCursorPosition(0, 0);
  45:          Console.Clear();
  46:          for (int y = 0; y < this.SizeY; y++)
  47:          {
  48:              for (int x = 0; x < this.SizeX; x++)
  49:              {
  50:                  Cell item = this.GetCell(x, y);
  51:                  Console.SetCursorPosition(x * 2, y);
  52:                  Console.Write(item.IsAlive? "●":"○");
  53:              }
  54:          }
  55:      }
  56:  }

 

接下來是封裝每個細胞本身跟環境互動的影響,把上一篇講的規則對應成程式碼的樣子。先來看看 CODE:

class Cell 的程式碼[copy code]
   1:  public class Cell //: Life
   2:  {
   3:      protected World CurrentWorld { get; private set; }
   4:      internal int PosX = 0;
   5:      internal int PosY = 0;
   6:      private const double InitAliveProbability = 0.2D;
   7:      private static Random _rnd = new Random();
   8:      public Cell(World world, int posX, int posY) //: base(world, posX, posY)
   9:      {
  10:          this.CurrentWorld = world;
  11:          // setup world
  12:          this.PosX = posY;
  13:          this.PosY = posY;
  14:          this.CurrentWorld.PutOn(this, posX, posY);
  15:          this.IsAlive = (_rnd.NextDouble() < InitAliveProbability);
  16:      }
  17:      public bool IsAlive { get; private set; }
  18:      protected IEnumerable<Cell> FindNeighbors()
  19:      {
  20:          foreach (Cell item in new Cell[] {
  21:              this.CurrentWorld.GetCell(this.PosX -1, this.PosY-1),
  22:              this.CurrentWorld.GetCell(this.PosX, this.PosY-1),
  23:              this.CurrentWorld.GetCell(this.PosX+1, this.PosY-1),
  24:              this.CurrentWorld.GetCell(this.PosX-1, this.PosY),
  25:              this.CurrentWorld.GetCell(this.PosX+1, this.PosY),
  26:              this.CurrentWorld.GetCell(this.PosX-1, this.PosY+1),
  27:              this.CurrentWorld.GetCell(this.PosX, this.PosY+1),
  28:              this.CurrentWorld.GetCell(this.PosX+1, this.PosY+1)})
  29:          {
  30:              if (item != null) yield return item;
  31:          }
  32:          yield break;
  33:      }
  34:      public void OnNextStateChange()
  35:      {
  36:          int livesCount = 0;
  37:          foreach (Cell item in this.FindNeighbors())
  38:          {
  39:              if (item.IsAlive == true) livesCount++;
  40:          }
  41:          if (this.IsAlive == true && livesCount <1)
  42:          {
  43:              //孤單死亡:如果細胞的鄰居小於一個,則該細胞在下一次狀態將死亡。
  44:              this.IsAlive = false;
  45:          }
  46:          else if (this.IsAlive == true && livesCount >= 4)
  47:          {
  48:              //擁擠死亡:如果細胞的鄰居在四個以上,則該細胞在下一次狀態將死亡。
  49:              this.IsAlive = false;
  50:          }
  51:          else if (this.IsAlive == true && (livesCount == 2 || livesCount == 3))
  52:          {
  53:              //穩定:如果細胞的鄰居為二個或三個,則下一次狀態為穩定存活。
  54:              //this.IsAlive = true;
  55:          }
  56:          else if (this.IsAlive == false && livesCount == 3)
  57:          {
  58:              //復活:如果某位置原無細胞存活,而該位置的鄰居為三個,則該位置將復活一細胞。
  59:              this.IsAlive = true;
  60:          }
  61:          else
  62:          {
  63:              // ToDo: 未定義的狀態? assert
  64:          }
  65:      }
  66:  }

這裡開始應用到 OOPL 第一個特性: 封裝。從程式碼可以看到,主要的邏輯都被包在裡面了,就 Game Of Life 裡提到的四條規則。

程式這樣寫起來,比那些作業的標準答案看起來舒服多了吧? 雖然行數多了一些,不過看起來比較有 OO 的樣子了。當然只是看起來爽是沒用的,這樣的架構,到目前為只除了邏輯清楚一點之外,還看不到其它很明顯的好處。不過當這個規責稍微複雜一點,OOPL的優點就會被突顯出來了。

下回,把題目做點變化,再來看看程式該如何調整…   ((待續))

--
附件: 範例程式碼



9/12/2009 4:09:30 AM

[設計案例] 生命遊戲#1, 前言

Microsoft.NET | C# | Threading | 技術隨筆 | 物件導向

[前言]

好久沒寫點自己覺的有內容的東西了... 最近 code 寫的少,實在沒有什麼了不起的新技術可以分享,而 thread 那種 "古典" 計算機科學的東西也寫的差不多了.. 就懶了起來。

雖然沒新技術好寫,不過老狗玩的把戲還是能榨出點渣的... 很多人都熟新技術,可以寫出很炫的程式,不過也常看到程式的結構真的是亂搞一通的... 所以我打算寫些 [設計案例] 的文章,舉一些我實作過的案例,說明什麼樣的問題可以用什麼方式或技術來解決。其實我想寫的就是像 design patterns 那類的東西,只不過我程度還差的遠,只能稱作 "案例" ... Orz

----------------------------------------------------------------------------------

最近 facebook 上有一些小遊戲,不知道在紅什麼... 突然間大家都在玩,就都是些模擬遊戲,像是開心農場、My FishBowl … 之類的,你要在裡面種東西或養魚,條件充足就會長大,收成等等... 然後透過 Facebook API 可以跟別人互動的遊戲。看到這類的 GAME,不禁想起過去在唸書時,幾個經典的作業題目,其中一個 [生命遊戲] (Game of Life) 就是這種 GAME 的始祖...

在 Wiki 找的到這段介紹:

http://zh.wikipedia.org/zh-hk/%E7%94%9F%E5%91%BD%E6%B8%B8%E6%88%8F

生命遊戲(Game of Life),又稱生命棋,是英國數學家約翰·何頓·康威(John Horton Conway)在1970年發明的細胞自動機(cellular automaton,也翻譯成「格狀自動機」)。

它最初於1970年10月在《科學美國人》(Scientific American)雜誌中馬丁·葛登能(Martin Gardner)的「數學遊戲」專欄出現。

1970… 我還沒出生... Orz, 不過, 這麼一個古老經典的問題,找的到一大堆範例程式,或是作業解答。清一色是用 C 這類配的上它的年紀的程式語言寫的,就算有 JAVA 版,大概也是換湯不換藥... 這四十年程式語言及軟體技術的進步,寫這種程式總該有點改變吧?

這篇我想寫的,就是這樣的問題,配合現在的 .NET / C#,能怎麼寫它? 這年代的軟體開發技術,對這種古典的程式能發揮什麼效益?

(警告: 剛好要交作業的人,可千萬別用我的方法交出去啊... 你的助教看不懂可能會給你零分...)

先找個範例來看看... 為了不讓過多的畫面處理程式碼,干擾到主程式的架構,我特地找了兩個 console based 的範例:

Java 版:
http://tw.myblog.yahoo.com/dust512/article?mid=25&prev=28&next=-1

多語言版 (C, Java, Python, Scala):
http://caterpillar.onlyfun.net/Gossip/AlgorithmGossip/LifeGame.htm

這... 這就是典型的 "Java 版 C 程式碼" 的範例... 用 Java 來寫只寫這樣,有點用牛刀的感覺... 新的開發環境強調這幾項:

  1. 物件導向 (封裝,多型,動態連結... etc)
  2. 多執行緒
  3. 其它語言特色 (這次會講到的是 yield return)

這些技術怎麼套進這程式? 先來看看這遊戲有幾個障礙要克服吧。遊戲的規則簡單明瞭,借轉貼上面第二個範例的說明:

生命遊戲(game of life)為1970年由英國數學家J. H. Conway所提出,某一細胞的鄰居包括上、下、左、右、左上、左下、右上與右下相鄰之細胞,遊戲規則如下:

  1. 孤單死亡:如果細胞的鄰居小於一個,則該細胞在下一次狀態將死亡。
  2. 擁擠死亡:如果細胞的鄰居在四個以上,則該細胞在下一次狀態將死亡。
  3. 穩定:如果細胞的鄰居為二個或三個,則下一次狀態為穩定存活。
  4. 復活:如果某位置原無細胞存活,而該位置的鄰居為三個,則該位置將復活一細胞。

以前我最討厭寫這種程式了,這種程式寫起來就跟 Regexp 一樣,是 "write only” 的 code… 怎麼說? 程式寫好後,可能自己都看不懂了,因為邏輯被切的亂七八糟... GAME 裡可能同時有好幾個細胞,每個都有獨立的規則,不過程式卻是一個主迴圈,每次執行每個細胞的一小段邏輯... 程式的流程就這樣被切碎了... 我打算用C#的 yield return, 解決這邏輯破碎的問題。

第二個障礙,就是這類程式,某種程度都是隨著時間的進行而跑的,比如上面的條件都是 "下一次狀態" … 把每次狀態改變定義一個時間 (比如一秒),這就是個 realtime 的模擬程式了。如果有的細胞是一秒改變一次狀態,有的是兩秒,有的是五秒... 那就傷腦筋了... 你的程式會被切的更破碎... 這些每種細胞特殊的部份,我打算用 OOP 的多型來解決。

最後,這種很明顯是 "並行" 的問題,照道理來說,用多執行緒是最適合的了。不過隨便也有成千上萬個 "細胞" 在成長,每個都來一個 thread 養它,再高級的 server 都撐不住吧? 這邊會來探討一下,怎麼用執行緒相關的技巧,來解決這問題。

 

--------------------------------------------------------------------------------------

寫到這裡,突然覺的這題目好大... Orz, 搞不好這幾篇要撐幾個月才寫的完... 至少有個題材好寫,等到我生出第一個 sample code, 就會有下一篇了... 如果有同好也想試試看的,也歡迎分享看看你的 code… 只不過我沒像 darkthread 有本錢提供獎品... 哈哈 :D



8/5/2009 2:30:49 AM

JPEG XR (就是 Microsoft HD Photo 啦) 已經是 ISO 正式標準了...

Microsoft.NET | 543 | WPF | 技術隨筆

先寫在前面,這篇不是什麼技術的探討或是評論,純脆是我個人看到這消息的想法而已。很久沒貼些軟體相關的文章了,最近比較少在動手寫 Code, 自然就沒什麼新題材好寫 @@,不過這兩天倒是看到一個蠻令人興奮的新聞,就是:

 

JPEG XR 已經正式通過 ISO 標準了!!

 

http://jpeg.org/newsrel26.html
http://blogs.msdn.com/billcrow/archive/2009/07/29/jpeg-xr-is-now-an-international-standard.aspx

 

JPEG 應該已經無人不知,無人不曉了吧? 不過當年還是有朋友鬧過笑話... 曾有人正經八百的來問我

"什麼是 [結合照片專業群組] 啊???" 就是 JPEG 啦 (無聊的話看一下底下的題外話)

我還丈二金剛摸不著頭腦,把他在看的整篇文章拿過來看,才晃然大悟他到底在問啥 =_= ... 原來是 "JPEG: Joint Photographic Experts Group”的縮寫... 當然類似的 MPEG (Moving Picture Experts Group) 也碰過類似的笑話... 無聊 GOOGLE 一下,竟然還查的到一篇範例...

http://support.microsoft.com/default.aspx/kb/235928/zh-tw

My God… 這翻譯真是比之前碰到了 "註冊傑克" 還絕 XD...

之前其實沒特別注意這些標準,曾經有印像的就是用 wavelet 壓縮方式的 JPEG2000... 嘗試取代 JPEG,也取得 ISO 的標準化,不過一直沒達成它的目的,只在特定領域還有應用空間。兩年前 Microsoft 隨著 Vista / WPF 推出 Windows Media Photo 的格式,後來為了讓它成為標準,換了個叫沒有 MS 色彩的名字: HD Photo, 最後變成現在的 JPEG XR ..

我是在兩年前,隨著 .NET 3.0 推出 WPF,剛好自己用的 CANON 相機的 RAW FILE 又被 WPF 支援,所以開始研究相關的 API 及 support .. 在關於 HD Photo 眾多報導中,有個觀點是我相當認同的。找不到較具代表性的消息來源,我就憑記憶寫一下,大意是:

 

隨著技術進步,未來影像設備 (如印表機,掃描器,顯示器等等) 的色彩表現能力及色域會遠超過 JPEG 格式的範圍 (現在就是了),因此儲存格式支援的動態範圍 (dynamic range) 越高,對於影像的長期保存越重要。

 

這就是處女座的龜毛個性啊... 衝著這個看法,我從 Canon PowerShot G2 時代開始,我就試著盡量用 .CRW 格式 (CANON RAW) 來保存相片,而不是用 JPEG。後來換了 Canon PowerShot G9,正好 WPF 出來,我就開始改用保存 .CR2 檔,而另外轉一份 JPEG 檔來作一般用途 (畢竟 JPEG 還是方便的多)。不過一張照片花掉 15 ~ 20mb, 保存起來壓力還真不小 =_=

 

現在看到 JPEG XR 的標準化,正好是我要的東西啊 :D   我需要的正是個能妥善保存這些影像資料細節的方式,同時能讓我輕鬆愉快的使用,不用耽心工具支不支援,或是其它五四三等問題困擾...。這些問題對阿宅來說,一點都不困難,有一缸子的工具辦的到,不過... 如果隨變看個照片,或是要 COPY 給家人朋友看,還要動用一堆雞絲,那也太辛苦了一點... 能有個通用的標準格式及大廠背書,那是再好也不過了 :D

 

所以,接下來要做什麼? 我突然慶興我一直都有留著這幾年拍下來的 RAW file (.CRW / .CR2) 檔案... 該是替我的歸檔程式翻新的時後了,下一步是開始嘗試用 .WDP 來取代現在放兩份 RAW + JPEG 的方式...



4/17/2009 7:51:00 PM

RUNPC 精選文章 - 運用ThreadPool發揮CPU運算能力

543 | C# | RUNPC | Threading | 我的作品 | 技術隨筆 | Microsoft.NET | [精選文章]

果然這個什麼東西都上網的年代,要三不五時的 GOOGLE 一下自己,才會知道那些網站把你的八卦跟內幕爆了出來... 不過應該沒啥週刊記者對我有興趣吧? 哈哈。在 GOOGLE 自己名字時,倒是意外發現,之前投稿的文章,又有一篇被拿來登在網站上的精選文章了 :D

特此留念一下 :D

http://www.runpc.com.tw/content/main_content.aspx?mgo=176&fid=E02

--

順便整理一下懶人包:

另一篇精選文章 [RUN!PC 精選文章 - 生產線模式的多執行緒應用]

過去投過的系列文章 (multi-threading programming using c#):

2008/11. 生產線模式的多執行緒應用
2008/09. 用ThreadPool發揮CPU運算能力
2008/06. SEMAPHORE在ASP.NET的應用
2008/04. 以ASP.NET開發同步WEB應用程式



3/3/2009 3:48:15 AM

EF#3. Entity & Inheritance

Microsoft.NET | C# | Entity Framework | ORM | SQL | 技術隨筆 | 物件導向

繼承 (inheritance) 是物件技術的核心,就是這個特性提供了 OOP 絕大部份的特色。這東西被拿掉的話,OOP就沒這麼迷人了。繼然談到了 ORM,就不能不來看看 R(關聯式資料庫) 怎麼被對應到 O(物件),同時還能處理好繼承關係。

RDBMS 連基本的物件 (Object Base) 都不支援了,更別說物件導向 (Object Oriented) 了。因此要搞懂 ORM 及繼承的關係,就得先瞭解基本的 OO 是怎麼實作 "繼承" 這個動作。這些知識是古早以前學 C++ 時唸到的,現在的 CLR 不知道有沒有新的作法? 不過應該大同小異吧! C++ 主要是靠 virtual table 來實作繼承關係,當子類別繼承父類別時,父類別定義的 data member 跟 method 就全都遺傳到子類別身上了,這動作就是靠 virtual table 作到的。細節我就不多說了,有興趣的讀者們請先上網找找相關資訊看一看。

ORM 的運氣好多了,只要處理資料的部份。因此前一段提到的 virtual table 如果要拿來應用也會簡單的多。virtual table 可以很直覺的想像成是 DBMS 裡 table schema 的定義,而一個物件 (instance) 的 virtual table 資料,正好就對應到該 table (DBMS) 的一筆資料。這正好是 ORM 基本的動作。大部份 OO 的書都會說,繼承就是 " Is A " 的關係。在資料上則是子類別擁有父類別所有的欄位定義。這很容易對應到資料庫的正規化,該如何切割資料表的規責。你可以切開靠 PK / FK 再併回來,或是直接反正規化讓它重複定義在多個 TABLE... 事實上,兩大 ORM (EF & NH) 都歸納出三種作法,後面來探討一下彼此的差異...

再來看看繼承關係,假設父類別 class A 對應到 table A, 那麼衍生出的子類別 class B 對應的 table B, 則應該要包含所有 table A 定義的欄位才對。從這點出發,就帶出了第一種作法: 就是把 table A 所有的欄位都建一份到 table B (註: table per concrete type)。

不過這樣看起來有點蠢,DBMS 熟悉的人也許會採另一種作法: 沒錯... table B 只要留個 foreign Key, 指向 table A 的 primary Key,需要時再 join 起來就好了,這是第二種作法 (註: table per type)。

唸過 DBMS 的人都還記得 "正規化" (normalization) 跟 "反正規化" 吧? 切割過頭也是很麻煩的,因此有第三種作法逆其道而行,就是建一個 table 給所有的子子孫孫類別共用。因此 table 需要的欄位,就是所有的子類別的所有欄位集大成,通通都建進來... 不用的話就空在那裡,這是第三種作法 (註: table per hierarchy)。

這三種作法,在 Entity Framework (以下簡稱 EF) 或是 NHibernate (以下簡稱 NH) 都有對應的作法,只不過名字不大一樣... 這篇 ADO.NET team blog 借紹的還不錯,可以參考看看。這三種方式,在 EF 裡的說法分別是 (括號裡是 NH 的說法,參考這篇: Inheritance Mapping):

  • Table per Hierarchy (NH: Table per class hierarchy)
  • Table per Type (NH: Table per subclass)
  • Table per Concrete Type (NH: Table per concrete class)

事時上,處理方式大同小異,不外乎用三種不同的對應方式,來處理物件繼承關係。這些不同類別的物件彼此有繼承關係,對應到 TABLE 的方法不同,各有各的優缺點。其實 ADO.NET team blog 講的都很清楚,我就不再多說,簡單列張比較表:

  適用於 不適用於
Table Per Hierarchy
  1. 最簡單的實作方式
  2. 所有同系類別的實體 (instance) 數量不會很多時
  3. 需要用單一 QUERY 查出所有的子類別物件時
  4. 繼承階層較簡單的情況
  5. 類別的欄位要調整很容易
  1. instance數量太多,會嚴重影響效能
  2. 無法在table schema上做太多嚴格的檢查
Table Per Type
  1. 繼承關係清楚的對應到 TABLE
  2. 需要用單一QUERY查出所有子類別的物件
  3. 不同於 TPH,可以針對每種類別,設定嚴僅的 table constraint
  4. 每個類別要變動或調整都很容易
  1. 繼承階層較多時,要取得單一 instance data 需要透過多層 join
  2. table 數量會隨著類別的數量快速增長
Table Per Concrete Type
  1. 綜合 TPH / TPT 的優點 (也綜合了兩者的缺點)
  2. 可以針對每種類別設定 table constraint
  3. ORM mapping 很簡單
  1. 要用單一QUERY查出所有子類別的物件並不容易 (需要把所有的 TABLE JOIN 起來)
  2. 父類別的欄位調整很麻煩,所有的 TABLE 都需要配合調整

 

[未完待續] to be continue…



1/23/2009 12:09:53 AM

EF#2. Entity & Encapsulation

Microsoft.NET | C# | Entity Framework | ORM | SQL | 物件導向

前一篇講了一堆大道理,這篇就來看一些實作吧。各種 ORM 的技術都有共同的目的,就是能把物件的狀態存到關聯式資料庫,而這樣的對應機制則是各家 ORM 競爭的重點,勝負的關鍵不外乎是那一套比較簡單? 那一套包裝出的 Entity 物件能夠更貼近一般的物件?

會有這樣的 "對應" 機制需求,原因只有一個,物件技術發展的很快,已經能解決大多數軟體開發的需求了,不過資料庫就沒這麼幸運,現在的 DBMS 撇開一些技術規格不談,本質上還是跟廿年前差不多,就是關聯式資料庫而已,本質上就是一堆 table + relationship, 配合 SQL 語法來處理資料。發展至今,物件技術跟資料庫技術能處理的問題,已經是兩個完全不同世界的問題了,三層式的架構在這段出現斷層...。

解決方式大概有兩條路,一種就是想辦法把這兩個世界串起來,就是 ORM framework 想做的事。另一個就是改造 RDBMS,讓 RDBMS 進化成也具有物件導向特性的資料庫。不過以眼前的五年十年來看,ORM 還是大有可為。ORM 只要能把 "對應" 這件事做到完美的地步,其實在某個層面上就已經做到 OODB 的願景了,只差在這些物件是活在 APP 這端,不是活在資料庫那端...。

扯遠了,接下來我會試著從物件技術的三大核心 (封裝、繼承、多型),及資料庫最需要的查尋機制 (QUERY) 來看看 Entity Framework 各能提供什麼支援,才能客觀的評論 Entity Framework 值不值得你投資在它身上。

在繼續看下去之前,請先俱備基本的 Entity Framework 運用的能力。在 MSDN 名家專欄裡 MVP(朱明中) 寫的這幾篇我覺的很不錯,可以參考看看。我就是看這幾篇入門的 :D。幾年前在比賽上碰過他幾次,我還蠻配服他的,可以靠自學而有今天的成就。以下是他寫的幾篇 ADO.NET / Entity Framework 的系列文章:

  1. 讀寫 ADO.NET Entity Framework (2007 年 9 月)
  2. 由 LINQ 存取 ADO.NET 物件 (2007 年 9 月)
  3. 整合 ADO.NET Entity Framework 到應用程式中 (2007 年 9 月)
  4. 首次接觸 ADO.NET Entity Framework (2007 年 9 月)
  5. ADO.NET Entity Framework 概觀 (2007 年 9 月)

 

在開始之前,我們先來看看一個最簡單的 Entity Framework 的範例,然後來看看封裝性能夠對你的程式帶來什麼影響? 先來看看只用到了 ORM 卻沒發揮封裝性的例子:

image

這是存放會員資料的表格,對應的 TABLE 很簡單,SQL 如下:

[copy code]
   1:  CREATE TABLE [dbo].[Users](
   2:    [ID] [nvarchar](50) NOT NULL,
   3:    [PasswordHash] [image] NOT NULL,
   4:    [PasswordHint] [nvarchar](100) NOT NULL,
   5:    [SSN] [nchar](10) NOT NULL,
   6:    [Gender] [int] NOT NULL,
   7:   CONSTRAINT [PK_Users] PRIMARY KEY CLUSTERED
   8:  ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]

 

大部份的人在 EDMX Designer 裡把資料表拉進來後,就開始用這個 Entity Class 了吧? 密碼的部份為了安全及實作上的考量,DB只存放 HASH,而 HASH 的運算則透過 .NET 程式來計算,不透過 SQL 的函數。作法決定後,你可能會寫出這樣的程式碼:

建立帳號的程式碼[copy code]
   1:  // 準備 object context
   2:  using (Membership ctx = new Membership())
   3:  {
   4:      // create user account:
   5:      User newUser = new User();
   6:      newUser.ID = "andrew";
   7:      newUser.PasswordHint = "12345";
   8:      newUser.PasswordHash = HashAlgorithm.Create("MD5").ComputeHash(Encoding.Unicode.GetBytes("12345"));
   9:      newUser.SSN = "A123456789";
  10:      newUser.Gender = 1;
  11:      ctx.AddToUserSet(newUser);
  12:      ctx.SaveChanges();
  13:  }

 

檢查密碼的程式碼[copy code]
   1:  // 準備 object context
   2:  using (Membership ctx = new Membership())
   3:  {
   4:      string passwordText = "12345";
   5:      User curUser = ctx.GetObjectByKey(new EntityKey("Membership.UserSet", "ID", "andrew")) as User;
   6:      bool isPasswordCorrect = true;
   7:      {
   8:          byte[] passwordTextHash = HashAlgorithm.Create("MD5").ComputeHash(Encoding.Unicode.GetBytes(passwordText));
   9:          if (passwordTextHash.Length != curUser.PasswordHash.Length)
  10:          {
  11:              isPasswordCorrect = false;
  12:          }
  13:          else
  14:          {
  15:              for (int pos = 0; pos < curUser.PasswordHash.Length; pos++)
  16:              {
  17:                  if (passwordTextHash[pos] != curUser.PasswordHash[pos])
  18:                  {
  19:                      isPasswordCorrect = false;
  20:                      break;
  21:                  }
  22:              }
  23:          }
  24:      }
  25:      Console.WriteLine("Password ({0}) check: {1}", passwordText, isPasswordCorrect ? "PASS" : "FAIL");
  26:  }

 

這樣的 User 類別設計有什麼問題? 我列幾個我認為設計上不妥的地方:

  1. 直接提供 PasswordHash 曝露過多不必要的實作細節
  2. 在台灣,身份證字號 (SSN) 跟性別 (Gender) 是相依的欄位 ( functional dependency )

以物件導向的角度來看,User 真正要提供的是接受 "驗證密碼" 的要求,至於你的實作是提供明碼或是用 Hash, 都是實作的細節。提供原始未加密的密碼,或是提供處理過的 HASH,在需求上都是不必要個功能。物件的介面定義要盡量以能滿足需求的最小介面為原則,其它的都不要公開,才滿足 "封裝性" 的要求。因此良好的設計應該把這些細節封裝起來,只在公開的介面表達你要提供的功能。

另外依照台灣的身份證字號規則, SNN 跟 Gender 是連動的。目前 User 的設計是把兩者的關係丟給前端寫網頁的人來維護,一不注意就會發生不一致的情況。DB 對於這種問題的解決方式,不外乎寫 trigger 或是其它 constraint 的方式來阻擋不正確的資料被寫入 DB,不過看了前面提到的規則,要單純用 SQL 的功能完整實作出來,還不大容易。

另一種作法,只儲存 SSN,Gender 欄位則以 VIEW 的方式提供,這樣就不會有不一致的問題。不過這方法的缺點在於,當邏輯太複雜的時後,常常會超出 SQL 能處理的範圍,效能也許會是個問題,或是 constraint 不能完全跟程式端一致。

就我看來,這類看似應該在 data layer 實作的複雜邏輯,又難以在 SQL DB 上面解決的問題,才是 Entity Framework 的強項。現在來看看 Entity Framework 能怎麼解決這些資料封裝的需求:

首先,把不需要公開的細節改成 Private 隱藏起來,包括 PasswordHash 的 Getter / Setter, Gender 更名為 GenderCode, 同時把 Getter / Setter 也改為 Private ...

接下來就要把這些封裝起來的細節,提供另一組較合適的公開資訊的方式。這時 .EDMX designer 替我們產出的 code 就能搭配 partial class 擴充功能了。來看看我們在 partial class 裡寫了什麼?

User.cs 的內容 (partial class)[copy code]
   1:  public partial class User
   2:  {
   3:      public string Password
   4:      {
   5:          set
   6:          {
   7:              this.PasswordHash = this.ComputePasswordHash(value);
   8:          }
   9:      }
  10:      public bool ComparePassword(string passwordText)
  11:      {
  12:          byte[] hash = this.ComputePasswordHash(passwordText);
  13:          // compare hash
  14:          if (this.PasswordHash == null) return false;
  15:          if (hash.Length != this.PasswordHash.Length) return false;
  16:          for (int pos = 0; pos < hash.Length; pos++)
  17:          {
  18:              if (hash[pos] != this.PasswordHash[pos]) return false;
  19:          }
  20:          return true;
  21:      }
  22:      public GenderCodeEnum Gender
  23:      {
  24:          get
  25:          {
  26:              return (GenderCodeEnum)this.GenderCode;
  27:          }
  28:      }
  29:      partial void OnSSNChanging(string value)
  30:      {
  31:          // ToDo: check ssn rules.
  32:          // sync gender code
  33:          this.GenderCode = int.Parse(value.Substring(1, 1));
  34:      }
  35:      private byte[] ComputePasswordHash(string password)
  36:      {
  37:          if (string.IsNullOrEmpty(password) == true) return null;
  38:          return HashAlgorithm.Create("MD5").ComputeHash(Encoding.Unicode.GetBytes(password));
  39:      }
  40:  }
  41:  public enum GenderCodeEnum : int
  42:  {
  43:      FEMALE = 0,
  44:      MALE = 1
  45:  }

 

被隱藏起來的 PasswordHash, 公開的介面就用 Password 的 Setter 跟 ComparePassword( ) method 取代,明確的用程式碼告訴所有要用它的 programmer:

"密碼只准你寫,不准你讀 (read only)... 只告訴你密碼對不對, 不會讓你把真正的密碼拿出去"

另一個部份,就是身份證字號跟性別的問題,則改用另一個方式解決。SSN 這個屬性維持不變,在它被更動時就一起更動 GenderCode 這個欄位。GenderCode 完全不對外公開,公開的只有把 int 轉成 GenderCodeEnum 的屬性: Gender。同時為了保護資料的正確性,只開放 Getter, 不開放 Setter。

 

同樣的程式,在我們調整過 Entity 的封裝之後,再來重寫一次看看:

建立新的使用者帳號[copy code]
   1:  // 準備 object context
   2:  using (Membership ctx = new Membership())
   3:  {
   4:      User newUser = new User();
   5:      newUser.ID = "andrew";
   6:      newUser.PasswordHint = "My Password: 12345";
   7:      newUser.Password = "12345";
   8:      newUser.SSN = "A123456789";
   9:      ctx.AddToUserSet(newUser);
  10:      ctx.SaveChanges();
  11:  }

 

 

檢查密碼是否正確[copy code]
   1:  // 準備 object context
   2:  using (Membership ctx = new Membership())
   3:  {
   4:      EntityKey key = new EntityKey("Membership.UserSet", "ID", "andrew");
   5:      User user = ctx.GetObjectByKey(key) as User;
   6:      // 要比對的密碼
   7:      string passwordText = "123456";
   8:      bool isPasswordCorrect = user.ComparePassword(passwordText);
   9:      Console.WriteLine("Password ({0}) check: {1}", passwordText, isPasswordCorrect ? "PASS" : "FAIL");
  10:  }

 

修改過的程式簡潔多了。不過比少打幾行程式碼更重要的是,它的邏輯更清楚,更不容易出錯。如果沒有妥善的處理封裝性的問題,可以想像寫出來的程式一定亂七八糟。要嘛不正確的資料都會被寫進 DB,不然就是 DB 有作適當的防範,但是程式沒有作好,最後就是到處都出現 SqlException ...

這裡只是簡單示範一下 Entity Framework 如何替資料提供封裝的特性,後續的文章會繼續示範 Entity Framework 如何能把 DBMS 的資料,進一步的應用到物件技術的繼承及多型等特性。敬請期待下集 :D



1/21/2009 8:35:27 PM

EF#1. 要學好 Entity Framework? 請先學好 OOP 跟 C# ...

Microsoft.NET | C# | Entity Framework | ORM | 技術隨筆 | 物件導向

這次為了能順利的學好 Entity Framework,花了不少工夫在研究它的作法。不過有一大半不是在 Entity Framework 本身,而是在 C# 的一些特別的語法跟 LINQ 身上...。也因為這樣,我深切的體認到一個 ORM 技術能不能成功,其實都是在 Hosting 這個 Framework 的環境夠不夠成熟...。

不過在摸索的過程中,找到的資訊都是片斷的,每一篇都是講實作,範例程式,操作步驟... 等等,而當時我最需要的反而是幫助我決定,Entity Framework 到底值不值得我押在上面投資五年開發計劃使用的 ORM 技術? 它跟 NHibernate (考慮中的另一項 ORM framework) 的優缺點為何? 未來發展的優缺點又是什麼? 架構上的差異在那? 另外 Linq to SQL 呢? 這些較偏架構性跟本質的討論及比較資訊,反而少之又少...。

雖然最後還是研究了些心得出來,不過實在是不想寫那些到處都看的到的實作,就來寫點不一樣的吧。第一篇會先寫寫 ORM 的背景知識,還有 Entity Framework 跟 C# 的語法是如何魚幫水,水幫魚,如何解決了過去 ORM 用起來都不大對勁的問題...。

 

繼續長篇大論前,先老王賣瓜一下。雖然我碰過的 ORM framework 不多,不過相關的理論跟技術則碰了不少。撇開大學就在研究的 OOP 不談,研究所的指導教授就從 SmallTalk 開使教... 兩年的專題研究都是 OODB (物件導向資料庫),相關論文也看了一堆。出來工作後又有幸用了幾年的 TAMINO (一套 native xml database), 之後又花了很多時間,在 SQL 2000 上面建立起一套 Object <-> XML <-> Database,類似 ORM 的 Framework ...

不過這麼一路下來,都沒有覺的簡單又可行的方案。除了上面講的是我親自參與過的之外, M$ 其實也發表過幾個類似的技術,像是 Typed DataSet 就是個較接近的產物。 Typed DataSet 其實有點接近現在的 Entity Framework 了。DataSet 就等同於 Entity Container / ObjectContext, DataTable 大致就等同於 EntitySet, 而 DataRow 則等同於 Entity, Relation 則大致等同於 Entity Framework 的 Navigation Property.... 不過用起來還是到處都看的到 DataSet 的影子,感覺血統還不夠純正...

不過現在的 Entity Framework 不一樣了,感覺就已經往實用的領域邁進了一大步! 並不是說 Entity Framework 做的很好 (以成熟度來說, NHibernate 比目前的 Entity Framework 好的多), 而是跟 Entity Framework 搭配的技術都成熟了。一套 ORM 要成功,必要的條件很多啊... 實作上的角度看來,我覺的重要的有這幾項:

  1. 要有優良的 Object / Relationship Mapping 機制、作法、工具等等
  2. Framework 本身的擴充及自訂的能力要夠
  3. 要有效的解決以物件角度思考的查詢 (QUERY) 問題
  4. "物件" 要看起來像 "物件",不是 "資料"
  5. 處理資料庫典型的問題,效能跟便利性不能跟直接操作 DB 差太多
  6. 你的牌子夠不夠響亮... (這是心理因素而已... 哈哈)

這些是深切的體認。不然的話 ORM 的東西跟本不難啊,以功能來說,Typed DataSet 其實就解決一大半實際的問題了。先來看看物件導向幾個關鍵的核心技術是啥?

  1. 封裝 ( Encapsulation ),抽像化型別 (ADT, Abstract Data Type),資料 (Data) 跟行為 (Behavior) 能夠綁在一起
  2. 繼承 ( Inheritance )
  3. 多型 ( Polymorphism )

以這樣 "物件" 的關點來看,M$ 在 Entity Framework 之前的資料庫技術其實都不合格。先來看看資料庫存取技術,如果能搭配這些物件技術,能有什麼樣的改進?

[封裝]
這就沒啥好講的了。物件技術有很好的封裝機制,public / protected / private 等等 scope modifier 就能提供很棒的封裝機制。不過資料庫很難做的好,資料庫的那套頂多叫作安全機制 (security) 或是授權機制 (authorization), 不是封裝 (encapsulation) ... 真正的封裝不是看你是那個帳號決定能不能讀資料? 而是你是那個 SCOPE 的程式,能不能存取封裝起來的內部資訊。DBMS 對於資料的控制能力很有限,不外呼 Key, Constraint, Relationship / Foreign Key 等等。像加解密,正規運算式 (regular expression) 等等,對 DBMS 來說就太複雜了。更複雜的封裝機制單靠 DB 就很不實際... 無奈在沒有 ORM 的前題下,這些問題則是直接曝露在你的程式碼每個地方...。換句話說,如果 ORM 能提供良好的封裝機制,ORM 就能取代掉目前的 Data Access Layer ,成為 APP 存取 DBMS 的主要 API 。

順便吐個苦水,也因為 DBMS 對於資料的控制能力有限,維護的 APP 總是碰到這種問題,就是錯誤的資料總是有辦法鑽進資料庫裡面。不為什麼,只因為 DBMS 本身擋不下來,而 Data Access Layer 又不夠爭氣到足以扛下這重責大任,最後只能靠 APP 自身的開發人員,靠紀律跟自律,還有良心來作好這層把關的動作... -_- 如果有套像樣的 ORM 能夠卡在這個位置,光是資料內容的把關,就是一大進步了。

 

[繼承]
繼承跟資料庫有什麼關係? 其實 ORM 如果能有效的把繼承的功能跟資料庫整合起來,也是很嚇人的。舉例來說,部落格支援文章,相片等等不同的內容,但是它們都要有一致的抽像行為,如新的內容要能夠訂閱 (rss subscription),要能夠有標簽 (tagging) 等等共通的功能,在物件技術我們會很直覺的用繼承來做到。定義 BlogContent 類別,把這些邏輯擺上去。之後再分別衍生出 ArticleContent / PhotoContent 等類別,把差異的實作補上去就完成了。不過同樣的概念別想直接套用在資料庫上,你的腦袋得負責這兩者之間的對應。

懂的這麼多的工程師很難找啊... 去那裡找這種人來寫 APP ? 其實搞懂這些也不難,C++ / C# 在解決這類問題,只是很簡單的利用到 virtual table 就搞定了。換到 DBMS,就把 virtual table 的資料結構套到 database schema 就可以。不過說來簡單,能夠搞懂這些,還能精確的實作出來的人不多... 真的作出來還會被嫌:

"它ㄨ的,誰設計的 table schema ? 亂切一通害我 T-SQL query 這麼難寫..."

嗯,沒事,藉機吐吐苦水。主要要表達的就只有一個,繼承關係要對應到資料庫上面,也是挺麻煩的一件事。Entity Framework / NHibernate 就都提供了三種對應的方法。這三種切割對應的方式,要選那一種? 這又是門學問... Orz, 以後再說。

 

[多型]
這個就更玄了。多型是建立在繼承的基礎上,不管你是什麼類別的 instance, 多型的機制可以在父類別的角度,對所有各種衍生類別的物件,一視同仁的操作。而在這統一的前題下,每種類別的物件又可以一國兩制的各自為政... (咳,這不是政治版...)。這樣的抽像程度就是資料庫遠遠所不及的。延序前面講的部落格內容的例子,你能想像這個 store procedure 該怎麼寫嗎?

"要寫一個 sp_update_blogcontent 的 store procedure, 如果 ID 指向的是 blog, 則要執行更新 HTML 的 code,如果是 photo, 則要更新存放圖檔的 BLOB 欄位..."

天那,在 DB 這個層次,寫這種 CODE 只能用很醜的 IF  ELSE 一層一層堆起來...,跟物件技術比起來,程式碼的描述及抽像化能力實在差太遠,在這層次能解決的問題複雜度很有限...。你如果是個聰明人,最好還是別在 DBMS 搞這些物件技術,會死人的...。比較好的作法是移到 APP 那層去作比較實際。

不錯,ORM 存在的原因又多了一個...。

 

所以,再回頭來看看,ORM真的要發揮它的效益的話,絕對不是只有用 "物件" 來代替 "資料" 而已 (還是老話一句,這樣的話用 Typed DataSet 就夠了)。至少對應出來的 "物件",還能有效的應用到這些物件導向的特性,同時 ORM framework 還能替你維持這些跟資料庫的對應關係,這樣 ORM 技術才能真正發揮它的效益,那些被講到爛的三層式架構才不會在 DBMS 這層就破功了。

來看看比較具體的部份。這些物件技術的特點,C# 早在 2000 年,JAVA 早在 199X 年就有了,沒什麼了不起。不過當年的 ORM 實在難用的很。當時的 OOPL 就是缺了些東西,ORM 的程式寫起來限制一堆... 對應到資料庫的物件,用起來就是跟一般的物件差很多,這也不能用,那也不能用。

現在的 C# 就不一樣了,進化到足以解決這些語言的限制。來看看:

  1. reflection, attribute:
    這個讓物件 (Entity) 可以寫的更簡潔,reflection + attribute 可以解決很多過去得繞一大圈才做的到的事。如 class 對應到那個 table, property 對應到那個 column 等等。
  2. partial class:
    ORM 免不了有些 code generator 的搭配。有 partial class 可以讓程式搭配 code generator 更容易一點。
  3. extension methods:
    現有的物件技術很難讓你在現有的 class 上去作擴充,而 extension methods 可以。
  4. Linq
    這可是一大進步。過去 ORM 的目的就是想把 DB 的細節藏起來,無奈碰到 QUERY 的話什麼都藏不住,往往淪落到要嘛都直接用 SQL, 不然就是只剩幾種基本的 API 可以用,無法完全取代直接用 SQL 的查詢。不過 Linq 出現之後這情況就改觀了,雖然像報表那樣複雜大型的 QUERY 還是得直接下 SQL,不過一般 AP 內的查詢都可以直接用 Linq 搞定了

其它當然還有別的,不過我自己覺的這幾點是關鍵,至少可以讓現在的 Entity Framework 在使用 Entity 時,不會再跟一般的物件有什麼不同。大部份你可以應用在一般物件的技巧,也都能套用在 Entity 身上。第一篇碎碎唸的部份就先寫到這裡。後面會示範一下幾種打造你專屬的 Entity 用到的技巧。想看後面的讀者們請耐心等待 :D



1/20/2009 2:16:16 AM

難搞的 Entity Framework - 跨越 Context 的查詢

Microsoft.NET | C# | Entity Framework | ORM

咳,沒錯... 兩個月都沒寫什麼東西出來,就是都在研究 Entity Framework 跟 Enterprise Library... Enterprise Library 倒還好,看看範例,看看 Key Scenario 大概就能入門了,不過 Entity Framework 就沒這麼簡單...

M$ 對 Entity Framework 的 Roadmap 規劃的很大,不過再怎麼樣附在 .NET 3.5 SP1 的也還只是第一版而已。背負著龐大的架構,而現在卻還不一定能拼過其它成熟的 ORM solution, 這就是辛苦的地方。

先抱怨一下它的設計工具... 好用是蠻好用的 (跟直接寫 XML 檔相比),不過小問題還不少。像是 TABLE 拉進去,刪掉再拉進去,就有機會 build 失敗... VIEW 拉進去沒辦法明確的指定 KEY 是那個欄位,不是結果不對就是 build 失敗,最後忍不住還是得去手動改 .edmx .. 這些事件老實說這兩個月也碰了不少 @_@

不過撇開工具的問題,Entity Framework 的設計還真是不錯。其它的心得就改天再講,先講困擾我最久的,也是大部份 ORM 的通病 - 大型 database 的問題。

這裡指的 "大型" 不是指資料筆數很多,是指 schema 很複雜的情況。大型的 AP 用到上百個 table + view 是很常見的,尤其是隨著改版,舊 table 沒刪掉,新的 table 又一直加,那還真是恐佈。所有的 ORM 都需要某種型態的 O / R Mapping, 不是寫設定檔,就是有視覺化的設計工具。不過... 你能想像一張有 500 個 table 的 ER-MODEL 嗎? 

要避免過大的單一 OR Mapping 設定,就只有做適度的切割了。在 Entity Framework / Visual Studio 2008 ,這點很容易做到,分成多個 .edmx 檔就可以了。不過問題會在後面,分開多個 .edmx 有幾個缺點:

  1. 會有多個 ObjectContext 產生,每邊都有物件要更新時,每個 ObjectContext 都得呼叫 SaveChange..
  2. 不論是 LINQ to Entities 或是 eSQL, 想要 join 橫跨在多個 .edmx 的資料時,會得到無情的錯誤... 不支援跨 context 的操作
  3. AssoicationSet 無法跨越 context 的範圍,意思是跨 .edmx 的 Entity, 不能靠 Navigation Property 來處理。

解決的方式當然也有,也查了 ADO.NET Team Blog,這兩篇是所有 GOOGLE 到的相關文章裡,講這問題講的最深入的..

有碰到這問題的,這兩篇一定要看一看。其實文章內探討的問題已經超過我的需求了,我只是要解決我面臨到的問題:

  1. 因應模組化需求,我需要把 .edmx 跟其相關的邏輯封裝在各別的 assembly
  2. 不同的模組間定義的 EntitySet 能夠用 eSQL 做 JOIN 的查詢
  3. 最基本的 LINQ 也不能少
  4. 新增新的模組時,其它模組不需要重新編譯

老實說這兩篇沒解決到我的問題,只不過瞄到了 Part 2 有這麼一段話:

9. At runtime, you could create either one Context that works with both the schema sets or two different contexts. To create a single context with both the schema sets, you would use the ObjectContext constructor that takes in an EntityConnectionString. In the Metadata parameter of the connection string, specify the paths to both sets of files.

哈哈,運氣不錯,關鍵的一段話沒有漏掉... 就這段話解決了我一個多月以來的困擾。Entity Connection 用的連接字串又臭又長,包括了你的 .csdl, .ssdl, .msl 三份定義檔在那裡,還有底層用的 database connection string. 湊起來一大串,像這樣:

medadata=res://*/Model1.csdl|res://*/Model1.ssdl|res://*/Model1.msl;............

而從這兩篇文章,除了各種 split / reuse 這堆設定檔的方法及優缺點之外,就是學到原來 entity connect string 可以併入多組對應檔啊 :D,像這樣:

medadata=res://*/Model1.csdl|res://*/Model1.ssdl|res://*/Model1.msl|res://*/Model2.csdl|res://*/Model2.ssdl|res://*/Model2.msl;............

沒錯!! 說穿了不值錢,就是把 entity connection string 這樣改一改就好了,eSQL 就可以透過單一 object context 就能存取兩份 .edmx 內定義的內容, Linq 則要自己用 CreateQuery< >() 方式來產生 EntitySet.. 其它就沒有什麼太大不同了。

總算搞定這個大問題!! 收工! 其它的應用就改天有空再慢慢貼了 :D 感謝各位在這兩個月枯水期還沒取消訂閱我的 BLOG ... :D



1/16/2009 2:10:00 AM

RUN!PC 精選文章 - 生產線模式的多執行緒應用

543 | C# | RUNPC | Threading | 我的作品 | 技術隨筆 | Microsoft.NET | [精選文章]

http://www.runpc.com.tw/content/main_content.aspx?mgo=178&fid=E08

無意間 search 我自己的名字,才發現這篇文章除了投稿到 RUN! PC 之外,原來還有刊在網站上的精選文章啊...

哈哈,暗爽一下,順道貼一下 link, 讓沒看到雜誌的網友們也有機會看一看在下的作品...



12/10/2008 11:30:18 PM

XmlWellFormedWriter.WriteRaw( ) 的 Bug 後續發展

Microsoft.NET | 543 | C# | MSDN | TROUBLE SHOOTING | XML | 技術隨筆

一時順手,就按下 Visual Studio 2008 上面的 [Report Bug] 回報上一篇發現的 Bug, 沒想到 M$ 真的有回應耶... :D

反正 M$ 在 connect 裡的回應本來就公開的,我就順手貼一下:

 

https://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=386899&wa=wsignin1.0

 

Hello,
Originally the WriteRaw method was designed for XmlWriters that are formatting text XML into a file. In those cases the WriteRaw method can be used to write out an XML fragment that is already formatted and checked for well-formedness. It can also be used for writing text nodes with special character that have been already escaped and no further processing of the text is needed.
However, when we introduced the XmlWriter over XmlDocument/XDocument (accessed via XPathNavigator editing methods), the use of the WriteRaw method on top of XmlDocument became controversial. We had two options:
1.    Threat it as a text
2.    Parse it into nodes


The second option is very difficult (if not possible) to do. The XML fragment can we written out in multiple WriteRaw calls, so we could not assume that a single WriteRaw will contain a fully enclosed fragment. It can also be interleaved with other XmlWriter calls and nested many times – overall a very hard thing to implement properly. So that is why we have decided to treat the WriteRaw content as text, which is what you are seeing.
If you have an XML fragment in a string and you want to append it to XmlDocument, you can do it like this:


                XmlDocument doc = new XmlDocument();
                XmlElement rootElement = doc.CreateElement("root");
                rootElement.InnerXml = "<a/><a/><a/><a/><a/>";
                doc.AppendChild(rootElement);


Or if you really want to use the XmlWriter from XPathNavigator.AppendChild(), you can create an XmlReader over your fragment and use the WriteNode method on the XmlWriter:


                XmlDocument doc = new XmlDocument();
                XmlWriter writer = doc.CreateNavigator().AppendChild();
                writer.WriteStartElement("root");
                using (XmlReader r = XmlReader.Create(new StringReader("<a/><a/><a/><a/><a/>"), new XmlReaderSettings { ConformanceLevel = ConformanceLevel.Fragment } ) ) {
                    writer.WriteNode(r, false);
                }
                writer.WriteEndElement();
                writer.Close();
                doc.Save(Console.Out);


I hope this helps.
Thank you,
-Helena Kotas, System.Xml Developer

 

看來 M$ 認為這是權宜之計,不算 BUG,要 USER 就直接避掉了,只是他建議的解法剛好就是我上一篇用的,用 XmlReader, 搭上 ConformanceLevel.Fragment 的設定解決掉...

只不過,這種情況,不是應該丟出 NotSupportException 比較好嘛? 幹嘛拿個不適當的實作填進來?

 

提外話,現在 WEB 2.0 的時代,M$ 工程師除了寫 CODE 之外,也要負責回客戶的問題了? 真辛苦...






精選文章

RUN! PC 文章及範例下載
2010/07. 結合檔案及資料庫的交易處理
2010/05. TxF讓檔案系統也能達到交易控制
2010/04. 生產者 vs 消費者 - 執行緒的供需問題
2008/11. 生產線模式的多執行緒應用
2008/09. 用ThreadPool發揮CPU運算能力
2008/06. SEMAPHORE在ASP.NET的應用
2008/04. 以ASP.NET開發同步WEB應用程式

如何學好 "寫程式" 系列
#1. 該如何學好 "寫程式" ??
#2. 為什麼 programmer 該學資料結構 ??
#3. 進階應用 - 資料結構 + 問題分析
#4. 你的程式夠 "可靠" 嗎?

#5. 善用 TRACE / ASSERT

安德魯是誰?

Andrew Wu | Create Your Badge

我喜歡鑽研物件導向、軟體工程及作業系統等相關技術。我會在這裡發表我的研究心得,也當作我自己的學習筆記。


Recent comments

Comment RSS