0. 前言
目前的專案, 遇到需要傳多個值, 查出對應資料的需求, 例如: 傳入多個組織單位代號, 去取出對應的組織單位名稱; 或者傳入多個產品類別, 去取出對應的產品資料.
經上網查詢, 至少有以下4種解決方式.
- 方式一: 採用 LINQ 的 Contains 方法
- 方式二: 自行撰寫 WHERE IN 的 SQL 敘述
- 方式三: 採用 Stored Procedure, 自行傳入 Table-Valued Parameter
- 方式四: 採用 Stored Procedure, 利用 EntityFrameworkExtras.EF6, 傳入 Table-Valued Parameter
以下茲以 傳入多個產品類別, 去取出對應的產品資料 為例, 進行說明.
本篇文章的編排比較類似個人的筆記, 說明會放在程式碼裡, 或圖片即能理解, 就不多作說明.
相關程式, 可 由此下載
1. 資料庫環境設置
1.1 資料庫與 Table 建立
USE MyCookies1;
GO
-- ===========================
-- 移除 Table
-- ===========================
DROP TABLE IF EXISTS [dbo].[Products];
GO
DROP TABLE IF EXISTS [dbo].[Categories]
GO
-- ===========================
-- 建立 Table
-- ===========================
CREATE TABLE [dbo].[Categories]
( [Id] INT NOT NULL
, [Name] NVARCHAR(30) NOT NULL
CONSTRAINT PK_Catgories PRIMARY KEY ( [Id] )
);
GO
CREATE TABLE [dbo].[Products]
( [Id] INT NOT NULL
, [Name] NVARCHAR(30) NOT NULL
, [CategoryId] INT NOT NULL
, [Price] INT NOT NULL
CONSTRAINT PK_Products PRIMARY KEY ( [Id] )
, CONSTRAINT FK_Categories FOREIGN KEY ( [CategoryId] )
REFERENCES [dbo].[Categories] ( [Id] )
);
GO
-- ===========================
-- 新增測試資料
-- ===========================
INSERT INTO [dbo].[Categories]
VALUES ( 1, N'餅乾類' )
, ( 2, N'蛋糕類' )
, ( 3, N'奶酪類' )
;
GO
INSERT INTO [dbo].[Products]
VALUES ( 1, N'夏威夷豆塔', 1, 50 )
, ( 2, N'堅果塔', 1, 55 )
, ( 3, N'養生堅果', 1, 60 )
, ( 4, N'乳酪塔', 1, 60 )
, ( 5, N'紅豆塔', 1, 60 )
, ( 6, N'草莓塔', 1, 65 )
, ( 7, N'輕乳酪蛋糕(片)', 2, 65 )
, ( 8, N'重乳酪蛋糕(片)', 2, 70 )
, ( 9, N'抹茶奶酪', 3, 90 )
, ( 10, N'草莓奶酪', 3, 85 )
, ( 11, N'芒果奶酪', 3, 85 )
, ( 12, N'紅豆奶酪', 3, 80 )
;
GO
CREATE OR ALTER VIEW [dbo].[ViewProducts]
AS
SELECT ProductId = P.Id
, ProductName = P.Name
, CategoryId = P.CategoryId
, Price = P.Price
, CategoryName = C.Name
FROM dbo.Products P
LEFT JOIN dbo.Categories C ON ( P.CategoryId = C.Id )
;
GO
1.2 Stored Procedure 的建立
含使用者自訂型態 udt_CategoryId, 及 Stored Procedure: dbo.usp_GetProductsByCategories.
USE MyCookies1;
GO
CREATE TYPE [dbo].[udt_CategoryId] AS TABLE (
[CategoryId] INT NOT NULL,
PRIMARY KEY CLUSTERED ([CategoryId] ASC));
GO
-- =============================================
-- Author: <Author,,Name>
-- Create date: <Create Date,,>
-- Description: <Description,,>
-- =============================================
CREATE OR ALTER PROCEDURE dbo.usp_GetProductsByCategories
@tbl_Categories udt_CategoryId READONLY
, @pi_ProductName NVARCHAR(30)
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
-- Insert statements for procedure here
SELECT A.ProductId
, A.ProductName
, A.CategoryId
, A.Price
, A.CategoryName
FROM ViewProducts A
JOIN @tbl_Categories B ON (A.CategoryId = B.CategoryId)
WHERE A.ProductName LIKE '%' + @pi_ProductName + '%'
;
END
-- -- ============================
-- -- 以 TSQL 呼叫
-- -- ============================
--
-- DECLARE @tbl_Categories udt_CategoryId;
-- DECLARE @ls_ProductName NVARCHAR(30) = N'紅豆';
--
-- INSERT INTO @tbl_Categories(CategoryId)
-- VALUES (1)
-- , (2)
-- , (3)
-- ;
--
-- EXEC usp_GetProductsByCategories @tbl_Categories, @ls_ProductName
-- ;
--
-- OUTPUT ==========>
-- ProductId ProductName CategoryId Price CategoryName
-- 5 紅豆塔 1 60 餅乾類
-- 12 紅豆奶酪 3 80 奶酪類
2. 方式一: 採用 LINQ 的 Contains 方法
2.1 範例程式:
說明: 採用傳入 產品字串 List 的方式; 利用 LINQ Contains.
/// <summary>
/// 傳入多個商品類別查詢產品, 以 Linq Contains 語句進行處理
/// </summary>
/// <param name="categories"></param>
/// <returns></returns>
public List<ViewProduct> GetProductsWithLinqContains(List<int> categories)
{
var products = new List<ViewProduct>();
_db.Database.Log = Console.Write; // Entity Framework 產生的 SQL 指令, 由 Console.Write 輸出
products = _db.ViewProducts.Where(x => categories.Contains(x.CategoryId)).ToList();
return products;
}
由 Entity Framework 產出的 SQL 指令, 如下; 其實, 是產出 WHERE IN 的敘述.
於 2020/1/5 下午 03:04:34 +08:00
開啟連接SELECT
[Extent1].[ProductId] AS [ProductId],
[Extent1].[ProductName] AS [ProductName],
[Extent1].[CategoryId] AS [CategoryId],
[Extent1].[Price] AS [Price],
[Extent1].[CategoryName] AS [CategoryName]
FROM (SELECT
[ViewProducts].[ProductId] AS [ProductId],
[ViewProducts].[ProductName] AS [ProductName],
[ViewProducts].[CategoryId] AS [CategoryId],
[ViewProducts].[Price] AS [Price],
[ViewProducts].[CategoryName] AS [CategoryName]
FROM [dbo].[ViewProducts] AS [ViewProducts]) AS [Extent1]
WHERE [Extent1].[CategoryId] IN (2, 3)
-- 於 2020/1/5 下午 03:04:35 +08:00
執行-- 於 50 毫秒後完成,結果: SqlDataReader
於 2020/1/5 下午 03:04:35 +08:00
關閉連接
2.2 測試方法:
說明: 撰寫測試程式, 利用 ExpectedObjects , 進行實際值與預期值的比對.
[TestMethod()]
[TestCategory("GetProducts")]
public void GetProductsWithLinqContainsTest()
{
// Arrange
var expected = new List<ViewProduct>
{ new ViewProduct() { ProductId = 7, ProductName = "輕乳酪蛋糕(片)", CategoryId = 2, Price = 65, CategoryName = "蛋糕類"}
, new ViewProduct() { ProductId = 8, ProductName = "重乳酪蛋糕(片)", CategoryId = 2, Price = 70, CategoryName = "蛋糕類"}
, new ViewProduct() { ProductId = 9, ProductName = "抹茶奶酪", CategoryId = 3, Price = 90, CategoryName = "奶酪類"}
, new ViewProduct() { ProductId = 10, ProductName = "草莓奶酪", CategoryId = 3, Price = 85, CategoryName = "奶酪類"}
, new ViewProduct() { ProductId = 11, ProductName = "芒果奶酪", CategoryId = 3, Price = 85, CategoryName = "奶酪類"}
, new ViewProduct() { ProductId = 12, ProductName = "紅豆奶酪", CategoryId = 3, Price = 80, CategoryName = "奶酪類"}
}.ToExpectedObject();
var categories = new List<int>() { 2, 3 };
// Act
var obj = new CookiesService();
var actual = obj.GetProductsWithLinqContains(categories);
// Assert
expected.ShouldEqual(actual);
}
3. 方式二: 自行撰寫 WHERE IN 的 SQL 敘述
如果不滿意 Entity Framework 產出的 SQL 指令, 想自己寫, 也是可以; 只是沒有辦法作到強型別的檢查. 且要自己小心, 別打錯字.
3.1 範例程式:
說明: 採用傳入 產品字串 List 的方式; 利用 SQL IN.
/// <summary>
/// 傳入多個商品類別查詢產品, 以 T-SQL IN 語句進行處理
/// </summary>
/// <param name="categories"></param>
/// <returns></returns>
public List<ViewProduct> GetProductsWithSqlIn(List<int> categories)
{
var products = new List<ViewProduct>();
_db.Database.Log = Console.Write; // Entity Framework 產生的 SQL 指令, 由 Console.Write 輸出
// -------------------
// STEP 1: 原始的 sql 字串
// -------------------
string sql = @"
SELECT A.ProductId
, A.ProductName
, A.CategoryId
, A.Price
, A.CategoryName
FROM ViewProducts A
WHERE A.CategoryId IN "
;
// -------------------
// STEP 2: 建立 IN 的參數
// -------------------
StringBuilder sb = new StringBuilder();
List<SqlParameter> parameters = new List<SqlParameter>();
int i = 1;
foreach (var category in categories)
{
// IN clause
sb.Append("@category_" + i + ",");
// parameters
parameters.Add(new SqlParameter("@category_" + i, SqlDbType.Int) { Value = category });
i++;
}
//去除最後的 "," ; 並加上 左/右 括號 及分號
string strIn = sb.ToString();
strIn = "(" + strIn.Substring(0, strIn.Length - 1) + ");";
// -------------------
// STEP 3: 串成新的 sql
// -------------------
sql = sql + strIn;
products = _db.Database.SqlQuery<ViewProduct>(sql, parameters.ToArray()).ToList();
return products;
}
3.2 測試方法:
說明: 撰寫測試程式, 利用 ExpectedObjects , 進行實際值與預期值的比對.
[TestMethod()]
[TestCategory("GetProducts")]
public void GetProductsWithLinqContainsTest()
{
// Arrange
var expected = new List<ViewProduct>
{ new ViewProduct() { ProductId = 7, ProductName = "輕乳酪蛋糕(片)", CategoryId = 2, Price = 65, CategoryName = "蛋糕類"}
, new ViewProduct() { ProductId = 8, ProductName = "重乳酪蛋糕(片)", CategoryId = 2, Price = 70, CategoryName = "蛋糕類"}
, new ViewProduct() { ProductId = 9, ProductName = "抹茶奶酪", CategoryId = 3, Price = 90, CategoryName = "奶酪類"}
, new ViewProduct() { ProductId = 10, ProductName = "草莓奶酪", CategoryId = 3, Price = 85, CategoryName = "奶酪類"}
, new ViewProduct() { ProductId = 11, ProductName = "芒果奶酪", CategoryId = 3, Price = 85, CategoryName = "奶酪類"}
, new ViewProduct() { ProductId = 12, ProductName = "紅豆奶酪", CategoryId = 3, Price = 80, CategoryName = "奶酪類"}
}.ToExpectedObject();
var categories = new List<int>() { 2, 3 };
// Act
var obj = new CookiesService();
var actual = obj.GetProductsWithSqlIn(categories);
// Assert
expected.ShouldEqual(actual);
}
4. 方式三: 採用 Stored Procedure, 自行傳入 Table-Valued Parameter
如果 SELECT 的邏輯, 不是很單純用一個 SQL 指令, 就可產生結果; 此時可考慮採用 Stored Procedure, 同時建立使用者資料型態, 傳入多值查詢條件. 如本文 1.2 Stored Procedure 的建立 的說明.
4.1 範例程式:
說明: 採用傳入 產品字串 List 的方式; 利用 Stored Procedure + User Defined DataType.
該 Stored Procedure, 共有 2 個傳入參數 @tbl_Categories, @pi_ProductName.
前者的 SqlDbType 必須定義為 SqlDbType.Structured, 且傳入自行定義的 data table, 作為對應的實際參數值.
/// <summary>
/// 傳入多個商品類別查詢產品, 以 Entity Framework 的 SqlQuery<T> 呼叫 stored procedure 進行處理
/// </summary>
/// <param name="categories"></param>
/// <param name="productName"></param>
/// <returns></returns>
public List<ViewProduct> GetProductsWithSpByEf(List<int> categories, string productName = "")
{
var result = new List<ViewProduct>();
_db.Database.Log = Console.Write; // Entity Framework 產生的 SQL 指令, 由 Console.Write 輸出
//建立查詢條件的 DataTable (只包含 1 個 Column)
DataTable dt = new DataTable("MyTable");
dt.Columns.Add(new DataColumn("CategoryId", typeof(int)));
DataRow row = null;
foreach (var category in categories)
{
row = dt.NewRow();
row["CategoryId"] = category;
dt.Rows.Add(row);
}
//建立查詢參數
var para1 = new SqlParameter("@tbl_Categories", SqlDbType.Structured) { Value = dt };
var para2 = new SqlParameter("@pi_ProductName", SqlDbType.NVarChar, 30) { Value = productName };
//設定 SqlDbType.Structured 那個參數對應的 User Defined Type
para1.TypeName = "dbo.udt_CategoryId";
result = _db.Database.SqlQuery<ViewProduct>("exec usp_GetProductsByCategories @tbl_Categories, @pi_ProductName", para1, para2).ToList();
return result;
}
4.2 測試方法:
說明: 撰寫測試程式, 利用 ExpectedObjects , 進行實際值與預期值的比對.
[TestMethod()]
[TestCategory("GetProducts")]
public void GetProductsWithSpByEfTest()
{
// Arrange
var expected = new List<ViewProduct>
{ new ViewProduct() { ProductId = 5, ProductName = "紅豆塔", CategoryId = 1, Price = 60, CategoryName = "餅乾類"}
, new ViewProduct() { ProductId = 12, ProductName = "紅豆奶酪", CategoryId = 3, Price = 80, CategoryName = "奶酪類"}
}.ToExpectedObject();
var categories = new List<int>() { 1, 2, 3 };
var productName = "紅豆";
// Act
var obj = new CookiesService();
var actual = obj.GetProductsWithSpByEf(categories, productName);
// Assert
expected.ShouldEqual(actual);
}
5. 方式四: 採用 Stored Procedure, 利用 EntityFrameworkExtras.EF6, 傳入 Table-Valued Parameter
如果覺得前述自行建立 data table 很不方便, 則可以採用 EntityFrameworkExtras.EF6 這個套件; 它透過 Attribute 的設置, 建立呼叫 Stored Procedure 時所需的對應.
5.1 範例程式:
說明: 採用傳入 產品字串 List 的方式; SQL 端採 Stored Procedure + User Defined DataType; C# 端, 採用 EntityFrameworkExtras.EF6 這個套件.
共有 5 個步驟, 細節可以參考程式碼的註解.
前 2 個步驗, 需要另外單獨建立對應的 2 個 class; 後 3 個步驟, 則是進行呼叫.
- STEP 1 : Define a stored procedure class
- STEP 2 : Define the user defined data type
- STEP 3 : 將傳入的查詢條件, 轉為 Extras.EF6 定義的 user defined data type 格式
- STEP 4 : 建立 Extras.EF6 的 proc instance
- STEP 5 : 透過 Extras.EF6 呼叫 stored procedure
/// <summary>
/// Extras : STEP 1 : Define a stored procedure class
/// </summary>
/// <remarks>
/// https://github.com/Fodsuk/EntityFrameworkExtras
/// </remarks>
[StoredProcedure("usp_GetProductsByCategories")]
public class GetProductsByCategories
{
/// <summary>
/// 多筆的產品類別代碼
/// </summary>
/// <remarks>
/// 必須定義 ParameterName, 以供 Extras.EF6 在呼叫時作對應
/// </remarks>
[StoredProcedureParameter(SqlDbType.Udt, ParameterName = "tbl_Categories")]
public List<udt_CategoryId> Categories { get; set; }
/// <summary>
/// 產品名稱
/// </summary>
/// <remarks>
/// 必須定義 ParameterName, 以供 Extras.EF6 在呼叫時作對應
/// </remarks>
[StoredProcedureParameter(SqlDbType.NVarChar, ParameterName = "pi_ProductName")]
public string ProductName { get; set; }
}
/// <summary>
/// Extras : STEP 2 : Define the user defined data type
/// </summary>
/// <remarks>
/// 必須指定該 Class, 是對應至那一個 User Defined Data Type;
/// 如果有多個, 就要建立多個不同的 Class
/// </remarks>
[UserDefinedTableType("udt_CategoryId")]
public class udt_CategoryId
{
[UserDefinedTableTypeColumn(1)]
public int CategoryId { get; set; }
}
/// <summary>
/// 傳入多個商品類別查詢產品, 透過 EntityFrameworkExtra.EF6, 呼叫 stored procedure 進行處理
/// </summary>
/// <param name="categories"></param>
/// <param name="productName"></param>
/// <returns></returns>
public List<ViewProduct> GetProductsWithSpByEfExtras(List<int> categories, string productName = "")
{
var result = new List<ViewProduct>();
_db.Database.Log = Console.Write; // Entity Framework 產生的 SQL 指令, 由 Console.Write 輸出
// Extras : STEP 3 : 將傳入的查詢條件, 轉為 Extras.EF6 定義的 user defined data type 格式
var codes = categories.Select(x => new udt_CategoryId() { CategoryId = x }).ToList();
// Extras : STEP 4 : 建立 Extras.EF6 的 proc instance
var proc = new GetProductsByCategories()
{
Categories = codes,
ProductName = productName
};
// Extras : STEP 5 : 透過 Extras.EF6 呼叫 stored procedure
result = _db.Database.ExecuteStoredProcedure<ViewProduct>(proc).ToList();
return result;
}
5.2 測試方法:
說明: 撰寫測試程式, 利用 ExpectedObjects , 進行實際值與預期值的比對.
[TestMethod()]
[TestCategory("GetProducts")]
public void GetProductsWithSpByEfExtrasTest()
{
// Arrange
var expected = new List<ViewProduct>
{ new ViewProduct() { ProductId = 5, ProductName = "紅豆塔", CategoryId = 1, Price = 60, CategoryName = "餅乾類"}
, new ViewProduct() { ProductId = 12, ProductName = "紅豆奶酪", CategoryId = 3, Price = 80, CategoryName = "奶酪類"}
}.ToExpectedObject();
var categories = new List<int>() { 1, 2, 3 };
var productName = "紅豆";
// Act
var obj = new CookiesService();
var actual = obj.GetProductsWithSpByEfExtras(categories, productName);
// Assert
expected.ShouldEqual(actual);
}
6. 結論
在專案的進行中, 有時會有傳入多個值, 查出對應資料的需求; 本文僅就此類需求, 上網查詢解決方式, 並作整理, 撰寫範例程式; 供有需要的朋友作參考.
7. 參考文件
- 01. Erland Sommarskog, Using Table-Valued Parameters in SQL Server and .NET
* 這篇針對採用 .NET 需要傳遞多值的狀況, 作了不少的說明. - 02. Entity Framework Tutorial, Execute Stored Procedure using DBContext
* 這篇主要是針對 stored procedure 的傳入參數, 是很單純的 SQL Data Type (例如: char, varchar, int ..), 在 Entity Framework 採用 Database first 的情境下, 進行匯入的操作過程說明. - 03. C# Corner, Access Stored Procedure With User Defined Data Type Using Entity Framework
* 這篇針對採用 EntityFraeework.Estras.EF6 有作了操作上的說明.
沒有留言:
張貼留言