以往接到專案時基本沒去處理內部的某些不是很優的寫法,如今稍有空暇整理一下,並提出分享,以產生兩層 Navigation Bar的程式碼為例子,並提供比較。
程式的優化很大一塊都是在於I/O讀取上的優化,分散的多次讀取會造成運作效率的低落,當資料龐大或系統壅塞時更可見其效益,今天以一個簡單的例子說明一下,主要使用的語法是SQL和LinQ。
// 用此物件來讀取 Menu + Item
// Num:編號(PK) Title:名稱 Url:連結(Menu層為空) Root:層次 0代表Menu 數字代表為 item Ran:排序
class MenuItem
{
public MenuItem()
{
}
public MenuItem(int Num_, string Ti_, string Url_, int Root_, int Ran_)
{
Num = Num_;
Title = Ti_;
Url = Url_;
Root = Root_;
Ran = Ran_;
}
public int Num { get; set; }
public string Title { get; set; }
public string Url { get; set; }
public int Root { get; set; }
public int Ran { get; set; }
}
資料表說明:
--選單選項
CREATE Table Menus(
Num identity(1,1) int,
Title Nvarchar(30) not null,
URL Nvarchar(180) null,
Root int not null,
Range int not null
)
-- 權限群組
CREATE Table Group(
(PK)g_name Nvarchar(5) not null,
ViewItems Nvarchar(Max) not null, -- 可檢視頁面 存檔資料為 int的文字串 例如: "3,5,9,125,13,17,26,235,152"
EditItems Nvarchar(Max) not null -- 有編輯權限頁面 存檔資料為 int的文字串 例如: "3,5,9,125,13,17,26,235,152"
)
-- 使用者
CREATE Table Users(
(PK)u_id Nvarchar(30) not null,
[Power] Nvarchar(5) not null
)
原本的寫法是兩個迴圈,大圈包小圈做SQL的資料讀取,大圈讀Menu 小圈讀Item,每次只讀取一行,這方法實在是不優,每次要開啟SQL Profile來除錯時,總是看到一大堆沒意義的查詢造成除錯非常麻煩,既然不是好方法,就不攤開展示是什麼了。
// 將結果封裝於 子程序中 以 uid (使用者名稱) 讀取出該權限可以進入的群組清單內 用子查詢的方式把 Menus + Items 一次全部取出到物件
publc void BuildNavBar(string uid)
{
List<MenuItem> menuItems = new List<MenuItem>();
string Query = "SELECT Num, Title, Url, Root, Range FROM Menus i WHERE Num in (SELECT value FROM STRING_SPLIT((SELECT ViewItems FROM Group g WHERE g.g_name= (select [Power] FROM Users a WHERE u_id=@uid)),',')) ORDER BY Root ,Range;";
string Link="", NavBar = "";
using(SqlConnection Conn = new SqlConnection(db))
{
try
{
SqlCommand sqa = new SqlCommand(Query, Conn);
sqa.Parameters.Add(new SqlParameter("@uid", SqlDbType.NVarChar) { Value = uid });
SqlDataReader Sr = sqa.ExecuteReader();
while (Sr.Read())
{
// 用此方法排除 Null資料的問題
Link = DBNull.Value.Equals(Sr["url"]) ? "" : Sra.GetString(Sr.GetOrdinal("url"));
menuItems.Add(
new MenuItem(
Sr.GetInt32(Sr.GetOrdinal("num")),
Sr.GetString(Sr.GetOrdinal("title")), Link,
Sr.GetInt32(Sr.GetOrdinal("root")),
Sr.GetInt32(Sr.GetOrdinal("range"))));
}
}
catch(Exception ex)
{
Literal1.Text = ex.ToString();
return;
}
}
// 讀取 Menu 層 (Root = 0)
var Menu = from m in menuItems where m.Root == 0 orderby m.Ran select m;
foreach (MenuItem it in Menu)
{
NavBar += @"<li class=""nav-item dropdown"">
<a class=""nav-link dropdown-toggle"" href=""#"" id=""navbarDropdownMenuLink" + it.Num.ToString() + @""" role=""button"" data-toggle=""dropdown"" aria-haspopup=""true"" aria-expanded=""false"">" + it.Title + @"</a>
<div class=""dropdown-menu"" aria-labelledby=""navbarDropdownMenuLink"">
";
// 讀取 Items層 (Root = Menu層的Num )
var Items = from i in menuItems where i.Root == it.Num orderby i.Ran select i;
foreach (MenuItem item in Items)
{
NavBar += "<a class=\"dropdown-item\" href=\"" + ResolveUrl(item.Url) + "\">" + item.Title + @"</a>
";
}
NavBar += @"</div></li>
";
}
// 轉換成Bootstrap的 NavBar 寫入到Literal物件中,溶入原始碼,忠實呈現所有的Tag內容。
Literal1.Text = NavBar;
}
最後,實測的平均載入時間由995毫秒變成520毫秒,本例的Menu+Items一共有86個 15組,舊方法是讀取Menu+Items一共86次,當然也有讀取16次的方法,也算得上是優化,以本文所提出新的寫法只需要讀取一次,然後用物件化查詢方式把後續的切割與分段都全數搞定,馬上就節省了85次IO動作,如果同一時間有300個用戶在線上的話,其節省IO的作法的效益更巨大。