三層架構中使用實體類填充泛型集合代替DataTable解決方案(ASP.NET+C#)

用三層架構開發項目,經常會遇到如下場景:

      D層負責與數據庫交互,一般是得到DataTable或DataSet對象,然後返回給B層,B層進行類似的處理來讀取數據:dt.Rows[0][“xxx”];或者dt.Rows[0][1];(強烈不建議使用)。

      有時DataTable也會被傳到UI層,與控件進行綁定,顯示數據。例如ASP.NET的repeater控件提取數據:<%# Eval(“xxx”)%>。

      無論是何種情況,使用DataTable不可避免的要填寫讀取的字段,這樣做的壞處不言而喻:

      |  非常容易寫錯,而且編譯器不檢查。

      |  必須瞭解數據庫的結構。

      |  不符合面向對象編程思想。

      |  DataTable爲弱類型,無法直觀的看出字段的數據類型。

      這個問題一直困擾着我,這次藉着重構機房收費系統的機會,研究了一下這個問題,找到了一種比較好的解決方案:在D層把DataTable轉換成單個實體類,再把實體類填充到泛型集合中。

 

核心思想如圖:

 

      實體類即數據庫的映射,因此實體類中的屬性和數據庫表中的字段是相對應的。把DataTable中的每一行記錄視爲一個實體類,把其中的字段讀取出來,存放到實體類的屬性中,再把所有的實體類存在泛型集合中。因此,DataTable中有多少個記錄,泛型集合中就有多少個實體類,每個實體類的屬性和DataTable的字段是相對應的。

      試想一下,這樣一來,傳到B層或U層的將是一個實體類集合,讀取數據將會是如下場景:list[0].xxx;

      這樣做的優點如下:

      |  編寫B層的人員無需手動填寫需要的字段,直接按一下點,全都提示出來了,想用哪個用哪個,不會出現寫錯的情況。

      |  不必瞭解數據庫結構。

      |  符合面向對象思想。

      |  實體類的屬性是強類型,每個字段的類型都是已知的。

 

      那麼用代碼如何實現呢?下面一一列舉。

      注意:以下代碼僅僅是爲了模擬,有些不規範的地方,代碼註釋中有提示,切勿模仿!

 

實體類代碼:


[csharp] 
using System; 
using System.Collections.Generic; 
using System.Linq; 
using System.Text; 
 
namespace Entity 

    /// <summary> 
    /// Users表實體類 
    /// </summary> 
    public class Users 
    { 
        private long _id; 
        private string _userName; 
        private string _passWord; 
 
        public long id  
        { 
            set {_id = value; } 
            get { return _id; } 
        } 
 
        public string userName  
        { 
            set { _userName = value; } 
            get { return _userName; } 
        } 
 
        public string passWord  
        { 
            set { _passWord = value; } 
            get { return _passWord; } 
        } 
    } 

 

將DataTable轉換成List<T>泛型集合助手類,這個類我放在了Entity實體類層中:


[csharp] 
using System; 
using System.Collections.Generic; 
using System.Linq; 
using System.Text; 
using System.Data; 
using System.Data.SqlClient; 
using System.Reflection; 
 
namespace Entity 

    /// <summary> 
    /// 將DataTable轉換成泛型集合IList<>助手類 
    /// </summary> 
    public class ConvertHelper 
    { 
        /// <summary> 
        /// 單表查詢結果轉換成泛型集合 
        /// </summary> 
        /// <typeparam name="T">泛型集合類型</typeparam> 
        /// <param name="dt">查詢結果DataTable</param> 
        /// <returns>以實體類爲元素的泛型集合</returns> 
        public static IList<T> convertToList<T>(DataTable dt) where T : new() 
        { 
            // 定義集合 
            List<T> ts = new List<T>(); 
 
            // 獲得此模型的類型 
            Type type = typeof(T); 
            //定義一個臨時變量 
            string tempName = string.Empty; 
            //遍歷DataTable中所有的數據行  
            foreach (DataRow dr in dt.Rows) 
            { 
                T t = new T(); 
                // 獲得此模型的公共屬性 
                PropertyInfo[] propertys = t.GetType().GetProperties(); 
                //遍歷該對象的所有屬性 
                foreach (PropertyInfo pi in propertys) 
                { 
                    tempName = pi.Name;//將屬性名稱賦值給臨時變量   
                    //檢查DataTable是否包含此列(列名==對象的屬性名)     
                    if (dt.Columns.Contains(tempName)) 
                    { 
                        // 判斷此屬性是否有Setter   
                        if (!pi.CanWrite) continue;//該屬性不可寫,直接跳出   
                        //取值   
                        object value = dr[tempName]; 
                        //如果非空,則賦給對象的屬性   
                        if (value != DBNull.Value) 
                            pi.SetValue(t, value, null); 
                    } 
                } 
                //對象添加到泛型集合中 
                ts.Add(t); 
            } 
 
            return ts;   
        } 
    } 

ASP.NET Web頁面後臺代碼(aspx.cs):


[csharp]
using System; 
using System.Collections.Generic; 
using System.Linq; 
using System.Web; 
using System.Web.UI; 
using System.Web.UI.WebControls; 
using Entity; 
using System.Data; 
using System.Data.SqlClient; 
 
public partial class _Default : System.Web.UI.Page 

    protected void Page_Load(object sender, EventArgs e) 
    { 
        if (!Page.IsPostBack)  
        { 
            DataTable dt = new DataTable(); 
            IList<Users> users = new List<Users>(); 
            
            //注意:只是爲了演示,纔在這讀數據庫的! 
            //在項目中千萬不要這麼做!另外sql鏈接字符串也不應該寫在程序中,而且要加密。 
            SqlConnection sqlCon = new SqlConnection("server=192.168.24.177;database=testDB;uid=sa;pwd=123"); 
            SqlCommand sqlCmd = new SqlCommand("select userName,passWord from t_testTable", sqlCon); 
            SqlDataReader sqlRd; 
            sqlCon.Open(); 
            sqlRd = sqlCmd.ExecuteReader(); 
            dt.Load(sqlRd); //DataTable構造完成,在這僅僅是爲了演示,構造DataTable 
            sqlCon.Close(); 
 
            users = ConvertHelper.convertToList<Users>(dt); //獲取DataTable轉換成的泛型集合 
            repUsers.DataSource = users; 
            repUsers.DataBind(); 
        } 
    } 

ASP.NET Web頁面前臺代碼(aspx):
[html] 
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default.aspx.cs" Inherits="_Default" %> 
 
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> 
 
<html xmlns="http://www.w3.org/1999/xhtml"> 
<head runat="server"> 
    <title></title> 
</head> 
<body> 
    <form id="frmDefault" runat="server"> 
        <div>  
            <asp:Repeater ID="repUsers" runat="server"> 
                <ItemTemplate> 
                    <table> 
                        <tr> 
                            <td><%# ((Entity.Users)Container.DataItem).id.ToString() %></td> 
                            <td><%# ((Entity.Users)Container.DataItem).userName %></td> 
                            <td><%# ((Entity.Users)Container.DataItem).passWord %></td> 
                        </tr> 
                    </table> 
                </ItemTemplate> 
            </asp:Repeater> 
        </div> 
    </form> 
</body> 
</html> 

      這段代碼在Web後臺讀取了數據中的數據,當然,只是爲了模擬,這樣寫是非常不可取的。得到DataTable後利用ConvertHelper轉換助手類的convertToList方法將DataTable轉換成本例中的Users類型的泛型集合。最後把該集合與Repeater控件綁定,顯示數據。

      Repeater之類的控件均支持綁定集合,而且支持的如此完美。個人認爲界面顯示那段代碼是最漂亮的。

      例如:<%#((Entity.Users)Container.DataItem).id.ToString() %>。這和以往的顯示方法大不相同。首先,沒有使用Eval。數據是從Container.DataItem屬性中讀取的,強制轉換爲實體類類型,然後直接調用實體類的id屬性。整個代碼行雲流水般使用,完全不需要參照數據庫,想用哪個屬性用哪個。另外,這樣讀取效率比用Eval高。

      在此,需要說明的是,我在這寫的DataTable到List<T>的類是比較可靠的。類似ConvertHelper.convertToList<Users>(dt),想把DataTable轉換成哪種實體類,調用方法的時候泛型參數(尖括號裏的)就寫哪個實體類,在這是Users。簡單說一下它的轉換原理。

      它會自動獲取實體類的屬性名,然後DataTable中匹配有沒有該名稱的字段,有的話就賦值。DataTable中的每一行記錄都這樣處理。

      基於以上原理,使用本方法必須滿足以下條件,也可以說是注意事項吧:

      |  實體類的屬性名必須和數據庫表中的字段名一模一樣。

      |  想用這種方法把DataTable轉換成實體類,必須有已知的實體類和DataTable中的數據想對應,也就是說必須明確你要轉換成的實體類類型,否則沒辦法指定泛型集  合的類型,也就沒辦法調用。

      |  各個表的字段儘量區別開來,不要相同。比如A表有個name字段,B表也有一個name字段,就不合理了。

       

      相信你看到這已經躍躍欲試了。可仔細思考一下,會發現一個大問題:這裏涉及的僅僅是單表查詢,聯合查詢顯然搞不定。

      聯合查詢搞不定的主要原因是它的數據來自多個表,沒有實體類與之對應,沒有實體類就沒有辦法轉換。

      目前比普遍的解決方案就是創建一個包含多個表的實體類,只要實體類中包含聯合查詢的這些表就可以,實體類中表示的表多了沒事。但是,很多種聯合查詢,就要建立很多實體類,這也是沒有辦法的事。

      也有比較變態的方法就是創建一個通用實體類,包含所有表的字段。

      要想用此方法,創建新的實體類是必然的了。但即使是這樣,我們也可以節省代碼,避免重複。

      節省代碼首先想到的是繼承,可惜C#不支持多重繼承,兩個以上實體類組合成一個實體類就沒轍了。用接口雖然可以實現多重繼承,但顯然不合理,接口不是幹這個用的,這樣一點代碼也沒省下,反而麻煩了。

      經過網友RenYue的提示,用聚合解決是種比較好的方案。

      聚合核心思想是:假如五個表聯合查詢,那麼新建一個實體類,這個新實體類的屬性是這個五個表對應的實體類,即用實體類封裝實體類,我們暫且叫它聚合實體類。

      這個想法很好,很好的複用了代碼,但是實現起來比較複雜。剛剛提供的單個表查詢的轉換方法(convertToList),顯然滿足不了。

      經過研究,我確定沒辦法寫一個通用的多表查詢轉換方法。但是可以實現確定個數的轉換方法。我寫了一個兩個表聯合查詢的轉換方法。

      把DataTable每一行記錄中的字段分解給所屬表對應的實體類,然後把這些實體類橫向聚合到聚合實體類的屬性中。如圖:

 

爲了演示,程序做如下變動:

 

在Entity項目中添加一個LoginLog類,用戶登錄日誌:


[csharp]
using System; 
using System.Collections.Generic; 
using System.Linq; 
using System.Text; 
 
namespace Entity 

    /// <summary> 
    /// LoginLog表實體類 
    /// </summary> 
    public class LoginLog 
    { 
        private long _id; 
        private string _userName; 
        private DateTime _loginTime; 
        private DateTime _logoutTime; 
 
        public long id  
        { 
            get { return _id; } 
            set { _id = value; } 
        } 
 
        public string userName 
        { 
            get { return _userName; } 
            set { _userName = value; } 
        } 
 
        public DateTime loginTime  
        { 
            get { return _loginTime; } 
            set { _loginTime = value; } 
        } 
 
        public DateTime logoutTime 
        { 
            get { return _logoutTime; } 
            set { _logoutTime = value; } 
        } 
    } 

在Entity項目中添加一個UsersJionLog類,該實體類用於Users表和LoginLog表聯合查詢,它的屬性是Users類和LoginLog類:


[csharp] 
using System; 
using System.Collections.Generic; 
using System.Linq; 
using System.Text; 
 
namespace Entity 

    /// <summary> 
    /// UsersJionLog表實體類,該實體類用於Users表和LoginLog表聯合查詢 
    /// </summary> 
    public class UsersJionLog 
    { 
        private Users _users; 
        private LoginLog _loginLog; 
 
        public Users users  
        { 
            set { _users = value; } 
            get { return _users; } 
        } 
 
        public LoginLog loginLog 
        { 
            set { _loginLog = value; } 
            get { return _loginLog; } 
        } 
    } 

在ConvertHelper類中添加兩個表聯合查詢的轉換方法:

[csharp]
/// <summary> 
/// 兩表查詢結果轉換成泛型集合 
/// </summary> 
/// <typeparam name="T">包含了兩個表的實體類,聚合實體類</typeparam> 
/// <typeparam name="U">實體類中的第一個實體類</typeparam> 
/// <typeparam name="V">實體類中的第二個實體類</typeparam> 
/// <param name="dt">從數據庫中查詢獲得的DataTable</param> 
/// <returns>以實體類爲元素的泛型集合</returns> 
public static IList<T> convertToJiontList<T, U, V>(DataTable dt) 
    where T : new() 
    where U : new() 
    where V : new() 

    //定義一個聚合實體類泛型集合,用來做返回值 
    List<T> ts = new List<T>(); 
    //定義一個泛型集合元素,用來填充集合 
    T t = new T(); 
    //獲取聚合實體類的屬性 
    PropertyInfo[] propertys = t.GetType().GetProperties(); 
    //利用單錶轉換,把DataTable數據填充到聚合實體類中第一個實體類 
    IList<U> uList = new List<U>(); 
    uList = convertToList<U>(dt); 
    //利用單錶轉換,把DataTable數據填充到聚合實體類中第二個實體類 
    IList<V> vList = new List<V>(); 
    vList = convertToList<V>(dt); 
 
    //經過以上兩步,uList和vList兩個集合的長度肯定是相同的 
 
    //把兩個實體類填充到聚合實體類中 
    for (int i = 0; i < uList.Count; i++)  
    { 
        propertys[0].SetValue(t, uList[i], null); //取uList中第i個元素,填充到聚合實體類的第一個屬性中 
        propertys[1].SetValue(t, vList[i], null); //取vList中第i個元素,填充到聚合實體類的第二個屬性中 
        ts.Add(t); //向聚合實體類集合中添加元素 
    } 
    return ts; 

ASP.NET Web後臺(aspx.cs)代碼:


[csharp] 
using System; 
using System.Collections.Generic; 
using System.Linq; 
using System.Web; 
using System.Web.UI; 
using System.Web.UI.WebControls; 
using Entity; 
using System.Data; 
using System.Data.SqlClient; 
 
public partial class _Default : System.Web.UI.Page 

    protected void Page_Load(object sender, EventArgs e) 
    { 
        if (!Page.IsPostBack)  
        { 
            DataTable dt = new DataTable(); 
            IList<UsersJionLog> usersJionLog = new List<UsersJionLog>(); 
 
            //注意:只是爲了演示,纔在這讀數據庫的! 
            //在項目中千萬不要這麼做!另外sql鏈接字符串也不應該寫在程序中,而且要加密。 
            SqlConnection sqlCon = new SqlConnection("server=192.168.24.177;database=testDB;uid=sa;pwd=123"); 
            SqlCommand sqlCmd = new SqlCommand("select t_Users.userName,t_LoginLog.loginTime,t_LoginLog.logoutTime from t_Users join t_LoginLog on t_LoginLog.userName = t_Users.userName where t_Users.userName = 'admin'", sqlCon); 
            SqlDataReader sqlRd; 
            sqlCon.Open(); 
            sqlRd = sqlCmd.ExecuteReader(); 
            dt.Load(sqlRd); //DataTable構造完成,在這僅僅是爲了演示,構造DataTable 
            sqlCon.Close(); 
 
            usersJionLog = ConvertHelper.convertToJiontList<UsersJionLog,Users,LoginLog>(dt); 
 
            repUsers.DataSource = usersJionLog; 
            repUsers.DataBind(); 
        } 
    } 

ASP.NET Web前臺(aspx):


[html]
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default.aspx.cs" Inherits="_Default" %> 
 
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> 
 
<html xmlns="http://www.w3.org/1999/xhtml"> 
<head runat="server"> 
    <title></title> 
</head> 
<body> 
    <form id="frmDefault" runat="server"> 
        <div>  
            <asp:Repeater ID="repUsers" runat="server"> 
                <ItemTemplate> 
                    <table> 
                        <tr> 
                            <td><%# ((Entity.UsersJionLog)Container.DataItem).users.userName %></td> 
                            <td><%# ((Entity.UsersJionLog)Container.DataItem).loginLog.loginTime.ToString() %></td> 
                            <td><%# ((Entity.UsersJionLog)Container.DataItem).loginLog.logoutTime.ToString() %></td> 
                        </tr> 
                    </table> 
                </ItemTemplate> 
            </asp:Repeater> 
        </div> 
    </form> 
</body> 
</html> 

      這樣就可以啦。

      需要解釋的有兩個地方:

      ConvertHelper.convertToJiontList<UsersJionLog,Users,LoginLog>(dt);這句調用代碼,注意該方法需要三個類型,這三個類型第一個是返回的實體類類型,也就是聚合實體類;第二個參數是聚合實體類中的第一個屬性的類型;第三個參數是聚合類中第二個屬性的類型。必須一一對應,否則沒法轉換!

      另外Web界面上的調用代碼,例如<%#((Entity.UsersJionLog)Container.DataItem).users.userName %>,這句話就是先把Container.DataItem轉換成聚合實體類UsersJionLog,然後讀取聚合實體類中的users屬性,這個屬性也是一個類(Users類),因此有自己的屬性userName。

      這樣就實現了多表聯合查詢的轉換。聚合類代表了聯合查詢的所有表,想讀取其中的哪個表,點一下選出來,然後再點一下就可以讀出這個表中的字段了,相當好用!

      根據兩個表聯合查詢的轉換思路,我們可以寫出N個表聯合查詢的轉換方法,但是沒辦法通用,因爲泛型的的類型必須是已知的,相信讀者可以體會到。

 

      寫的比較倉促,希望對大家有所幫助!

      有問題歡迎和我交流!

 

附:

 

本文所用數據庫創建腳本:


[sql] 
create database testDB; 
use testDB; 
create table t_Users( 
    id bigint IDENTITY(1,1) PRIMARY KEY, 
    userName varchar(100) NOT NULL UNIQUE, 
    passWord varchar(100) NOT NULL 
); 
insert into t_Users(userName,passWord) values('admin','123'); 
insert into t_Users(userName,passWord) values('guest','456'); 
 
create tabLe t_LoginLog( 
    id bigint IDENTITY(1,1) PRIMARY KEY, 
    userName varchar(100), 
    loginTime datetime NOT NULL, 
    logoutTime datetime NOT NULL, 
    CONSTRAINT FK_LoginLog_userName FOREIGN KEY(userName) REFERENCES t_Users(userName) 
); 
 
insert into t_LoginLog(userName,loginTime,logoutTime)values('admin','2012-7-30','2012-7-30'); 
insert into t_LoginLog(userName,loginTime,logoutTime)values('guest','2012-8-30','2012-8-30'); 
insert into t_LoginLog(userName,loginTime,logoutTime)values('admin','2012-9-30','2012-9-30'); 
insert into t_LoginLog(userName,loginTime,logoutTime)values('guest','2012-10-30','2012-10-30'); 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章