上一篇中,我们分析了页面中的控件和数据绑定中的扩展方法,今天我们看数据的查询和显示。其中,数据的查询属于业务逻辑层(Business),而显示属于UI层。
假设我们要根据以下两个条件进行查询:
- 日志分类(ID)
- 发布时间(范围)
两者为“AND”的关系,则查询可以通过通过扩展IQueryable<Blog>类实现,该功能定义在BlogExtension类中(位于DongBlog.Business\Blogs\Blog.cs文件中),代码如下:
1: /// <summary>
2: /// 根据日志分类取得日志
3: /// </summary>
4: /// <param name="query">日志查询</param>
5: /// <param name="blogClassID">日志分类ID</param>
6: /// <param name="createDataTimeStart">日志发表的起始时间</param>
7: /// <param name="createDataTimeEnd">日志发表的结束时间</param>
8: /// <returns>该分类下的日志</returns>
9: public static List<Blog> GetBlogsBy(this IQueryable<Blog> query,
int? blogClassID, DateTime? createDataTimeStart, DateTime? createDataTimeEnd)
10: {
11: if (query == null)
12: throw new ArgumentNullException("query");
13:
14: var q = query.AsQueryable();
15:
16: if (blogClassID.HasValue)
17: q = q.Where(b => b.BlogClassID == blogClassID.Value);
18: if (createDataTimeStart.HasValue)
19: q = q.Where(b => b.CreateDateTime > createDataTimeStart.Value);
20: if (createDataTimeEnd.HasValue)
21: q = q.Where(b => b.CreateDateTime < createDataTimeEnd.Value);
22:
23: return q
24: .OrderByDescending(b => b.CreateDateTime)
25: .ToList();
26: }
这段代码充分运用了Linq面向对象的方法进行数据库的查询,表间关联使用类的引用表示,最终会由Linq2SQL翻译为SQL语句。这种方法可以完成非常复杂的查询,在我们团队目前的项目中,最多的在一次查询中涉及五个表,几十个字段条件,条件还包括等于,大于、小于、属于,相似等多种情况。
这段代码虽然实现了查询功能,不过存在几个问题:第一,CreateDataTimeStart和CreateDataTimeEnd应该是一个表示范围的整体;第二,函数的参数随着查询条件的增加会变得越来越臃肿;第三,没有考虑分页。我们重构一下以上代码,解决这些问题。
在重构之前,还有一个要注意的是:重构的安全网——测试。在这个系统中,由于规模很小,没有实现完整的测试。但是所有的代码都是可以(并且容易)测试的,比如上面这个查询函数,可以通过实现用于测试的IQueryable<Blog>实现自动化测试。
我们先来重构第一个问题,抽取CreateDataTimeStart和CreateDataTimeEnd作为一个类,称之为TimeRange,放在Common层中,代码如下:
1: using ...
5:
6: namespace DongBlog.Common
7: {
8: /// <summary>
9: /// 表示一个时间段
10: /// </summary>
11: public class TimeRange
12: {
13:
14: /// <summary>
15: /// 取得或设置起始时间
16: /// </summary>
17: public DateTime? StartTime { get; set; }
18:
19: /// <summary>
20: /// 取得或设置终止时间
21: /// </summary>
22: public DateTime? EndTime { get; set; }
23:
24: /// <summary>
25: /// 构造一个时间段
26: /// </summary>
27: public TimeRange() { }
28: /// <summary>
29: /// 构造一个时间段
30: /// </summary>
31: /// <param name="startTime">起始时间</param>
32: /// <param name="endTime">终止时间</param>
33: public TimeRange(DateTime startTime, DateTime endTime)
34: {
35: this.StartTime = startTime;
36: this.EndTime = endTime;
37: }
38: }
39: }
然后GetBlogs函数可以重构为:
1: public static List<Blog> GetBlogsBy(
this IQueryable<Blog> query, int? blogClassID, TimeRange createDateTime)
2: {
3: if (query == null)
4: throw new ArgumentNullException("query");
5:
6: var q = query.AsQueryable();
7:
8: if (blogClassID.HasValue)
9: q = q.Where(b => b.BlogClassID == blogClassID.Value);
10:
11: if (createDateTime != null)
12: {
13: if (createDateTime.StartTime.HasValue)
14: q = q.Where(b => b.CreateDateTime > createDateTime.StartTime.Value);
15: if (createDateTime.EndTime.HasValue)
16: q = q.Where(b => b.CreateDateTime < createDateTime.EndTime.Value);
17: }
18:
19: return q
20: .OrderByDescending(b => b.CreateDateTime)
21: .ToList();
22: }
对月第二和第三个问题,我们利用前面提到的Common层的Query相关类一并解决。添加BlogQueryInformation类,代码如下:
1: /// <summary>
2: /// 日志查询信息
3: /// </summary>
4: public class BlogQueryInformation : QueryInformation
5: {
6: /// <summary>
7: /// 取得或设置日志分类ID
8: /// </summary>
9: public int? BlogClassID { get; set; }
10: /// <summary>
11: /// 取得或设置日志发表时间
12: /// </summary>
13: public TimeRange CreateDateTime { get; set; }
14: }
这样,就可以把GetBlogs方法重构如下:
1: public static QueryResult<Blog> GetBlogsBy(
this IQueryable<Blog> query, BlogQueryInformation queryInfo)
2: {
3: if (query == null)
4: throw new ArgumentNullException("query");
5: if (queryInfo == null)
6: throw new ArgumentNullException("queryInfo");
7:
8: var q = query.AsQueryable();
9:
10: if (queryInfo.BlogClassID.HasValue)
11: q = q.Where(b => b.BlogClassID == queryInfo.BlogClassID.Value);
12:
13: if (queryInfo.CreateDateTime != null)
14: {
15: if (queryInfo.CreateDateTime.StartTime.HasValue)
16: q = q.Where(b => b.CreateDateTime > queryInfo.CreateDateTime.StartTime.Value);
17: if (queryInfo.CreateDateTime.EndTime.HasValue)
18: q = q.Where(b => b.CreateDateTime < queryInfo.CreateDateTime.EndTime.Value);
19: }
20:
21: var resultList = q.OrderByDescending(b => b.CreateDateTime)
22: .Skip(queryInfo.Begin).Take(queryInfo.Limit)
23: .ToList();
24:
25: return new QueryResult<Blog>(resultList, q.Count());
26: }
最后两句代码实现了分页功能。以后添加新的查询条件时,只需要修改BlogQueryInformation和GetBlogs方法就可以了,函数的签名不需要修改。
然后我们看页面的调用,Default页面中,负责显示的代码如下:
1: protected void Page_Load(object sender, EventArgs e)
2: {
3: if (!IsPostBack)
4: display();
5: }
6:
7: private void display()
8: {
9: var pageCountHelper = new PageCountHelper(this, "篇");
10: var queryInfo = new BlogQueryInformation();
11:
12: queryInfo.BlogClassID = getBlogClassID();
13: queryInfo.CreateDateTime = getCreateDateTime();
14: queryInfo.Begin = pageCountHelper.CurrentPageCount
* pageCountHelper.RecordPerPage;
15: queryInfo.Limit = pageCountHelper.RecordPerPage;
16:
17: var result = Database.Blogs.GetBlogsBy(queryInfo);
18: ListView_BlogList.DataSource = result.ResultList;
19: LiteralPageCount.Text = pageCountHelper.GetPageCountHtml(result.Total);
20:
21: DataBind();
22: }
23: private TimeRange getCreateDateTime()
24: {
25: var createDateTime = new TimeRange();
26: return TimeRange.TryPrase(Request["CreateDateTime"], createDateTime) ?
createDateTime : null;
27: }
28: private int? getBlogClassID()
29: {
30: var blogClassIDString = Request["BlogClassID"];
31: return String.IsNullOrEmpty(blogClassIDString) ?
null : new Nullable<int>(Convert.ToInt32(blogClassIDString));
32: }
即:构造查询条件 -> 调用业务逻辑层(这里是GetBlogs方法)进行查询 -> 显示查询结果。其中用到了PageCountHelper这个辅助类,负责处理页面的分页,原理很简单,无非是拼Html,细节可以参考代码。
Master页面中,用于显示日志分类的ListView也使用这种方法实现数据绑定。至此,日志显示、根据日志分类的简单筛选和分页都已经实现了,最后我们得到了如下效果:
其中的测试数据,使用测试(InitialTestData)填充。