2010年4月28日 星期三

Entity Framework: 新增關聯資料的方法

使用 Entity Framework 新增一筆資料是極其簡單的事。然而建立相關聯的資料呢?也相當簡單!以下介紹常見的狀況。

DataSchema

在這個範例中,使用最簡單的 master-detail 的資料表。一個是 Order,一個是 OrderDetail。Sql Script 如下

CREATE TABLE [dbo].[Order](
    [OrderId] [int] IDENTITY(1,1) NOT NULL,
    [OrderDate] [datetime] NOT NULL,
 CONSTRAINT [PK_Order] PRIMARY KEY CLUSTERED 
(
    [OrderId] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON)
)
go
CREATE TABLE [dbo].[OrderDetail](
    [DetailId] [int] IDENTITY(1,1) NOT NULL,
    [OrderId] [int] NOT NULL,
    [ProductName] [nchar](50) COLLATE Chinese_Taiwan_Stroke_CI_AS NOT NULL,
 CONSTRAINT [PK_OrderDetail] PRIMARY KEY CLUSTERED 
(
    [DetailId] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON)
)
go
ALTER TABLE [dbo].[OrderDetail]  WITH CHECK ADD  CONSTRAINT [FK_OrderDetail_Order] FOREIGN KEY([OrderId])
REFERENCES [dbo].[Order] ([OrderId])
GO
ALTER TABLE [dbo].[OrderDetail] CHECK CONSTRAINT [FK_OrderDetail_Order]
GO

使用EDM designer 產出的 Model 如下圖image

範例一

以下是最簡單的方法,也是基本上我認為這樣的寫法最適當

class Program
{
  static void Main(string[] args)
  {
    AddData1();
    ShowData();
  }

  private static void ShowData()
  {
    using (Database1Entities context = new Database1Entities())
    {
      var orders = context.Orders.Include("OrderDetails");
      foreach (var order in orders)
      {
        Console.WriteLine("order id = {0}", order.OrderId);
        foreach (var d in order.OrderDetails)
          Console.WriteLine("detail prod={0}", d.ProductName);
      }
    }
  }

  private static void AddData1()
  {
    using (Database1Entities context = new Database1Entities())
    {
      var order = new Order() {
        OrderDate = new DateTime(2010, 4, 12)
      };

      var detail = new OrderDetail {
        ProductName = "Prod1"
      };
      order.OrderDetails.Add(detail);
      context.Orders.AddObject(order);
      context.SaveChanges();
    }
  }
}

要注意的是,我們使用了 order.OrderDetails.Add(detail) 將order 與 detail 的關聯建立起來。接下來使用 context.Orders.AddObject(order) 將 order 新增到 context 中,因此當 context.SaveChanges() 時,context 會查覺到 order 是新增的資料,才會將資料寫到資料庫中。

這樣的方法相當完美,原因在開發人員只需關注物件模型的關係即可,不必知道物件模型關聯的 key 是哪一個 property。

範例二

下面的方法也可以執行。

private static void AddData2()
{
  using (Database1Entities context = new Database1Entities())
  {
    var order = new Order()
    {
      OrderDate = new DateTime(2010, 4, 12)
    };
    context.Orders.AddObject(order);
    context.SaveChanges();

    var detail = new OrderDetail
    {
      OrderId = order.OrderId,
      ProductName = "Prod1"
    };
    context.OrderDetails.AddObject(detail);
    context.SaveChanges();
  }
}

與範例一不同的是,範例二並不強調 order 與 detail 的物件關聯,而是直接指定 detail 的 OrderId。因此order 與 detail 是分開寫入資料庫的。

範例二就是比較差的寫法了,原因是在開發人員需要了解關聯的property 為何,此例為 OrderDetai 的 OrderId 關聯到 Order 的 OrderId。長久下來,開發人員會習慣以 Data driven 的寫作方式看待 Entity Framework,故不建議這樣做。

範例三

範例三與範例二大致相同

private static void AddData3()
{
  using (Database1Entities context = new Database1Entities())
  {
    var order = new Order()
    {
      OrderDate = new DateTime(2010, 4, 12)
    };
    context.Orders.AddObject(order);
    context.SaveChanges();

    var detail = new OrderDetail
    {
      Order = order, //這一行換了
      ProductName = "Prod1"
    };
    context.OrderDetails.AddObject(detail);
    context.SaveChanges();
  }
}

