根據上次的內容,繼續完成更新產品,也繼續將資料存取層完成。讓範例更接近真實應用。
參考資料
[ADO.NET] 為何 / 如何 使用 SQLParameter 物件
上次提到怎麼把資料庫讀取程式重構到單獨類別,範例程式中都有個小數一直變化,不曉得有沒有人注意到。
那指的是距離主題完成目標還有多遠,上次才停留在 0.4 ,這次就讓我們慢慢補足剩下的部分。
首先,上次範例都只有讀取產品清單,難道這樣可以完成上頭派下來的任務嗎?當然不可能,光是產品就應該有增刪修查,再加上與其他表也會有互動。這樣才是一個正常的任務。
我們試著把這樣的功能完成,並且依照上次的做法重構,當然我們還是從爛程式開始,一步步重構。為簡化範例程式,只做到能編輯就好了。
先提一下,我的爛程式中,有十分嚴重的 SQL Injection 的問題,這會是在任何重構之前的優先修正事項。
第一步,先假設我們受命完成產品編輯,其中包括剛才提到的增刪修查。根據老寫法,如果把清單及明細寫在同一頁,大概頁面及程式會長這樣。
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Demo5.aspx.cs" Inherits="WebApplication1.Demo5" %>
<%@ Import Namespace="System.Data.SqlClient" %>
<%@ Import Namespace="System.Data" %>
<script runat="server">
protected void Page_Init(object sender, EventArgs e)
{
// 這裡是上次改的
WebApplication1.DataObject.DataAccess da = new WebApplication1.DataObject.DataAccess();
List<WebApplication1.DataObject.ProductValueObject> list = da.getProducts();
this.GridView1.DataSource = list;
this.GridView1.DataBind();
this.FormView1.Visible = false;
}
protected void Page_Load(object sender, EventArgs e)
{
}
protected void GridView1_RowCommand(object sender, GridViewCommandEventArgs e)
{
string ProductNo = e.CommandArgument.ToString();
this.HF_ProductNo.Value = ProductNo;
try
{
string connStr = ConfigurationManager.ConnectionStrings["DemoConnectionString"].ConnectionString;
using (SqlConnection conn = new SqlConnection(connStr))
{
// 拜託拜託,這個寫法絕對有 SQL Injection 的資安問題
// 此範例為待改進的不良範例,請勿照抄,至少也請看完下方的改進一
string SELECTCmd = "SELECT TOP 1 * FROM Product WHERE ProductNo = '" + ProductNo + "'";
SqlCommand cmd = new SqlCommand(SELECTCmd, conn);
conn.Open();
var dt = new DataTable();
SqlDataReader reader = cmd.ExecuteReader();
dt.Load(reader);
this.FormView1.DataSource = dt;
this.FormView1.DataBind();
this.FormView1.Visible = true;
}
}
catch (Exception ex)
{
// Do Something
}
}
protected void FormView1_ItemCommand(object sender, FormViewCommandEventArgs e)
{
if (e.CommandName == "CancelCmd")
{
this.FormView1.Visible = false;
this.HF_ProductNo.Value = "-1";
}
if (e.CommandName == "UpdateCmd")
{
this.getProductAndUpdate();
}
}
private void getProductAndUpdate()
{
var ctlProductNoLabel = this.FormView1.FindControl("ProductNoLabel");
var ctlEan13TextBox = this.FormView1.FindControl("Ean13TextBox");
var ctlProductTypeTextBox = this.FormView1.FindControl("ProductTypeTextBox");
var ctlTempTextBox = this.FormView1.FindControl("TempTextBox");
var ctlExpireTextBox = this.FormView1.FindControl("ExpireTextBox");
var ctlLengthTextBox = this.FormView1.FindControl("LengthTextBox");
var ctlWidthTextBox = this.FormView1.FindControl("WidthTextBox");
var ctlHeightTextBox = this.FormView1.FindControl("HeightTextBox");
var ctlWeightTextBox = this.FormView1.FindControl("WeightTextBox");
string ProductNoLabel = ((ITextControl)ctlProductNoLabel).Text;
string Ean13TextBox = ((ITextControl)ctlEan13TextBox).Text;
string ProductTypeTextBox = ((ITextControl)ctlProductTypeTextBox).Text;
string TempTextBox = ((ITextControl)ctlTempTextBox).Text;
string ExpireTextBox = ((ITextControl)ctlExpireTextBox).Text;
string LengthTextBox = ((ITextControl)ctlLengthTextBox).Text;
string WidthTextBox = ((ITextControl)ctlWidthTextBox).Text;
string HeightTextBox = ((ITextControl)ctlHeightTextBox).Text;
string WeightTextBox = ((ITextControl)ctlWeightTextBox).Text;
try
{
string connStr = ConfigurationManager.ConnectionStrings["DemoConnectionString"].ConnectionString;
using (SqlConnection conn = new SqlConnection(connStr))
{
// 拜託拜託,這個寫法絕對有 SQL Injection 的資安問題
// 此範例為待改進的不良範例,請勿照抄,至少也請看完下方的改進一
string UpdateCmd =
" UPDATE Product " +
" SET Ean13 = '" + Ean13TextBox + "', ProductType = '" + ProductTypeTextBox + "', Temp = '" + TempTextBox + "', Expire = '" + ExpireTextBox + "', " +
" Length = '" + LengthTextBox + "', Width = '" + WidthTextBox + "', Height = '" + HeightTextBox + "', Weight = '" + WeightTextBox + "' " +
" WHERE ProductNo = '" + ProductNoLabel + "' ";
SqlCommand cmd = new SqlCommand(UpdateCmd, conn);
conn.Open();
cmd.ExecuteNonQuery();
conn.Close();
}
}
catch (Exception ex)
{
// Do Something
}
}
</script>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title></title>
</head>
<body>
<form id="form1" runat="server">
<asp:HiddenField ID="HF_ProductNo" runat="server" Value="-1" />
<asp:GridView ID="GridView1" runat="server" EnableViewState="false" AutoGenerateColumns="false" OnRowCommand="GridView1_RowCommand">
<Columns>
<asp:BoundField DataField="ProductNo" HeaderText="ProductNo" ReadOnly="True" SortExpression="ProductNo" />
<asp:BoundField DataField="Ean13" HeaderText="Ean13" SortExpression="Ean13" />
<asp:BoundField DataField="ProductType" HeaderText="ProductType" SortExpression="ProductType" />
<asp:BoundField DataField="Temp" HeaderText="Temp" SortExpression="Temp" />
<asp:BoundField DataField="Expire" HeaderText="Expire" SortExpression="Expire" />
<asp:BoundField DataField="Length" HeaderText="Length" SortExpression="Length" />
<asp:BoundField DataField="Width" HeaderText="Width" SortExpression="Width" />
<asp:BoundField DataField="Height" HeaderText="Height" SortExpression="Height" />
<asp:BoundField DataField="Weight" HeaderText="Weight" SortExpression="Weight" />
<asp:TemplateField>
<ItemTemplate>
<asp:Button runat="server" CommandArgument='<%# Eval("ProductNo") %>' CommandName="GoEdit" Text="this is a Pan." />
</ItemTemplate>
</asp:TemplateField>
</Columns>
</asp:GridView>
<asp:FormView ID="FormView1" runat="server" DataKeyNames="ProductNo" DefaultMode="Edit" OnItemCommand="FormView1_ItemCommand">
<EditItemTemplate>
ProductNo:<asp:Label ID="ProductNoLabel" runat="server" Text='<%# Eval("ProductNo") %>' /><br />
Ean13:<asp:TextBox ID="Ean13TextBox" runat="server" Text='<%# Bind("Ean13") %>' /><br />
ProductType:<asp:TextBox ID="ProductTypeTextBox" runat="server" Text='<%# Bind("ProductType") %>' /><br />
Temp:<asp:TextBox ID="TempTextBox" runat="server" Text='<%# Bind("Temp") %>' /><br />
Expire:<asp:TextBox ID="ExpireTextBox" runat="server" Text='<%# Bind("Expire") %>' /><br />
Length:<asp:TextBox ID="LengthTextBox" runat="server" Text='<%# Bind("Length") %>' /><br />
Width:<asp:TextBox ID="WidthTextBox" runat="server" Text='<%# Bind("Width") %>' /><br />
Height:<asp:TextBox ID="HeightTextBox" runat="server" Text='<%# Bind("Height") %>' /><br />
Weight:<asp:TextBox ID="WeightTextBox" runat="server" Text='<%# Bind("Weight") %>' /><br />
<asp:LinkButton ID="UpdateButton" runat="server" CausesValidation="True" CommandName="UpdateCmd" Text="更新" />
<asp:LinkButton ID="UpdateCancelButton" runat="server" CausesValidation="False" CommandName="CancelCmd" Text="取消" />
</EditItemTemplate>
</asp:FormView>
</form>
</body>
</html>
範例看完了,來看看怎麼改進。第一件最重要的事,不是重構,而是摘掉資安問題,套小朱常在論壇上講的「沒有修正 SQL Injection 之前,重構沒有意義。」所以第一件事,先來把所有 SQL Injection 都做基本處理。
先用 SQLParameter 好好來處理一下參數的傳遞,本範例目前也只打算做到這等級,關於其它資安問題,之後有機會再來寫。所以讓我們把程式部份改成這樣 (0.4 -> 0.6) :
protected void Page_Init(object sender, EventArgs e)
{
// 這裡是上次改的
WebApplication1.DataObject.DataAccess da = new WebApplication1.DataObject.DataAccess();
List<WebApplication1.DataObject.ProductValueObject> list = da.getProducts();
this.GridView1.DataSource = list;
this.GridView1.DataBind();
this.FormView1.Visible = false;
}
protected void Page_Load(object sender, EventArgs e)
{
}
protected void GridView1_RowCommand(object sender, GridViewCommandEventArgs e)
{
string ProductNo = e.CommandArgument.ToString();
this.HF_ProductNo.Value = ProductNo;
try
{
string connStr = ConfigurationManager.ConnectionStrings["DemoConnectionString"].ConnectionString;
using (SqlConnection conn = new SqlConnection(connStr))
{
// 摘掉 SQL Injection 就是神清氣爽,心情都好起來了呢
string SELECTCmd = "SELECT TOP 1 * FROM Product WHERE ProductNo = @ProductNo";
SqlCommand cmd = new SqlCommand(SELECTCmd, conn);
cmd.Parameters.AddWithValue("@ProductNo", ProductNo);
conn.Open();
var dt = new DataTable();
SqlDataReader reader = cmd.ExecuteReader();
dt.Load(reader);
this.FormView1.DataSource = dt;
this.FormView1.DataBind();
this.FormView1.Visible = true;
}
}
catch (Exception ex)
{
// Do Something
}
}
protected void FormView1_ItemCommand(object sender, FormViewCommandEventArgs e)
{
if (e.CommandName == "CancelCmd")
{
this.FormView1.Visible = false;
this.HF_ProductNo.Value = "-1";
}
if (e.CommandName == "UpdateCmd")
{
this.getProductAndUpdate();
}
}
private void getProductAndUpdate()
{
var ctlProductNoLabel = this.FormView1.FindControl("ProductNoLabel");
var ctlEan13TextBox = this.FormView1.FindControl("Ean13TextBox");
var ctlProductTypeTextBox = this.FormView1.FindControl("ProductTypeTextBox");
var ctlTempTextBox = this.FormView1.FindControl("TempTextBox");
var ctlExpireTextBox = this.FormView1.FindControl("ExpireTextBox");
var ctlLengthTextBox = this.FormView1.FindControl("LengthTextBox");
var ctlWidthTextBox = this.FormView1.FindControl("WidthTextBox");
var ctlHeightTextBox = this.FormView1.FindControl("HeightTextBox");
var ctlWeightTextBox = this.FormView1.FindControl("WeightTextBox");
string ProductNoLabel = ((ITextControl)ctlProductNoLabel).Text;
string Ean13TextBox = ((ITextControl)ctlEan13TextBox).Text;
string ProductTypeTextBox = ((ITextControl)ctlProductTypeTextBox).Text;
string TempTextBox = ((ITextControl)ctlTempTextBox).Text;
string ExpireTextBox = ((ITextControl)ctlExpireTextBox).Text;
string LengthTextBox = ((ITextControl)ctlLengthTextBox).Text;
string WidthTextBox = ((ITextControl)ctlWidthTextBox).Text;
string HeightTextBox = ((ITextControl)ctlHeightTextBox).Text;
string WeightTextBox = ((ITextControl)ctlWeightTextBox).Text;
try
{
string connStr = ConfigurationManager.ConnectionStrings["DemoConnectionString"].ConnectionString;
using (SqlConnection conn = new SqlConnection(connStr))
{
// 摘掉 SQL Injection 就是神清氣爽,心情都好起來了呢
string UpdateCmd =
" UPDATE Product " +
" SET Ean13 = @Ean13, ProductType = @ProductType, Temp = @Temp, Expire = @Expire, " +
" Length = @Length, Width = @Width, Height = @Height, Weight = @Weight " +
" WHERE ProductNo = @ProductNo ";
SqlCommand cmd = new SqlCommand(UpdateCmd, conn);
cmd.Parameters.AddWithValue("@Ean13", Ean13TextBox);
cmd.Parameters.AddWithValue("@ProductType", ProductTypeTextBox);
cmd.Parameters.AddWithValue("@Temp", TempTextBox);
cmd.Parameters.AddWithValue("@Expire", ExpireTextBox);
cmd.Parameters.AddWithValue("@Length", LengthTextBox);
cmd.Parameters.AddWithValue("@Width",WidthTextBox );
cmd.Parameters.AddWithValue("@Height", HeightTextBox);
cmd.Parameters.AddWithValue("@Weight", WeightTextBox);
cmd.Parameters.AddWithValue("@ProductNo", ProductNoLabel);
conn.Open();
cmd.ExecuteNonQuery();
conn.Close();
}
}
catch (Exception ex)
{
// Do Something
}
}
使用了 SQLParameter ,不再透過組字串操作資料庫,底層組件會幫你處理好。如果你的專案中到處都還是組字串操作資料庫,拜託立刻,立刻!改成用 SQLParameter 來傳遞變數。
經過這個修改後,我們繼續進行重構。仿照上次的寫法,將查詢單筆資料以及更新資料的語法都放到 DataAccess 。所以現在程式碼如下 (0.6~0.65) :
using System;
using System.Data;
using System.Data.SqlClient;
using System.Configuration;
using System.Collections.Generic;
namespace WebApplication1.DataObject
{
public class DataAccess
{
private ProductValueObject parseDataToObject(IDataReader row)
{
var returnObj = new ProductValueObject();
returnObj.ProductNo = row["ProductNo"].ToString();
returnObj.Ean13 = row["Ean13"].ToString();
returnObj.ProductType = row["ProductType"].ToString();
returnObj.Temp = row["Temp"].ToString();
returnObj.Expire = row["Expire"].ToString();
returnObj.Length = (double)row["Length"];
returnObj.Width = (double)row["Width"];
returnObj.Height = (double)row["Height"];
returnObj.Weight = (double)row["Weight"];
return returnObj;
}
public List<ProductValueObject> getProducts()
{
var returnList = new List<ProductValueObject>();
try
{
string connStr = ConfigurationManager.ConnectionStrings["DemoConnectionString"].ConnectionString;
using (SqlConnection conn = new SqlConnection(connStr))
{
SqlCommand cmd = new SqlCommand("SELECT * FROM Product", conn);
conn.Open();
var dt = new DataTable();
SqlDataReader reader = cmd.ExecuteReader();
while (reader.Read())
{
var productItem = this.parseDataToObject(reader);
returnList.Add(productItem);
}
}
}
catch (Exception ex)
{
// Do Something
}
return returnList;
}
protected ProductValueObject getProduct(string ProductNo)
{
ProductValueObject obj = null;
try
{
string connStr = ConfigurationManager.ConnectionStrings["DemoConnectionString"].ConnectionString;
using (SqlConnection conn = new SqlConnection(connStr))
{
string SELECTCmd = "SELECT TOP 1 * FROM Product WHERE ProductNo = @ProductNo";
SqlCommand cmd = new SqlCommand(SELECTCmd, conn);
cmd.Parameters.AddWithValue("@ProductNo", ProductNo);
conn.Open();
var dt = new DataTable();
SqlDataReader reader = cmd.ExecuteReader();
while (reader.Read())
{
obj = this.parseDataToObject(reader);
}
}
}
catch (Exception ex)
{
// Do Something
}
return obj;
}
private void updateProduct(string ProductNo, string Ean13, string ProductType, string Temp, string Expire, string Length, string Width, string Height, string Weight)
{
try
{
string connStr = ConfigurationManager.ConnectionStrings["DemoConnectionString"].ConnectionString;
using (SqlConnection conn = new SqlConnection(connStr))
{
string UpdateCmd =
" UPDATE Product " +
" SET Ean13 = @Ean13, ProductType = @ProductType, Temp = @Temp, Expire = @Expire, " +
" Length = @Length, Width = @Width, Height = @Height, Weight = @Weight " +
" WHERE ProductNo = @ProductNo ";
SqlCommand cmd = new SqlCommand(UpdateCmd, conn);
cmd.Parameters.AddWithValue("@Ean13", Ean13);
cmd.Parameters.AddWithValue("@ProductType", ProductType);
cmd.Parameters.AddWithValue("@Temp", Temp);
cmd.Parameters.AddWithValue("@Expire", Expire);
cmd.Parameters.AddWithValue("@Length", Length);
cmd.Parameters.AddWithValue("@Width", Width);
cmd.Parameters.AddWithValue("@Height", Height);
cmd.Parameters.AddWithValue("@Weight", Weight);
cmd.Parameters.AddWithValue("@ProductNo", ProductNo);
conn.Open();
cmd.ExecuteNonQuery();
conn.Close();
}
}
catch (Exception ex)
{
// Do Something
}
}
}
}
原本頁面中充斥了直接存取資料庫的語法,現在全都改呼叫 DataAccess 的程式,頁面再度瘦身,內容如下:
protected void Page_Init(object sender, EventArgs e)
{
// 這裡是上次改的
WebApplication1.DataObject.DataAccess da = new WebApplication1.DataObject.DataAccess();
List<WebApplication1.DataObject.ProductValueObject> list = da.getProducts();
this.GridView1.DataSource = list;
this.GridView1.DataBind();
this.FormView1.Visible = false;
}
protected void Page_Load(object sender, EventArgs e)
{
}
protected void GridView1_RowCommand(object sender, GridViewCommandEventArgs e)
{
string ProductNo = e.CommandArgument.ToString();
this.HF_ProductNo.Value = ProductNo;
WebApplication1.DataObject.DataAccess da = new WebApplication1.DataObject.DataAccess();
WebApplication1.DataObject.ProductValueObject prodVO = da.getProduct(ProductNo);
List<WebApplication1.DataObject.ProductValueObject> list = new List<WebApplication1.DataObject.ProductValueObject>();
list.Add(prodVO);
this.FormView1.DataSource = list;
this.FormView1.DataBind();
this.FormView1.Visible = true;
}
protected void FormView1_ItemCommand(object sender, FormViewCommandEventArgs e)
{
if (e.CommandName == "CancelCmd")
{
this.FormView1.Visible = false;
this.HF_ProductNo.Value = "-1";
}
if (e.CommandName == "UpdateCmd")
{
this.getProductAndUpdate();
}
}
private void getProductAndUpdate()
{
var ctlProductNoLabel = this.FormView1.FindControl("ProductNoLabel");
var ctlEan13TextBox = this.FormView1.FindControl("Ean13TextBox");
var ctlProductTypeTextBox = this.FormView1.FindControl("ProductTypeTextBox");
var ctlTempTextBox = this.FormView1.FindControl("TempTextBox");
var ctlExpireTextBox = this.FormView1.FindControl("ExpireTextBox");
var ctlLengthTextBox = this.FormView1.FindControl("LengthTextBox");
var ctlWidthTextBox = this.FormView1.FindControl("WidthTextBox");
var ctlHeightTextBox = this.FormView1.FindControl("HeightTextBox");
var ctlWeightTextBox = this.FormView1.FindControl("WeightTextBox");
string ProductNoLabel = ((ITextControl)ctlProductNoLabel).Text;
string Ean13TextBox = ((ITextControl)ctlEan13TextBox).Text;
string ProductTypeTextBox = ((ITextControl)ctlProductTypeTextBox).Text;
string TempTextBox = ((ITextControl)ctlTempTextBox).Text;
string ExpireTextBox = ((ITextControl)ctlExpireTextBox).Text;
string LengthTextBox = ((ITextControl)ctlLengthTextBox).Text;
string WidthTextBox = ((ITextControl)ctlWidthTextBox).Text;
string HeightTextBox = ((ITextControl)ctlHeightTextBox).Text;
string WeightTextBox = ((ITextControl)ctlWeightTextBox).Text;
WebApplication1.DataObject.DataAccess da = new WebApplication1.DataObject.DataAccess();
da.updateProduct(ProductNoLabel, Ean13TextBox, ProductTypeTextBox, TempTextBox, ExpireTextBox, LengthTextBox, WidthTextBox, HeightTextBox, WeightTextBox);
}
到這裡先停一下,或許有人會問,這些東西寫在單獨類別和頁面有什麼差異嗎? 「當然有,不然幹嘛這樣寫呢…」我當然不會這樣回答,也太不技術人了 XD
首先這些切割,都遵尋最基本的 SOLID 原則:物件責任單一化。當一個 Method 塞太多責任時,會讓行數和複雜度不斷成長。到某程度會引發修改者的怒氣也跟著成長,這樣不好。如果能把部份功能抽到另一個 Method ,形同抽出黑箱。黑箱作業不好,但穩定的黑箱程式是讓人開心的,畢竟 Black Box 可是穩定的代名詞呢,哈哈。
抽出 Method 代表可以復用部份程式,不僅減少了手指軟骨的磨損,於測試時也可以分開測試。以上面為例,我們的 getProducts / getProduct / updateProduct 可以分別由各頁面呼叫而不再重寫。真的需要測試「資料程式正確性」時,僅針對此三 Method 撰寫測試碼就好,假設修改前有 N 個頁面要讀取產品清單 / 單一產品, M 個頁面需要修改產品資料,光是測試就是 M ∪ N 頁要跑,而且全都是手~動~操~作。今天改完後,測試只要寫三個,就算是手動測試,數量都從 N + M => 3 ,整個輕鬆愉快,改天同事把程式改爛要亂賴,我們就兩手一攤說:「這是我的測試頁面,馬上來看一下成功或失敗。」,馬上反擊回去了,生活開心~☆
SOLID 原則重點都放在幾項:修改幅度減少、容易擴充有彈性、程式穩定不易壞,建議一定要瞭解這五個原則。如果需要討論,相信各大社群都很歡迎新人進來的。哈哈。
講完好處,休息也夠了。來看看下一步要改什麼,讓我們把眼光看到 DataAccess 這個類別,有沒有看到三個 Method 都有共用的程式:「讀取連線字串,組 SQL ,執行 SQL 」,讓這些程式散佈各處也沒什麼問題,但倘若有一天我們想將連線字串的 KEY 值換掉,或是將資料庫換成 MySQL 等免費資料庫,隨著資料庫連線方法越來越多,到時候改不完,根本無法換掉 (先忽視 SQL 語法的差異) 。
來吧,我們試著抽出資料庫連線處理的程式。把現有程式分為為商業邏輯及資料存取,讓商業邏輯層只剩 SQL 語法和處理資料就好,資料庫連線就交由資料存取層。不過此處會卡一個問題,由於透過 SQLParameter 來傳遞參數,其中一個做法是使用字典來當成資料容器。當然目前都透過文字傳遞參數不會有問題,實務上無法這麼做,這點我們之後再解。
第一階段,我們把 updateProduct 中資料庫連線、執行,都抽到另一個方法 executeCmd 中。概念上很簡單, updateProduct 成為溝通介面,當呼叫它時,將 SQL 語法和參數都傳入 executeCmd 並執行之。這個修改很小不易出錯, CODE 會變成如下 (0.65 => 0.7):
private void executeCmd(string CmdSQL, Dictionary<string, string> Parameters)
{
string connStr = ConfigurationManager.ConnectionStrings["DemoConnectionString"].ConnectionString;
using (SqlConnection conn = new SqlConnection(connStr))
{
SqlCommand cmd = new SqlCommand(CmdSQL, conn);
foreach (KeyValuePair<string, string> pair in Parameters)
{
cmd.Parameters.AddWithValue("@" + pair.Key, pair.Value);
}
conn.Open();
cmd.ExecuteNonQuery();
conn.Close();
}
}
public void updateProduct(string ProductNo, string Ean13, string ProductType, string Temp, string Expire, string Length, string Width, string Height, string Weight)
{
try
{
string UpdateCmd =
" UPDATE Product " +
" SET Ean13 = @Ean13, ProductType = @ProductType, Temp = @Temp, Expire = @Expire, " +
" Length = @Length, Width = @Width, Height = @Height, Weight = @Weight " +
" WHERE ProductNo = @ProductNo ";
Dictionary<string, string> paras = new Dictionary<string, string>();
paras.Add("Ean13", Ean13);
paras.Add("ProductType", ProductType);
paras.Add("Temp", Temp);
paras.Add("Expire", Expire);
paras.Add("Length", Length);
paras.Add("Width", Width);
paras.Add("Height", Height);
paras.Add("Weight", Weight);
paras.Add("ProductNo", ProductNo);
this.executeCmd(UpdateCmd, paras);
}
catch (Exception ex)
{
// Do Something
}
}
這個小小的變更,概念上卻將資料庫的相依性解除了, updateProduct不再相依於 SQLConnection, SQLCommand, SQLParameter ,如果需要更換資料庫時,就將 executeCmd 改寫即可。
當然,我們寫了更新產品,一定也要再補上查詢產品,但查詢因為有回傳值,無法直接套用更新的語法,只好另外寫了。讓我們建立一個 readCmd 方法,並將程式抽進去,回傳值先暫定為 DataTable ,有件事情要小心,原本程式採用 DataReader ,所以 parseDataToObject 也得改寫。程式如下:
private ProductValueObject parseDataToObject(DataRow row)
{
var returnObj = new ProductValueObject();
returnObj.ProductNo = row["ProductNo"].ToString();
returnObj.Ean13 = row["Ean13"].ToString();
returnObj.ProductType = row["ProductType"].ToString();
returnObj.Temp = row["Temp"].ToString();
returnObj.Expire = row["Expire"].ToString();
returnObj.Length = (double)row["Length"];
returnObj.Width = (double)row["Width"];
returnObj.Height = (double)row["Height"];
returnObj.Weight = (double)row["Weight"];
return returnObj;
}
private DataTable readCmd(string SqlCmd, Dictionary<string, string> param)
{
string connStr = ConfigurationManager.ConnectionStrings["DemoConnectionString"].ConnectionString;
DataTable dt = new DataTable();
using (SqlConnection conn = new SqlConnection(connStr))
{
SqlCommand cmd = new SqlCommand(SqlCmd, conn);
foreach (KeyValuePair<string, string> pair in param)
{
cmd.Parameters.AddWithValue("@" + pair.Key, pair.Value);
}
conn.Open();
SqlDataReader reader = cmd.ExecuteReader();
dt.Load(reader);
}
return dt;
}
public List<ProductValueObject> getProducts()
{
var returnList = new List<ProductValueObject>();
try
{
string SELECTCmd = "SELECT * FROM Product";
DataTable dt = this.readCmd(SELECTCmd, new Dictionary<string, string>());
foreach (DataRow row in dt.Rows)
{
var productItem = this.parseDataToObject(row);
returnList.Add(productItem);
}
}
catch (Exception ex)
{
// Do Something
}
return returnList;
}
public ProductValueObject getProduct(string ProductNo)
{
ProductValueObject obj = null;
try
{
string SELECTCmd = "SELECT TOP 1 * FROM Product WHERE ProductNo = @ProductNo";
Dictionary<string, string> dicParam = new Dictionary<string, string>();
dicParam.Add("ProductNo", ProductNo);
DataTable dt = this.readCmd(SELECTCmd, dicParam);
foreach(DataRow row in dt.Rows)
{
obj = this.parseDataToObject(row);
}
}
catch (Exception ex)
{
// Do Something
}
return obj;
}
至此,算是告一個小段落,將資料庫管理程式放到統一的 Method 中,商業邏輯只負責組查詢 SQL 及處理參數。但讓我們再做一個努力,根據「職責分離」概念,資料庫管理程式是一個泛用,且和商業邏輯無關的功能,值得為它抽出類別。首先我們將幾個類別更名,明確定義出這個類別的功能範疇,檔名也要記得改一下哦。
從public class DataAccess
改為 public class ProductManager
從 public class ProductValueObject
改為 public class ProductModel
再來,我們建立一個 SQLDataAccess 這個類別,路徑放在 Root > DataObject > SQLDataAccess.cs ,程式如下 (0.7~0.8) :
using System.Data;
using System.Data.SqlClient;
using System.Configuration;
using System.Collections.Generic;
namespace WebApplication1.DataObject
{
internal class SQLDataAccess
{
internal DataTable readCmd(string SqlCmd, Dictionary<string, string> param)
{
string connStr = ConfigurationManager.ConnectionStrings["DemoConnectionString"].ConnectionString;
DataTable dt = new DataTable();
using (SqlConnection conn = new SqlConnection(connStr))
{
SqlCommand cmd = new SqlCommand(SqlCmd, conn);
foreach (KeyValuePair<string, string> pair in param)
{
cmd.Parameters.AddWithValue("@" + pair.Key, pair.Value);
}
conn.Open();
SqlDataReader reader = cmd.ExecuteReader();
dt.Load(reader);
}
return dt;
}
internal void executeCmd(string CmdSQL, Dictionary<string, string> param)
{
string connStr = ConfigurationManager.ConnectionStrings["DemoConnectionString"].ConnectionString;
using (SqlConnection conn = new SqlConnection(connStr))
{
SqlCommand cmd = new SqlCommand(CmdSQL, conn);
foreach (KeyValuePair<string, string> pair in param)
{
cmd.Parameters.AddWithValue("@" + pair.Key, pair.Value);
}
conn.Open();
cmd.ExecuteNonQuery();
conn.Close();
}
}
}
}
同時, ProductManager 也改寫為:
using System;
using System.Data;
using System.Collections.Generic;
namespace WebApplication1.DataObject
{
public class ProductManager
{
private SQLDataAccess sqlDA = new SQLDataAccess();
private ProductModel parseDataToObject(DataRow row)
{
var returnObj = new ProductModel();
returnObj.ProductNo = row["ProductNo"].ToString();
returnObj.Ean13 = row["Ean13"].ToString();
returnObj.ProductType = row["ProductType"].ToString();
returnObj.Temp = row["Temp"].ToString();
returnObj.Expire = row["Expire"].ToString();
returnObj.Length = (double)row["Length"];
returnObj.Width = (double)row["Width"];
returnObj.Height = (double)row["Height"];
returnObj.Weight = (double)row["Weight"];
return returnObj;
}
public List<ProductModel> getProducts()
{
var returnList = new List<ProductModel>();
try
{
string SELECTCmd = "SELECT * FROM Product";
DataTable dt = this.sqlDA.readCmd(SELECTCmd, new Dictionary<string, string>());
foreach (DataRow row in dt.Rows)
{
var productItem = this.parseDataToObject(row);
returnList.Add(productItem);
}
}
catch (Exception ex)
{
// Do Something
}
return returnList;
}
public ProductModel getProduct(string ProductNo)
{
ProductModel obj = null;
try
{
string SELECTCmd = "SELECT TOP 1 * FROM Product WHERE ProductNo = @ProductNo";
Dictionary<string, string> dicParam = new Dictionary<string, string>();
dicParam.Add("ProductNo", ProductNo);
DataTable dt = this.sqlDA.readCmd(SELECTCmd, dicParam);
foreach (DataRow row in dt.Rows)
{
obj = this.parseDataToObject(row);
}
}
catch (Exception ex)
{
// Do Something
}
return obj;
}
public void updateProduct(string ProductNo, string Ean13, string ProductType, string Temp, string Expire, string Length, string Width, string Height, string Weight)
{
try
{
string UpdateCmd =
" UPDATE Product " +
" SET Ean13 = @Ean13, ProductType = @ProductType, Temp = @Temp, Expire = @Expire, " +
" Length = @Length, Width = @Width, Height = @Height, Weight = @Weight " +
" WHERE ProductNo = @ProductNo ";
Dictionary<string, string> paras = new Dictionary<string, string>();
paras.Add("Ean13", Ean13);
paras.Add("ProductType", ProductType);
paras.Add("Temp", Temp);
paras.Add("Expire", Expire);
paras.Add("Length", Length);
paras.Add("Width", Width);
paras.Add("Height", Height);
paras.Add("Weight", Weight);
paras.Add("ProductNo", ProductNo);
this.sqlDA.executeCmd(UpdateCmd, paras);
}
catch (Exception ex)
{
// Do Something
}
}
}
}
這個改寫完成了,好累啊,喘口氣。回顧一下我們的 ProductManager ,除了 SQL 語法外,終於完全沒有資料庫物件在裡面了 (DataTable 並不算是資料庫物件,它可以由程式塞值) 。還記得主題是「責任分離」分離嗎? 到現在才勉強達成「商業邏輯歸 Business 層,而資料庫連線則是放在 DataAccess 層」。
今天的程式碼量很多,但其實步驟並不複雜,一步步執行這些步驟,我們很確實的將 Webform 往現代化程式推進一點。程式碼量真的變多了嗎? 從長遠的眼光來看,當程式越能重用,以後撰寫相同程式碼就變少了,實際上抽出 Library 的目的也是這樣的,透過重用減少重寫,要記得重寫容易犯錯,但重用會讓我們使用已成熟的程式。
雖然離重構目標只剩下一點點,但放到下次吧。
下次,讓我們從 0.8 ~ 1 ,完成這次重構後,再來討論其它改造主題。來做個預告,下次內容是:「依設定讀取 Table 」、「切換資料庫」、「類別庫」。