可能大家对于数据库的操作太过于熟悉了,以至于忘记.Net提供的强大而灵活的数据操作。例如,当我们想对数据进行筛选时,首先想到的是“Where”而不是List< T>.FindAll();当我们想对数据进行排序时,首先想到的是“Sort”而不是List< T>.Sort();当我们想对数据进行分页时,首先想到的是存储过程,而不是List< T>.GetRange()。。。
当然在这里并不是要指明数据库的直接操作不够好,在数据量比较大的时候,数据库的直接操作效率确实很高,然而在对较少数据进行操作时,一次性取出数据然后缓存在服务器上,这对于以后的排序、筛选、分页等操作直接对缓存进行,则会使效率提高很多。
方法不是绝对的,也并没有绝对的更优更劣,这里只是提供了不同的思路,具体的方法选用还是得根据实际情况来进行,所以在这里笔者详细介绍下.Net本身强大的对象数据操作。
在这里的数据我使用了之前一个农田数据采集的数据库,具体的字段有:id编号,光照度,温度,导电度,湿度,所属农田编号和记录时间。
作为样本,我们现在想实现的效果是,根据时间展示4个记录数据就好。
首先我们最为熟悉和第一反应肯定是用数据库进行筛选操作,我们建立数据表对应的业务对象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事件,我们用一个按钮来辅助做回发操作。
基本的基于数据库的操作过程便是如此,执行之后得到到的效果如下图:
上面我们演示了传统的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;
}
一切就是这样的顺利,最终运行的出的效果为:
所有工作都已经完成了,我们可以测试一下通过这方式对数据库的依赖是否减少(理论上只需执行一次SQL操作)。我们可以打开SQL 2008中的事件探测器(SQL Server Profiler)进行测试。
单击工具栏的“橡皮擦”图标,先对列表清除。然后运行第一次基于数据库筛选的index.aspx文件,可以看到对列表的每次操作,无论翻页还是筛选,都会对数据库进行一次查询操作。然后单击“橡皮擦”清除列表,运行第二次基于对象的筛选的程序,可以看到果然和预期一样,只进行了一次访问,后继的翻页还是筛选对数据库都未构成依赖,全部都是对缓存进行了相关操作。