範例三與範例二的概念相同。原來範例二中 detail 是指定 OrderId ,而範例三中 detail 改指定 Order 為 order

只增加 Detail

如果已經知道 master 的 primary key,只想新增 detail 的資料呢?下面的範例四是可以執行的。

範例四

static void Main(string[] args)
{
  AddData1();
  AddNewDetail1();
  ShowData();
}

private static void AddNewDetail1()
{
  using (Database1Entities context = new Database1Entities())
  {
    var order = context.Orders.Where(o => o.OrderId == 1).First();
    var detail = new OrderDetail
    {
      ProductName = "Prod2"
    };
    order.OrderDetails.Add(detail);
    context.SaveChanges();
  }
}

因為物件模型的關係,範例四的做法相當直覺,也相當多的開發人員這樣寫。但是,我們已經知道了 OrderId 為1,範例四會先載入 OrderId 為1的資料並載入記憶體,再進行後續的新增detail 動作。這樣一來,反而效能不彰。可不可以像以前一樣,直接新增一筆 OrderDetail 就好了,不需要載入 Order 啊?

下面的範例五可以在不載入 master (即 order) 到記憶體的情況下增加 detail 資料。

範例五

static void Main(string[] args)
{
  AddData1();
  AddNewDetail();
  ShowData();
}

private static void AddNewDetail()
{
  using (Database1Entities context = new Database1Entities())
  {
    var detail = new OrderDetail
    {
      OrderId = 1, //指定了 master 的 key
      ProductName = "Prod2"
    };
    context.OrderDetails.AddObject(detail);
    context.SaveChanges();
  }
}

結論

在 Entity Framework 中,關聯資料的寫法可以有相當多種,但其中的差別需要開發人員注意哦!

2010年4月27日 星期二

Linq to Xml: 寫出 XDeclaration 的資料

當以 linq to xml 的方式組 xml 是非常輕易的事情。但在下面的程式碼,想要輸出 xml declaration 卻碰到了軟釘子

錯誤寫法

   1: static void Main(string[] args)
   2: {
   3:   string xml = "<r><a>3</a></r>";
   4:   XDocument doc = new XDocument(
   5:     new XDeclaration("1.0", "big5", null),
   6:     XElement.Parse(xml));
   7:   Console.WriteLine(doc.ToString());
   8: }

輸出如下所述,並沒有包含預期的<?xml version="1.0" encoding="big5"?>

   1: <r>
   2:   <a>3</a>
   3: </r>

輸出到文字檔

為什麼呢?但如果改成下面的程式輸出到文字檔,在檔案內容就是正確的。

   1: static void Main(string[] args)
   2: {
   3:   string xml = "<r><a>3</a></r>";
   4:   XDocument doc = new XDocument(
   5:     new XDeclaration("1.0", "big5", null),
   6:     XElement.Parse(xml));
   7:   doc.Save(@"c:\temp\a.xml");
   8: }

StringWriter

我們也可以改用 StringWriter 寫出。如下例

   1: static void Main(string[] args)
   2: {
   3:   string xml = "<r><a>3</a></r>";
   4:   XDocument doc = new XDocument(
   5:     new XDeclaration("1.0", "big5", null),
   6:     XElement.Parse(xml));
   7:   var writer = new StringWriter();
   8:   doc.Save(writer);
   9:   Console.WriteLine(writer.ToString());
  10: }

輸出如下

   1: <?xml version="1.0" encoding="utf-16"?>
   2: <r>
   3:   <a>3</a>
   4: </r>

使用 StringWriter 的特性更怪,它會視XDocument 的內容來覆寫實際的 encoding。由於c# 的 string 都是 unicode,故一律輸出成 utf-16 的編碼。

最後,找到一個較能符合需求的方式。如下

兩段式ToString

   1: static void Main(string[] args)
   2: {
   3:   string xml = "<r><a>3</a></r>";
   4:   XDocument doc = new XDocument(
   5:     new XDeclaration("1.0", "big5", null),
   6:     XElement.Parse(xml));
   7:   Console.WriteLine(doc.Declaration.ToString() +  doc.ToString());
   8: }

使用兩段式的 ToString()方式是蠻奇怪的。但這是我試到最簡單的方法了

CS0012: The type 'System.Data.Objects.DataClasses.EntityObject' is defined in an assembly that is not referenced

在開發 asp.net MVC 並使用 Entity Framework 4.0 時,在執行網頁發生這樣的一個錯誤。

