1. 程式人生 > >基於物件的資料篩選與排序(一)

基於物件的資料篩選與排序(一)

可能大家對於資料庫的操作太過於熟悉了,以至於忘記.Net提供的強大而靈活的資料操作。例如,當我們想對資料進行篩選時,首先想到的是“Where”而不是List< T>.FindAll();當我們想對資料進行排序時,首先想到的是“Sort”而不是List< T>.Sort();當我們想對資料進行分頁時,首先想到的是儲存過程,而不是List< T>.GetRange()。。。
當然在這裡並不是要指明資料庫的直接操作不夠好,在資料量比較大的時候,資料庫的直接操作效率確實很高,然而在對較少資料進行操作時,一次性取出資料然後快取在伺服器上,這對於以後的排序、篩選、分頁等操作直接對快取進行,則會使效率提高很多。
方法不是絕對的,也並沒有絕對的更優更劣,這裡只是提供了不同的思路,具體的方法選用還是得根據實際情況來進行,所以在這裡筆者詳細介紹下.Net本身強大的物件資料操作。

首先我們在Web介面上放置這些控制元件,如下圖:
這裡寫圖片描述

在這裡的資料我使用了之前一個農田資料採集的資料庫,具體的欄位有:id編號,光照度,溫度,導電度,溼度,所屬農田編號和記錄時間。
作為樣本,我們現在想實現的效果是,根據時間展示4個記錄資料就好。

基於SQL的篩選

首先我們最為熟悉和第一反應肯定是用資料庫進行篩選操作,我們建立資料表對應的業務物件AgriData

 public class AgriData:IData
    {   
        public int Tem { get; set; }
        public int Ele { get
; set; } public int Sun { get; set; } public int Water { get; set; } public int AgriDataBelong { get; set; } public DateTime Date { get; set; } }

對於採集的資料這裡使用List< AgriData>進行儲存。接下來我們建立一個SqlAgriDataManager類進行資料的儲存工作,並返回List< AgriData>。SqlAgriDataManager的實現思路通常如下:

 public class SqlAgriDataManager
    {
        //填充List<AgriData>並返回
        public static List<AgriData> GetList(string query)
        {
            List<AgriData> list = null;

            SqlDataReader reader = ExcuteReader(query);
            if (reader.HasRows)
            {
                list = new List<AgriData>();
                while (reader.Read())
                    list.Add(GetItem(reader));
            }

            reader.Close();
            return list;
        }

        //資料庫讀取資料返回SqlDataReader
        private static SqlDataReader ExcuteReader(string query)
        {
            string strCon = ConfigurationManager.ConnectionStrings["db_AgricultureConnectionString"].ConnectionString;
            SqlConnection con = new SqlConnection(strCon);       
            SqlCommand com = new SqlCommand(query,con);
            con.Open();

            SqlDataReader reader = com.ExecuteReader(CommandBehavior.CloseConnection);
            return reader;
        }

        //將讀取的資料進行封裝
        private static AgriData GetItem(SqlDataReader record)
        {
            AgriData agr = new AgriData();
            agr.Tem = Convert.ToInt32(record["AgriDataTem"]);
            agr.Sun = Convert.ToInt32(record["AgriDataSun"]);
            agr.Ele = Convert.ToInt32(record["AgriDataEle"]);
            agr.Water = Convert.ToInt32(record["AgriDataWater"]);

            return agr;
        }
    }

這段程式碼理解也比較容易,首先連線資料庫,執行相應的篩選語句返回一個儲存了資料的SqlDataReader物件,接著將每個資料中列值封裝到AgriData物件中,逐個填充最終返回List< AgriData>物件。
接下要做的便是提供ObjectDataSource的資料來源,也就是我們剛才獲取的List< AgriData >集合,很顯然我們要在頁面上呼叫的便是GetList方法,具體的頁面檔案index.aspx程式碼如下:

 <asp:ObjectDataSource ID="ObjectAgrList" runat="server" SelectMethod="GetList" TypeName="Manager.SqlAgriDataManager" OnSelecting="ObjectAgrList_Selecting">
                                <SelectParameters>
                                    <asp:Parameter Name="query" Type="String" />
                                </SelectParameters>
                            </asp:ObjectDataSource>

