1. 程式人生 > >[ASP.NET MVC 小牛之路]06

[ASP.NET MVC 小牛之路]06

在家閒著也是閒著,繼續寫我的[ASP.NET MVC 小牛之路]系列吧。在該系列的上一篇博文中,在顯示書本資訊列表的時候,我們是在程式程式碼中手工造的資料。本文將演示如何在ASP.NET MVC中使用Entity Framework從資料庫中獲取資料。雖然本文題目聽上去比較簡單,但如果你認真閱讀,相信你一定會有所收穫。

本文目錄:

ORM 和 EF

當我們要開發一個應用程式,就要考慮怎樣展示資料,怎樣持久化資料。考慮這個問題時我們所要關心的東西,最重要的莫過於程式的效能、開發的簡易性和程式碼的可維護、可擴充套件性。

持久化(Persistence),是指在應用程式中能永久地儲存各個處理狀態資訊的機制。如果沒有持久化這個機制,狀態只能儲存在記憶體中,機器關機後就會丟失。

在應用程式中,當要永久地儲存資料時,我們會選擇關係資料庫(Relation DataBase);當要臨時地儲存資料時,我們則使用儲存在記憶體中的物件。目前對於大多數開發人員來說,都是用關係資料庫技術來作為持久化機制。雖然現在有些人正在嘗試使用物件資料庫(Object DataBase)技術,但關係資料庫技術在許多年以內依然會是最主要的持久化機制。

為什麼會出現物件資料庫? SQL語言是一種非過程化的面向集合的解釋型語言,而很多高階語言是過程化的面向物件的編譯型語言,這使得兩種語言之間存在著不匹配,導致效率不如人意。這種不匹配被稱為“阻抗失配”,物件資料庫的出現就是為了解決“阻抗失配”。

我們知道,在關係資料庫技術中是用Table以行和列的結構來存放和組織資料的。在.NET 2.0以前,C#還沒有泛型的時候,人們基本上用填充在DataSet中的DataTable來對映並存放從關係資料庫中查詢出來的資料,正如下面程式碼所示:

using (SqlConnection conn = new SqlConnection(connString)) {
    using (SqlDataAdapter da = new SqlDataAdapter("Select * from order", conn)) {
        DataTable dt = new DataTable();
        da.Fill(dt);
        ...
    }
}

這種方式雖然能讓面嚮物件語言匹配關係資料庫,但它有一些明顯的缺點,如非型別安全、難操控、低效能等。從.NET 2.0開始,人們開始通過泛型技術用實體模型物件的集合來匹配關係資料庫中的資料,這種方式解決了DataTable方式所面臨的缺點,且它有強型別、在VS中自動完成、編譯時檢查等特點,廠受.Net開發人員的喜愛。

為了讓開發人員不用手動去做這種“匹配”工作,人們研發了很多ORM工具(如Entity Framework、NHibernate等)。ORM(Object Relation Mapping)工具,顧名思義,它的角色就是為了解決“關係”和“面向物件”之間的“失配”,它可以使得開發人員不用過多關心持久層而可以花更多的時間專注於業務。

Entity Framework(EF)是微軟以ADO.NET為基礎所發展出來的ORM解決方案,Entity Data Model(EDM) 為主。EF利用了抽象化資料結構的方式,將每個資料庫物件都轉換成應用程式中的類物件(Entity),而資料欄位都轉換為屬性 (Property),關係則轉換為結合屬性 (Association),讓資料庫的 E/R 模型完全的轉成物件模型,如此讓開發人員就能用熟悉的面向物件程式語言來呼叫訪問。EF 4.0 以後支援Database FirstModel FirstCode First三種生成模式,Code First模式用的人比較多。

概念和理論性的東西就不多講了,我也是帶著疑問去查詢網路資源根據自己的理解半摘半歸納出來的,而且鄙人不才,很多東西想描述但都不知道怎麼組織語言。

本文目的主要是讓讀者對EF有個感性的認識,然後瞭解EF在ASP.NET MVC中的應用,不會去研究原理性的東西,下次有空再單獨介紹EF吧。

使用Entity Framework

接著上一篇博文[ASP.NET MVC 小牛之路]05 - 使用Ninject,我們現在要把程式碼中手工造的資料改成從資料庫讀取。為此,我們先準備下資料庫。本示例使用的是MS SQL Server,使用其他資料庫也是一樣的。先建立一個名為BookShop的資料庫,然後執行下面的指令碼建立一個Books表:

CREATE TABLE Books 
( 
    [ID] INT NOT NULL PRIMARY KEY IDENTITY, 
    [Title] NVARCHAR(100) NOT NULL,
    [Isbn] VARCHAR(20) NOT NULL, 
    [Summary] NVARCHAR(1000) NOT NULL,
    [Author] NVARCHAR(50) NOT NULL,
    [Thumbnail] VARBINARY(MAX),  
    [Price] DECIMAL(16, 2) NOT NULL,
    [Published] DATE NOT NULL,
)

 然後隨便在表中加幾條用於測試的資料:

接下來我們就要讓應用程式連線資料庫了。由於上一篇博文是我在公司用休息的時間寫的,公司的電腦裝的是VS2010,家裡的筆記本裝的是VS2012,所以得重新把上篇博文的示例移到VS2012上,對於本示例,VS2010和VS2012都是一樣的。上一篇示例專案的目錄結構如下:

 

本文的示例將在上篇的這個示例基礎上繼續。

用NuGet在BookShop.Domain工程中安裝Entity Framework包,方法請參考本系列的上一篇文章。