Compiler Error Message: CS0012: The type 'System.Data.Objects.DataClasses.EntityObject' is defined in an assembly that is not referenced. You must add a reference to assembly 'System.Data.Entity, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'.

我有參考 System.Data.Entity 啊!

原來,MVC2 為了相容.net framework 3.5,故無法在 web.config hard code 參考 System.Data.Entity。為了這個原因,我們必須在 web.config 上自行加入參考給 asp.net 使用。

  <system.web>
    <compilation debug="true" targetFramework="4.0">
      <assemblies>
        <add assembly="System.Web.Abstractions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
        <add assembly="System.Web.Routing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
        <add assembly="System.Web.Mvc, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
        <add assembly="System.Data.Entity, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
      </assemblies>
    </compilation>

Entity Framework 的 Bug?

開發應用程式時,編譯錯誤,出現了下面的訊息

Cannot implicitly convert type 'System.Data.Objects.ObjectSet<MyEntity>' to 'System.Linq.IQueryable<MyEntity>'

查了一下 msdn ObjectSet 的說明,的確有繼承 IQueryable<T> 啊?怎麼會無法 convert呢?一時間彷彿之前所學盡錯,不知所以為何如此。

再回到 Visual Studio 2010, 發現錯誤更多了,這次有三個錯誤?我還沒有改程式啊!

原來,Visual Studio 2010 為了效能,編譯時如果發現錯誤,會立即回報,但不會管前因後果。查一下這三個錯誤,發現沒有參考 System.Data.Entity 才是主因。

Error    1    The type 'System.Data.Objects.ObjectContext' is defined in an assembly that is not referenced. You must add a reference to assembly 'System.Data.Entity, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'.   

加上參考後,編譯就成功了。

2010年4月26日 星期一

一堆的新版本出來了。Enterprise Library 5.0, Office 2010, Moss 2010

一個沒注意,一堆新版本就冒出來。

Enterprise Library 5.0 , EL 5.0 on MSDN
Office 2010 及 Moss 2010 則必須到 MSDN subscriptions 下載

這下子,連上次的 Visual Studio 2010,可以讓我忙整年了。

ASP.NET 4.0 的改變(1):ValidateRequest=False 失敗

[ASP.NET 2.0]

為了防止 XSS 攻擊,asp.net 2.0 加上了 ValidateRequest 的設計。這一道防護架在網頁層級(aspx)。每一個 aspx 可以自行調整,如下例

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="WebApp._Default" ValidateRequest="true|false" %>

ValidateRequest 的預設值是true。
如果每一個網頁都要改成 ValidateRequest=”false”, 如果要一個個網頁進行修改,豈不是逼瘋人?將每個網頁的預設值改成false,就必須到 Web.config 修改了。如下例。

<?xml version="1.0"?>
<configuration>
  <system.web>
    <pages validateRequest="false" />
  </system.web>
</configuration>

在 asp.net 2.0 的設定相當的實用,並且防止了新進開發人員不小心的錯誤。除非特意地打開潘朵拉寶盒,惡意的 XSS 攻擊可以說是小case。

[ASP.NET 4.0]

在 ASP.NET 4.0的年代,Request Validation 由網頁層級更提昇到應用程式層級。驗證的方法是在 BeginRequest之前就檢查 XSS攻擊。因此,除了原本的 aspx,現在更包括了所有的 requests,如 ashx, wcf, web service 等所有的 asp.net request。

雖然提昇的防護罩保護範圍,卻也連帶地原本上述的設定開始變的不太一樣。如果單一網頁(例如留言版)不需要檢查 request ,想要自行防護呢?原本 ASP.NET2.0 的設定方式是不足了,因為尚未到網頁層級就被擋掉了。

此時,目前看到的文件,最方便的作法就是設回到asp.net 2.0 的防護方式了。在httpRuntime 上,設定驗證方式為 2.0 的檢式。範例如下

<?xml version="1.0"?>
<configuration>
  <system.web>
    <httpRuntime requestValidationMode="2.0"/><!-- 回到 2.0 的防護方式-->
    <pages validateRequest="false" />
  </system.web>
</configuration>
這樣的方式,雖然很方便地回到了舊的 asp.net 2.0 年代,但也失去了全面防護的美意。
有沒有辦法兩全齊美呢?方法是自訂 Reqeust Validator。

[自訂 ReqeustValidator]

ASP.NET 4.0 更增加了自訂 ReqeustValidator 的功能。範例可參考 Creating your own custom request validation。好處呢?自己訂驗證方式,可以自行控制驗證行為。壞處當然是多出一些程式需要維護了。

