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

来源:互联网 发布:python项目交易平台 编辑:程序博客网 时间:2024/06/08 04:56

可能大家对于数据库的操作太过于熟悉了,以至于忘记.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文件,可以看到对列表的每次操作,无论翻页还是筛选,都会对数据库进行一次查询操作。然后单击“橡皮擦”清除列表,运行第二次基于对象的筛选的程序,可以看到果然和预期一样,只进行了一次访问,后继的翻页还是筛选对数据库都未构成依赖,全部都是对缓存进行了相关操作。