在BookShop.Domain工程的Concrete資料夾中新增一個名為EFDbContext的類,程式碼如下:

public class EFDbContext : DbContext { 
    public DbSet<Book> Books { get; set; } 
}

使用EF Code First第一步就是建立一個繼承自System.Data.Entity.DbContext的類,這個類將為資料庫中的每個表定義一個屬性,屬性的名稱代表資料庫中的表名。DbSet作為返回型別,它是用於生成CRUD(Create、Read、Update和Delete)操作的裝置,對映資料庫表的行。

我們需要在BookShop.WebUI工程中的web.config配置檔案中新增資料庫的連線字串,來告訴EF怎樣連線資料庫。根據自己機器上的資料庫配置連線字串如下:

<connectionStrings>
  <add name="EFDbContext" connectionString="Data Source=.\SQLEXPRESS;Initial Catalog=BookShop;User ID=sa;Password=sa" providerName="System.Data.SqlClient" />
</connectionStrings>

接下來,我們把BookShop.Domain工程下Concrete檔案中的BookRepository類檔案改造一下,把程式碼中手工造的資料改成從資料庫讀取,以測試應用程式是否可以正常連線資料庫。修改後的BookRepository類如下:

public class BookRepository : IBookRepository {
    private EFDbContext context = new EFDbContext();

    public IQueryable<Book> Books {
        get { return context.Books; }
    }
}

在我們的這個倉儲類中,我們改使用EF,通過建立一個EFDbContext類的例項來獲取資料庫中的資料。如你所見,我們不需要自己寫ADO.NET程式碼去連線和讀取資料庫,非常簡潔明瞭,我們就是這樣使用Entity Framework的。我們來看一下執行效果吧:

到這我們已經成功使用EF連線上了資料庫,並從資料庫中讀取出來了資料。我們還可以通過Linq進行非常靈活的查詢,就像寫SQL一樣。比如要查詢價格在100元以下的前10條記錄,並且按價格從低到高顯示,那麼我們可以在BookShop.WebUI工程下的BookController中的List方法中這樣寫:

public ViewResult List() {
    return View(repository.Books
        .OrderBy(b => b.Price)
        .Where(b => b.Price < 100)
        .Take(10));
}

或許你很快就會對EF獲取資料庫的方式產生這樣的疑問:EF從資料庫中讀取整個Books表的資料到記憶體,然後返回給呼叫者(上面程式碼中的repository.Books)用Linq語句過濾使用者想要的前10條資料,如果Books表中有幾百萬條資料,那記憶體豈不是完蛋了,EF不會這麼傻吧?EF會不會根據Linq查詢語句智慧地生成SQL文字再到資料庫中去查詢資料呢?這裡就要講講IQueryable和IEnumerable了。

IQueryable 和 IEnumerable

其實,對於上面的即有過慮又有排序的條件查詢Linq語句,EF是讀取資料庫中整個Books表中的資料到記憶體,還是根據Linq查詢語句智慧的生成SQL再執行查詢,完全編碼者來決定的。我們開啟BookShop.Domain工程的BookRepository類檔案,請注意該類中Books屬性的返回型別:

...
public IQueryable<Book> Books {
    get { return context.Books; }
}

上篇博文中,我們對使用IQueryable作為返回型別提了個疑問:為什麼IQueryable而不用IEnumerable作為返回型別?答案是:使用IQueryable,EF會根據呼叫者的Linq表示式先生成相應的SQL查詢語句,然後到資料庫中執行查詢,查詢出來的資料即是使用者想要的資料;而使用IEnumerable,Linq表示式的過濾、排序等操作都是在記憶體中發生的,即EF會先從資料庫中把整個表的資料查詢出來放在記憶體中,然後由呼叫者使用Linq語句進行過濾、排序等操作。是不是這樣呢?我們來監視一下兩種情況EF生成的SQL語句就知道了。

我們先來看看使用IQueryable的情況。重新執行一下程式,然後使用SQL Server Management Studio的活動和監視器檢視一下我們的BookShop應用程式所執行的SQL語句,結果如下:

結果證明使用IQueryable,EF是先根據Linq表示式生成相應的SQL語句再執行查詢的。

我們再稍稍修改一下程式碼來看看用IEnumerable的情況。BookRepository類修改如下:

public class BookRepository : IBookRepository {
    private EFDbContext context = new EFDbContext();

    public IEnumerable<Book> Books {
        get { return context.Books; }
    }
}

當然BookRepository類所實現的IBookRepository介面(在BookShop.Domain工程的Abstract資料夾中)也要改一下:

public interface IBookRepository {
    IEnumerable<Book> Books { get; }
}

再重新執行一下應用程式,用活動和監視器檢視最後執行的SQL語句如下圖:

我們看到改用IEnumerable後,EF生成的SQL沒有任何過濾、排序等的操作,它一次把表中的所有資料都Select出來,和上面寫的Linq表示式一點都沒關係。

IQueryable雖然可以很智慧地根據Linq表示式生成相應的SQL語句,但畢竟有一個分析Linq表示式的過程,相對來說效能比IEnumerable要差。那麼我們什麼時候用IEnumerable,什麼時候用IQueryable呢?我想,對於少量的資料(比如從資料庫中讀取應用程式相關的系統資訊)和不需要對資料進行過濾操作的情況,用IEnumerable比較適合;對於資料量較大需要對資料進行過濾(比如分頁查詢)的情況,則用IQueryable比較合適。