2010年4月23日 星期五

Visual Studio 2010: 編譯錯誤,could not create the new file tracking log file

在升級原 solution 到 Visual Studio 2010 後,發現無法 compile 了?出現了下面的錯誤

could not create the new file tracking log file: C:\Projects\?FileTracker : error FTK1011: could not create the new file tracking log file: C:\Projects\?

這是什麼東西啊!為何有個 tracking log file 呢?還出現了一個 ? 號.

這個?號我們應該常遇到,也就是當encoding 設不正確時會出現的 ? 號。因此,我判斷是路徑使用了中文字。

檢查一下,果然是中文路徑的問題,將原目錄中文的「客戶」改成英文的「 Customer」 後,就可以編譯了。

天啊,這樣的問題,微軟還是不小心再犯了。

2010年4月18日 星期日

EF 4: 執行 Sql script

以往上 EF 3.5 時,ObjectContext 只能執行 eintity sql。

在 EF 4.0 時,可以直接執行 T-Sql 語法了。如下範例

   1: static void Main(string[] args)
   2: {
   3:   string sql = "select name from sys.objects ";
   4:   using (ObjectContext context = new ObjectContext("name=db"))
   5:   {
   6:     var names =  context.ExecuteStoreQuery<string>(sql);
   7:     foreach (var name in names)
   8:       Console.WriteLine(name);
   9:   }
  10: }

優點

如果自己已經知道有更好的 sql script 下法,又不想寫成 stored procedure 時,就可以使用這樣的寫法。

缺點

以往認為 Entity framework 或 Linq to Sql 是沒有 sql injection 的問題。這樣一來方便之門一開,牛鬼蛇神又可以暗渡陳倉了。

在上例中,如果要取得 string sql = "select name, database_id from sys.databases" 的結果時,就無法以單一 string 得到回傳值。程式必須增加一個新的類別,以得到回傳值。如下例。

   1: class Program
   2:   {
   3:     static void Main(string[] args)
   4:     {
   5:       string sql = "select name, database_id from sys.databases";
   6:       using (ObjectContext context = new ObjectContext("name=db"))
   7:       {
   8:         var names = context.ExecuteStoreQuery<Result>(sql);
   9:         foreach (Result r in names)
  10:           Console.WriteLine("{0}:{1}",r.Database_Id, r.Name);
  11:       }
  12:     }
  13:  
  14:     class Result
  15:     {
  16:       public int Database_Id { get; set; }
  17:       public string Name { get; set; }
  18:     }
  19:   }

缺點

select 不同數目的欄位,就必須宣告不同的類別(如 Result)以獲得回傳值。果然不是很方便。L2S 就有這樣的工具,那 Entity framework 有沒有呢? EF 4.0 就實作這樣的工具了。

首先,當然需要建立 stored procedure 了

   1: create proc usp_Sample1
   2: as 
   3:     select name, database_id from sys.databases

再來,在 entity data model designer 上按右鍵,執行 Update model from database,並選取剛剛建立的 usp_Sample1

image

打開 Model Browser,在 Model1.edmx/Store/Stored Procedures 下選到剛剛的 usp_Sample1後,按右鍵執行 Add Function Import功能

image

由於我們的回傳值是多欄位的型態,當然是 Complex。選擇 Complex 了,再按 Get Column Information鍵

image

接著按 Create New Complex Type鍵,精靈會幫我們產生符合需求的類別。同時,我們也可以修改類別的名稱。由於是 demo,此時我們不修改。完成後按 ok 鍵.

image

最後修改程式如下。

   1: static void Main(string[] args)
   2: {
   3:   using (var context = new DbEntities("name=MSPetShop4Entities"))
   4:   {
   5:     var names = context.ExecuteFunction<usp_Sample1_Result>("usp_Sample1");
   6:     foreach (usp_Sample1_Result r in names)
   7:       Console.WriteLine("{0}:{1}",r.database_id, r.name);
   8:   }
   9: }

優點

這樣一來,程式少寫了一點,是比較愉快的事。但事實上 code 的行數並沒有減少,只是改使用了 Entity Data Model 所產生的程式碼。要選哪一種呢?當然選後者,因為後者出錯的機會比較少,交給新人時,就更有把握了。

 

結論

