基于对象的数据筛选与排序(一)

可能大家对于数据库的操作太过于熟悉了,以至于忘记.Net提供的强大而灵活的数据操作。例如,当我们想对数据进行筛选时,首先想到的是“Where”而不是List< T>.FindAll();当我们想对数据进行排序时,首先想到的是“Sort”而不是List< T>.Sort();当我们想对数据进行分页时,首先想到的是存储过程,而不是List< T>.GetRange()。。。
当然在这里并不是要指明数据库的直接操作不够好,在数据量比较大的时候,数据库的直接操作效率确实很高,然而在对较少数据进行操作时,一次性取出数据然后缓存在服务器上,这对于以后的排序、筛选、分页等操作直接对缓存进行,则会使效率提高很多。
方法不是绝对的,也并没有绝对的更优更劣,这里只是提供了不同的思路,具体的方法选用还是得根据实际情况来进行,所以在这里笔者详细介绍下.Net本身强大的对象数据操作。

首先我们在Web界面上放置这些控件,如下图:
基于对象的数据筛选与排序(一)_第1张图片

在这里的数据我使用了之前一个农田数据采集的数据库,具体的字段有: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并返回
        public static List GetList(string query)
        {
            List list = null;

            SqlDataReader reader = ExcuteReader(query);
            if (reader.HasRows)
            {
                list = new List();
                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事件,我们用一个按钮来辅助做回发操作。
基本的基于数据库的操作过程便是如此,执行之后得到到的效果如下图:
基于对象的数据筛选与排序(一)_第2张图片

基于对象的筛选

上面我们演示了传统的SQL的数据筛选操作,那么在此基础上是怎样进行基于对象的筛选的,又是怎样提升性能的(没有优化的操作就不会有被推广的意义)呢?
同样,我们沿用刚才所创建的控件进行操作,在本例中基于对象的筛选就是对List< AgriData>的筛选。实现的思路也并不难,首先创建一个重载的GetList(下篇代码直接新建了一个类来实现该方法,未在原SqlAgriDataManager类中实现重载)方法,然后取出所有的AgriData并添加到缓存中,然后创建一个新的List< AgriData>,将缓存中的所有数据遍历,将符合要求的项添加到该List< AgriData>中,最后再返回该集合,从而实现了相关的筛选操作。代码如下(为了利于区别,在这里新建了一个类):

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

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

            return list;
        }

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

            //将从缓存中提取的List根据年月日筛选出来
            if (agriList != null)
            {
                list = new List();

                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 obj)

因此math参数是一个返回bool类型并且只有一个传递参数的方法,在FindAll()内部再将这个方法传递进去。
现在我们要做的工作就是完成Predicate< T>封装的筛选规则,和定义Predicate< T>委托的方法。

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

显然这里的筛选条件为了更好的扩展性需要进行变更,我们可以定义一个泛型数据筛选类DataFilter< T>来进行筛选条件的设置,于是这个GetList方法变为:

public static List GetList(List agriList, DataFilter filter)

那么具体的这个DataFilter的设计思路是怎样的呢?
考虑到Predicate< T>只能传递一个参数,我们用数据对象作为参数即这里的AgriData业务对象进行参数传递,于是DataFilter< T>这个类和DataFilter< T>中的bool型筛选方法就应该是这样定义的:

public class DataFilter 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 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
    {
        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 GetList(List agriList, DataFilter filter)
        {
            List list = null;

            //通过List自带的FindAll进行筛选
            if (agriList != null)
                list = agriList.FindAll(new Predicate(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 Filter {
            get {
                DataFilter 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;
        }

一切就是这样的顺利,最终运行的出的效果为:
基于对象的数据筛选与排序(一)_第3张图片
所有工作都已经完成了,我们可以测试一下通过这方式对数据库的依赖是否减少(理论上只需执行一次SQL操作)。我们可以打开SQL 2008中的事件探测器(SQL Server Profiler)进行测试。
单击工具栏的“橡皮擦”图标,先对列表清除。然后运行第一次基于数据库筛选的index.aspx文件,可以看到对列表的每次操作,无论翻页还是筛选,都会对数据库进行一次查询操作。然后单击“橡皮擦”清除列表,运行第二次基于对象的筛选的程序,可以看到果然和预期一样,只进行了一次访问,后继的翻页还是筛选对数据库都未构成依赖,全部都是对缓存进行了相关操作。

你可能感兴趣的:(C#,linq-sql)