ObjctDataSource使用GetList()方法作為SelectCommand(注意要使用靜態方法),ObjectDataSource的ID將會用於GridView的DataSourceID。
好的接下來我們進行後臺操作,即查詢條件——時間的拼裝(這裡資料庫中的時間並沒有用通常的DateTime型別,而是vchar),我們來看一下具體實現:

public partial class index : System.Web.UI.Page
    {
        //獲取下拉列表Year的值
        public int Year {
            get { return Convert.ToInt32(ddlistYear.SelectedValue); 
            }

        //獲取下拉列表Month的值
        public int Month {
            get { return Convert.ToInt32(ddlistMounth.SelectedValue); }
        }

        //獲取下拉列表Day的值
        public int Day {
            get { return Convert.ToInt32(ddlistDay.SelectedValue); }
        }

        //拼裝Sql語句
        public string QuerySql 
        {
            get
            {
                int year = Year;
                int mounth = Month;
                int day = Day;

                string str = string.Empty;

                if (year != 0)
                    str += year.ToString() + "/";
                if (mounth != 0)
                    str += mounth.ToString() + "/";
                if (day != 0)
                    str += day.ToString() + " ";

                return "select AgriDataTem,AgriDataEle,AgriDataSun,AgriDataWater from AgriData where " +
                    "AgriDataTime like '" + str + "%'";
            }
        }

        //頁面載入事件
        protected void Page_Load(object sender, EventArgs e)
        {
            if (!IsPostBack)
            {
                AppedListItem(ddlistMounth, 12);
                AppedListItem(ddlistDay, 30);
            }
        }

        protected void AppedListItem(DropDownList list,int end)
        {
            for (int i = 1; i <= end; i++)
            {
                list.Items.Add(new ListItem(i.ToString()));
            }
        }

        protected void ddlistYear_SelectedIndexChanged(object sender, EventArgs e)
        {
            gvAgriculture.DataBind();
        }

        protected void ddlistMounth_SelectedIndexChanged(object sender, EventArgs e)
        {
            gvAgriculture.DataBind();
        }

        protected void ddlistDay_SelectedIndexChanged(object sender, EventArgs e)
        {
            gvAgriculture.DataBind();
        }

        //每個列表的回發都很會觸發gvAgriculture.DataBind(),然後出發這裡
        protected void ObjectAgrList_Selecting(object sender, ObjectDataSourceSelectingEventArgs e)
        {
            e.InputParameters["query"] = this.QuerySql;
        }
    }

這段程式碼中Year、Month、Day分別對應3個DropDownList控制元件的SelectedValue,同時用AppendListItem方法對月和日控制元件賦初值(年列表直接賦予了2017,當然為了簡便,這裡沒有對不同月的天數進行處理,直接為30天),在每個下拉列表SelectedIndex發生變化時,對GridView控制元件進行資料繫結,在回發過程中觸發新的查詢操作即ObjectDataSource的Selecting事件,我們用一個按鈕來輔助做回發操作。
基本的基於資料庫的操作過程便是如此,執行之後得到到的效果如下圖:
這裡寫圖片描述

基於物件的篩選

上面我們演示了傳統的SQL的資料篩選操作,那麼在此基礎上是怎樣進行基於物件的篩選的,又是怎樣提升效能的(沒有優化的操作就不會有被推廣的意義)呢?
同樣,我們沿用剛才所建立的控制元件進行操作,在本例中基於物件的篩選就是對List< AgriData>的篩選。實現的思路也並不難,首先建立一個過載的GetList(下篇程式碼直接新建了一個類來實現該方法,未在原SqlAgriDataManager類中實現過載)方法,然後取出所有的AgriData並新增到快取中,然後建立一個新的List< AgriData>,將快取中的所有資料遍歷,將符合要求的項新增到該List< AgriData>中,最後再返回該集合,從而實現了相關的篩選操作。程式碼如下(為了利於區別,在這裡新建了一個類):

public class ObjAgriDataManager
    {
        public static List<AgriData> GetList()
        {
            List<AgriData> list = HttpContext.Current.Cache["AgriList"] as List<AgriData>;

            if (list == null)
            {
                list = SqlAgriDataManager.GetList("select AgriDataTem,AgriDataEle,AgriDataSun,AgriDataWater from AgriData");
                HttpContext.Current.Cache.Insert("AgriList", list);
            }

            return list;
        }

        public static List<AgriData> GetList(List<AgriData> agriList,int year,int month,int day)
        {
            List<AgriData> list = null;
            bool canAdd;

            //將從快取中提取的List<AgriData>根據年月日篩選出來
            if (agriList != null)
            {
                list = new List<AgriData>();

                foreach (AgriData n in agriList)
                {
                    canAdd = true;

                    //為0時即時間段都符合要求
                    if (year != 0 && year != n.Date.Year)
                        canAdd = false;
                    if (month != 0 && month != n.Date.Month)
                        canAdd = false;
                    if (day != 0 && day != n.Date.Day)
                        canAdd = false;

                    if (canAdd) list.Add(n);
                }
            }

            return list;
        }

OK,我們來仔細看一下這個程式碼,無參的GetList方法在無快取情況下通過SqlAgriDataManager中的GetList方法執行sql語句將得到的資料一次性快取到快取中,在有快取資料情況下直接使用快取中資料。第二個GetList方法中通過輸入的年、月、日對agriList進行篩選,從而返回篩選後的集合物件。
很顯然,上面的方法擴充套件性是很差的,現在是根據年、月、日查詢,那有需要根據所屬農田Id查詢時,又需要對該方法進行修改,或者再寫一個過載方法,這顯然不符合面向物件設計模式,因為程式碼沒有得到重用。
實際上,.Net框架已經為這些問題做好了解決方案,在List< T>上提供了一個FindAll(Predicate< T> math)方法進行篩選工作,Predicate< T>是一個泛型委託:

public delegate bool Predicate<T>(T obj)

因此math引數是一個返回bool型別並且只有一個傳遞引數的方法,在FindAll()內部再將這個方法傳遞進去。
現在我們要做的工作就是完成Predicate< T>封裝的篩選規則,和定義Predicate< T>委託的方法。

public static List<AgriData> GetList(List<AgriData> agriList,int year,int month,int day)

顯然這裡的篩選條件為了更好的擴充套件性需要進行變更,我們可以定義一個泛型資料篩選類DataFilter< T>來進行篩選條件的設定,於是這個GetList方法變為:

public static List<AgriData> GetList(List<AgriData> agriList, DataFilter<AgriData> filter)

那麼具體的這個DataFilter的設計思路是怎樣的呢?
考慮到Predicate< T>只能傳遞一個引數,我們用資料物件作為引數即這裡的AgriData業務物件進行引數傳遞,於是DataFilter< T>這個類和DataFilter< T>中的bool型篩選方法就應該是這樣定義的:

public class DataFilter<T> where T : AgriData
    {  
        public bool MatchRule(T param)
        {
            if (year != 0 && year != param.Date.Year) return false;
            if (month != 0 && month != param.Date.Month) return false;
            if (day != 0 && day != param.Date.Day) return false;

            return true;
        }
    }

因為year,month,day是比較通常的查詢操作,為了便於封裝和實現這個查詢約束,我們這裡定義一個介面,僅含有一個DateTime型別的Date屬性,對於所有實現了該介面的類,都可以使用上面的篩選方法(一個不包含年、月、日的類顯然不符合這裡的篩選條件)。

 public interface IData
    {
        DateTime Date { get; set;}
    }

同時對AgriData類進行修改,讓他實現這個介面:

public class AgriData:IData

好了,有了這樣的約束介面,我們可以將DataFilter的約束條件更改為IData,同時我們完善該篩選類的具體程式碼:

public class DataFilter<T> where T : IData
    {
        private int year, month, day;

        public DataFilter(int year, int month, int day)
        {
            this.year = year;
            this.month = month;
            this.day = day;
        }

        //方便使用的一組建構函式
        public DataFilter(DateTime date) : this(date.Year, date.Month, date.Day) { }
        public DataFilter(int year, int month) : this(year, month, 0) { }
        public DataFilter(int year) : this(year, 0, 0) { }
        public DataFilter() : this(0, 0, 0) { }

        //基於對時間篩選的基本邏輯
        public bool MatchRule(T param)
        {
            if (year != 0 && year != param.Date.Year) return false;
            if (month != 0 && month != param.Date.Month) return false;
            if (day != 0 && day != param.Date.Day) return false;

            return true;
        }
    }

我們回到之前的問題,資料篩選不單單隻要篩選出時間符合條件的問題,如還要篩選符合的所屬農田Id咋辦,我們工作到了這裡,應該很容易聯想到DataFilter< T>應該作為一個篩選類的基類,同時應將MathRule方法作為可重寫方法,這樣更利於子類的相關實現。於是便有了這樣的修改:

 public virtual bool MatchRule(T param) {}

接下來我們來看一下對所屬農田進行的篩選時如何進行的:

public class AgriDataFilter : DataFilter<AgriData>
    {
        private int agriDataBelong;

        //同時對時間和所屬農田id的查詢賦值 
        public AgriDataFilter(int year, int month, int day, int id)
            : base(year, month, day)
        {
            this.agriDataBelong = id;
        }

         public override bool MatchRule(AgriData param)
        {
            bool result = base.MatchRule(param);

            //0
            if (agriDataBelong == 0 || agriDataBelong == param.AgriDataBelong) return true;

            return result;
        }
    }

現在ObjAgriDataManager中的GetList方法也顯而易見了:

 public static List<AgriData> GetList(List<AgriData> agriList, DataFilter<AgriData> filter)
        {
            List<AgriData> list = null;

            //通過List<T>自帶的FindAll進行篩選
            if (agriList != null)
                list = agriList.FindAll(new Predicate<AgriData>(filter.MatchRule));

            return list;
        }

同時,我們對SqlAgriDataManager中的GetItem擴充一下:

private static AgriData GetItem(SqlDataReader record)
        {
            AgriData agr = new AgriData();
            agr.Tem = Convert.ToInt32(record["AgriDataTem"]);
            agr.Sun = Convert.ToInt32(record["AgriDataSun"]);
            agr.Ele = Convert.ToInt32(record["AgriDataEle"]);
            agr.Water = Convert.ToInt32(record["AgriDataWater"]);
            agr.AgriDataBelong = Convert.ToInt32(record["AgriDataBelong"]);
            agr.Date = Convert.ToDateTime(record["AgriDataTime"]);
            return agr;
        }

最後要做的就是對index.aspx頁面上的ObjectDataSource控制元件的屬性重新配置一下:

<asp:ObjectDataSource ID="ObjectDataSource" runat="server" SelectMethod="GetList" TypeName="Manager.ObjAgriDataManager" OnSelecting="ObjectDataSource_Selecting">
                                <SelectParameters>
                                    <asp:Parameter Name="agriList" Type="Object" />
                                    <asp:Parameter Name="filter" Type="Object" />
                                </SelectParameters>
                            </asp:ObjectDataSource>

後臺得到DateFilter的處理為:

public DataFilter<AgriData> Filter {
            get {
                DataFilter<AgriData> filter = new AgriDataFilter(Year, Month, Day, 100);
                return filter;
            }
        }

ObjectDataSource的Selecting的事件處理為(其他DropDownList的相關處理事件無需更改):

protected void ObjectDataSource_Selecting(object sender, ObjectDataSourceSelectingEventArgs e)
        {
            e.InputParameters["agriList"] = ObjAgriDataManager.GetList();
            e.InputParameters["filter"] = Filter;
        }

一切就是這樣的順利,最終執行的出的效果為:
這裡寫圖片描述
所有工作都已經完成了,我們可以測試一下通過這方式對資料庫的依賴是否減少(理論上只需執行一次SQL操作)。我們可以開啟SQL 2008中的事件探測器(SQL Server Profiler)進行測試。
單擊工具欄的“橡皮擦”圖示,先對列表清除。然後執行第一次基於資料庫篩選的index.aspx檔案,可以看到對列表的每次操作,無論翻頁還是篩選,都會對資料庫進行一次查詢操作。然後單擊“橡皮擦”清除列表,執行第二次基於物件的篩選的程式,可以看到果然和預期一樣,只進行了一次訪問,後繼的翻頁還是篩選對資料庫都未構成依賴,全部都是對快取進行了相關操作。