在 Entity Framework 3.5 時,常常會拿來與 Linq To Sql (L2S) 作一番功能比較。到了 Entity Framework 4.0 時,無論在 framework 本身或工具上,都能完整地涵蓋原來  L2S的功能。相信下一版的 Visual Studio 出現時,大概 L2S 會被正式地被 deprecate 了.

2010年4月13日 星期二

Visual Studio 2010 RTM release

Visual Studio 2010 出來了!請到 msdn subscription 下載。
等這一天讓我多等了一個月。

下面列出測試版的下載位置,或者您也可以下載免費的 Express 版.

2010年4月9日 星期五

單元測試(6):可測試性2

這個系列,請見
單元測試(1): 什麼是單元測試
單元測試(2): 單元測試的好與壞
單元測試(3): 單元測試的好與壞2
單元測試(4): 單元測試的好與壞3
單元測試(5): 可測試性

需求

今天在 Review 同事的程式時,發現了不可告人的秘密。程式片段如下。

   1: using System;
   2: using System.IO;
   3:  
   4: namespace ConsoleApplication1
   5: {
   6:   class Class1
   7:   {
   8:     public void Do()
   9:     {
  10:       string[] lines = File.ReadAllLines(@"c:\temp\myfile.txt");
  11:       foreach (var line in lines)
  12:       {
  13:         try
  14:         {
  15:           ParseLineAndSaveToDb(line);
  16:         }
  17:         catch (Exception e)
  18:         {
  19:           SendMail(e.Message);
  20:         }
  21:       }
  22:     }
  23:   }
  24: }

用意大概是:讀取客戶上傳的檔案,解析每一行資料後,儲存到資料庫內。當一行資料有錯誤,仍繼續執行匯入。錯誤的訊息以寄信的方式寄給負責人處理。

問題

這段程式看來完美,也合乎客戶/長官的需求。但詭異的是,當我建立Do() 的單元測試後,該單元測試從未發生例外。
天啊,因此這段程式執行起來永遠不會出錯。永不會出錯的程式一定是好程式??

當然紙包不住火的,當程式執行結果不如預期時,就會回過頭來找 bug。這樣一來就很難找了。單元測試建立起來後無法進行自動測試,因為程式沒有出錯的機會,單元測試 3A pattern 中的 Assert 沒有發揮空間。

為了讓單元測試有機會進行實質的 Assert (斷言),我們必須對程式進行修改,而且必須符合客戶需求。修改後的程式如下

   1: using System;
   2: using System.IO;
   3: using System.Collections.Generic;
   4:  
   5: namespace ConsoleApplication1
   6: {
   7:   class Class1
   8:   {
   9:     public List<string> Do()
  10:     {
  11:       string[] lines = File.ReadAllLines(@"c:\temp\myfile.txt");
  12:       List<string> errors = new List<string>();
  13:       foreach (var line in lines)
  14:       {
  15:         try
  16:         {
  17:           ParseLineAndSaveToDb(line);
  18:         }
  19:         catch (Exception e)
  20:         {
  21:           SendMail(e.Message);
  22:           errors.Add(e.Message);
  23:         }
  24:       }
  25:       return errors;
  26:     }
  27:   }
  28: }

注意到Do() 加上了回傳值 List<string>,用來回傳發生的錯誤訊息。單元測試時,就可根據回傳錯誤長度來判斷是否發生過錯誤。

   1: [TestMethod()]
   2: public void DoTest()
   3: {
   4:   Class1 target = new Class1();
   5:   List<string> errors = target.Do();
   6:   Assert.AreEqual(0, errors.Count);
   7: }

結論

當然解法不一定要回傳 List<string> ,回傳一個錯誤發生次數的整數值也可以。重點是「程式的可測試性」。我們寫出的程式必須具備可被測試的特性,來確保程式的品質。

客戶需求是「錯誤發生時,必須仍繼續匯入」,原始的程式已經滿足了客戶需求。但程式的「可測試性」這個需求尚未滿足啊!因此我們再因應「可測試性」的需求再修改程式。

大略地整理需求的分類

  1. 客戶需求:來自客戶原始的商業需求。這一點無庸置疑地一定要滿足。
  2. 品質需求:例如安全性、效能、可管理性等。這一點也廣泛地被接受,但客戶未必關心。客戶未必關心的事,當然有些時候就偷懶啦。
  3. 測試需求:這裡就是我們強調的事情。程式需要具備「可被測試」的特性,尤其是可被「自動化測試」,以利回歸測試的進行。

Share with Facebook