2013年3月31日 星期日

Entity Framework 與 Stored Procedure - 回傳多種資料集

前面兩篇講的是 Entity Framework 與 Stored Procedure 的基本 CRUD 操作,那麼這一篇就來個進階一些的處理,以往直接使用 ADO.NET 執行 Stored Procedure 在遇到回傳多個資料集的時候, 可以使用 DataReader 的 NetResust 方法來處理,通常這種 Stored Procedure 回傳多種資料集會是在 Master - Details 的情況,例如訂單與訂單明細,而換成 Entity Framework 的時候,是不是可以在執行後就可以讓我們直接得到兩種型別的資料呢?

讓我們看下去 ……


操作示範執行環境:
ASP.NET MVC 4 (C#) with Entity Framework 5.0.0, MS SQL Server Express 2008 R2

首先了解一下這個傳回多種資料集結果的 Stored Procedure 的內容:

USE [Northwind]
GO
/****** Object:  StoredProcedure [dbo].[ReturnTwoResult]    Script Date: 03/31/2013 19:51:46 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
 
-- =============================================
ALTER PROCEDURE [dbo].[ReturnTwoResult]
    @OrderID int
AS
BEGIN
    select * from [dbo].[Orders]
    where OrderID = @OrderID
 
    select * from [dbo].[Order Details]
    where OrderID = @OrderID
END

這個內容相當簡單,就是傳入一個 OrderID 然後傳回 Order 主檔以及關連的 OrderDetail 資料,這個在顯示 Master-Details 的資料處理上會經常用到,這個 SP 執行後的結果如下:

image

 

於是就在 EF Model 加入了這一個 Stored Procedure,

image

然後觀察 Mapping Details,就發現到 EF Model 的 Mapping 內容只有對應到第一個傳回的資料型別,也就是只有對應到 Order 而已,並沒有處理傳回的第二種資料集 Order_Details,

image

其實不用真的執行下去就知道只會拿到第一個 Order 的資料而已,但我想應該還是可以有其他的方式來解決。


使用 DbCommand 執行 Stored Procedure 並且搭配 Enterprise Libraray DAAB 的 Row Mapper

其實使用 DbCommand 的方式也就是使用一般 ADO.NET 的操作方式,透過 DbCommand 執行 Stored Procedure,然後以 DbDataReader 承接回傳的結果,先處理 Order 的資料後,再使用 NextResult() 方法,讓 DbDataReader 再處理 Order_Detail 資料。

而會使用到 EntLib DAAB 的 Row Mapper 是為了要將回傳資料對應到類別的處理,我不想再去用一筆一筆對應的老方式做資料的對應,太浪費時間了,而 EntLib DAAB 有提供這樣一個可以讓我們自定對應的方法,所以我就使用 EntLib DAAB 的 Row Mapper 來做這件事情,當然也是有其他的方式可以做同樣的事情,但我就不是那麼熟悉了。

我先建立了一個「OrderMasterDetailsViewModel」類別,這就是給頁面使用的 Model 類別,

public class OrderMasterDetailsViewModel
{
    public Order Master
    {
        get;
        set;
    }
 
    public List<Order_Detail> Details
    {
        get;
        set;
    }
}
}

接著就是 Controller Action 方法的處理內容,

public ActionResult Details(int? id = 0)
{
    if (id != 0)
    {
        var cmd = db.Database.Connection.CreateCommand();
        cmd.CommandType = CommandType.StoredProcedure;
        cmd.CommandText = "[dbo].[ReturnTwoResult]";
 
        DbParameter param = cmd.CreateParameter();
        param.ParameterName = "OrderID";
        param.Value = id;
 
        cmd.Parameters.Add(param);
 
        Order order = new Order();
        List<Order_Detail> orderDetails = new List<Order_Detail>();
 
        try
        {
            db.Database.Connection.Open();
 
            var reader = cmd.ExecuteReader();
 
            IRowMapper<Order> orderMapper = MapBuilder<Order>.MapNoProperties()
                .MapByName(x => x.OrderID)
                .MapByName(x => x.CustomerID)
                .MapByName(x => x.EmployeeID)
                .MapByName(x => x.OrderDate)
                .MapByName(x => x.RequiredDate)
                .MapByName(x => x.ShippedDate)
                .MapByName(x => x.ShipVia)
                .MapByName(x => x.Freight)
                .MapByName(x => x.ShipName)
                .MapByName(x => x.ShipAddress)
                .MapByName(x => x.ShipCity)
                .MapByName(x => x.ShipRegion)
                .MapByName(x => x.ShipPostalCode)
                .MapByName(x => x.ShipCountry)
                .Build();
 
            if (reader.Read())
            {
                order = orderMapper.MapRow(reader);
            }
 
            reader.NextResult();
 
            IRowMapper<Order_Detail> orderDetailMapper = MapBuilder<Order_Detail>.MapNoProperties()
                .MapByName(x => x.OrderID)
                .MapByName(x => x.ProductID)
                .MapByName(x => x.UnitPrice)
                .MapByName(x => x.Quantity)
                .MapByName(x => x.Discount)
                .Build();
 
            while (reader.Read())
            {
                orderDetails.Add(orderDetailMapper.MapRow(reader));
            }
 
            OrderMasterDetailsViewModel viewModel = new OrderMasterDetailsViewModel()
            {
                Master = order,
                Details = orderDetails
            };
            return View(viewModel);
        }
        catch
        {
            throw;
        }
        finally
        {
            db.Database.Connection.Close();
        }
    }
    return RedirectToAction("Index");
}

執行結果:

image

image

image

這個處理方式其實已經跳脫出 EF 的處理方式,以 ADO.NET 的方式來做處理。

 

修改 EF 的 EDMX CSDL 內容

這方式可以參考「MSDN Data Developer Center - Stored Procedures with Multiple Result Sets」裡面Multiple Result Sets with Configured in EDMX  這一部分的內容,先決條件是你的專案必須是 .NET Framework 4.5。

我這邊強烈的不建議各位這麼做,因為 EDMX 的內容修改不易,另外 EDMX 會隨著每次 Model 內容的更新而有所變化,雖然 Function Import 的部份不會受到影響,但仍有可能讓 EDMX 內容受到損壞,尤其是在多人開發的專案裡更是不建議採取這個方法。

 


Stroed Procedure 的結果是傳回多種資料集合時,建議大家使用既有的 ADO.NET 方式,然後再搭配有 Mapping 功能的輔助套件,這樣在處理這樣的結果會比較輕鬆與方便,至於去修改 EF 的 EDMX XML 內容,我真的是不建議,甚至我不奢求可以使用 Entity Framework 來解決這部份的需求。

 

延伸閱讀:

ADO.Entity Framework: Stored Procedure Customization - meek - Site Home - MSDN Blogs
http://blogs.msdn.com/b/meek/archive/2008/03/26/ado-entity-framework-stored-procedure-customization.aspx

MSDN - Developing Applications Using the Data Access Application Block - Returning Data as Objects for Client Side Querying - Building Output Mappers
http://msdn.microsoft.com/en-us/library/ff664486(v=pandp.50).aspx

 

以上

4 則留言:

  1. mrkt 大大
    想請問您, EF 呼叫sp 有辦法直接把資料 塞進 order.orderdetail 物件集合裡嗎?

    回覆刪除
    回覆
    1. 直接? 我不清楚你所說的直接把資料塞進某物件集合裡的意思是什麼?
      不過我記得我也有寫過一篇 Stored Procedure 與 Auto Mapper 搭配的文章,
      其實作法跟這一篇所說的 Row Mapper 方式差不多,
      要釐清的是,不管是用那一種型別,當我們所取得的資料內容與所要使用的目標類別有所差異的時候,
      沒有所謂的直接塞進去就可以,除非取得的資料與目標類別是一致的情況,
      不然還是需要做處理,而 AutoMapper 就是可以讓我們只要設定一次,之後就不用再做重複的 Mapping 設定動作。

      「使用 AutoMapper 處理類別之間的對映轉換」
      http://kevintsengtw.blogspot.tw/2013/04/automapper.html
      這一篇裡面就是用 Stored Procedure 搭配 AutoMapper 的說明。

      http://kevintsengtw.blogspot.tw/search/label/AutoMapper

      刪除
    2. 感謝 mrkt 大大的回覆
      我的意思是 EF 不知道可不可以透過如下類似的sql join 語法,將資料直接塞入Order & OrderDetail
      public clas Order{
      public string id{set;get;};
      public string name{set;get;};
      public ICollection OrderDetail{set;get;};
      }
      var order = Context.Database.SqlQuery("select * from order o left outer join orderdetail d on o.id = d.orderid");
      然後 order.orderdetail 就可以取得該訂單下的所有orderdetail資料

      ps 我試都試不出來,再請您指導了,感謝。

      刪除
    3. Hello,你的試不出來是有發生什麼樣得錯誤情況或是訊息嗎?
      你可以從錯誤訊息裡看出問題是在哪裡,然後可以查查 MSDN,看看使用 Entity Framework 應該如何正確的操作 SqlQuery,
      這樣就會知道你的程式裡少了什麼,
      http://msdn.microsoft.com/zh-tw/library/system.data.entity.database.sqlquery(v=vs.113).aspx

      刪除

提醒

千萬不要使用 Google Talk (Hangouts) 或 Facebook 及時通訊與我聯繫、提問,因為會掉訊息甚至我是過了好幾天之後才發現到你曾經傳給我訊息過,請多多使用「詢問與建議」(在左邊,就在左邊),另外比較深入的問題討論,或是有牽涉到你實作程式碼的內容,不適合在留言板裡留言討論,請務必使用「詢問與建議」功能(可以夾帶檔案),謝謝。

最近的留言