如何使用 Linq2Sql 按月份、Type 和 Description 分组
结论
在 group by 的复合键中加入 Date.Year、Date.Month、Type 和 Description,然后对每组的 Amount 调用 Sum。
年份不能省略,否则 2011 年 11 月和 2012 年 11 月的数据会被合并。月份名称只用于展示,最好先在数据库中完成汇总,再进行转换,不要直接写进 Linq to SQL 查询。
定义返回模型
公开方法不能直接返回匿名类型,因此需要先定义一个用于网格绑定的模型:
public sealed class BillingHistoryItem
{
public int UserID { get; set; }
public int Year { get; set; }
public int Month { get; set; }
public string MonthName { get; set; }
public string Type { get; set; }
public string Description { get; set; }
public decimal Amount { get; set; }
}
如果数据库映射中的 Type 不是 string,请将该属性改为实际的映射类型,例如 char。
Linq to SQL 查询
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
public static IEnumerable<BillingHistoryItem> GetBillingHistory(
int userId,
DateTime startDate,
DateTime endDate)
{
using (SBMData2.SBMDataContext db = new SBMData2.SBMDataContext())
{
var groupedQuery =
from b in db.Billings
where b.UserID == userId
&& b.Date >= startDate
&& b.Date < endDate
group b by new
{
b.UserID,
Year = b.Date.Year,
Month = b.Date.Month,
b.Type,
b.Description
}
into billingGroup
orderby billingGroup.Key.Year descending,
billingGroup.Key.Month descending,
billingGroup.Key.Type,
billingGroup.Key.Description
select new
{
billingGroup.Key.UserID,
billingGroup.Key.Year,
billingGroup.Key.Month,
billingGroup.Key.Type,
billingGroup.Key.Description,
Amount = billingGroup.Sum(x => x.Amount)
};
return groupedQuery
.AsEnumerable()
.Select(x => new BillingHistoryItem
{
UserID = x.UserID,
Year = x.Year,
Month = x.Month,
MonthName = CultureInfo
.GetCultureInfo("en-US")
.DateTimeFormat
.GetMonthName(x.Month),
Type = x.Type,
Description = x.Description,
Amount = x.Amount
})
.ToList();
}
}
这里的分组键相当于:
UserID + Year + Month + Type + Description
同一用户在同一年、同一个月内,只要 Type 和 Description 相同,相关记录就会汇总成一行。
AsEnumerable() 之前的筛选、分组、排序和求和由数据库执行。转到内存后,再把月份数字转换为 November 这样的名称,因为 Linq to SQL 通常无法将 DateTimeFormat.GetMonthName() 翻译成 SQL。
如果界面需要显示中文月份,可以改为:
CultureInfo
.GetCultureInfo("zh-CN")
.DateTimeFormat
.GetMonthName(x.Month)
也可以直接显示年月,让不同年份的同名月份更容易区分:
string.Format("{0:D4}-{1:D2}", x.Year, x.Month)
页面绑定
页面的绑定方式不需要调整:
protected void Page_Load(object sender, EventArgs e)
{
int userID = foo.Data.User.GetIDFromUserName(
HttpContext.Current.User.Identity.Name);
DateTime endDate = DateTime.Now;
DateTime startDate = endDate.AddMonths(-3);
RadGrid2.DataSource = BusinessLogic.GetBillingHistory(
userID,
startDate,
endDate);
RadGrid2.DataBind();
}
日期范围需要注意
下面的条件使用左闭右开区间:
b.Date >= startDate && b.Date < endDate
与 <= endDate 相比,这种写法更适合 endDate 表示下一周期起点的情况,可以避免重复计算边界上的记录。
DateTime.Now.AddMonths(-3) 得到的是向前推三个月的滚动区间,不一定包含三个完整的日历月。如果需要查询本月和之前两个完整月份,可以明确计算每月月初:
DateTime currentMonthStart =
new DateTime(DateTime.Today.Year, DateTime.Today.Month, 1);
DateTime startDate = currentMonthStart.AddMonths(-2);
DateTime endDate = currentMonthStart.AddMonths(1);
金额显示
查询只负责计算金额。美元符号和负数括号等格式应由网格展示层处理,例如:
amount.ToString("C")
付款是否显示为负数,要看数据库中的 Amount 是否已经按负值保存。如果付款记录存储的是正数,需要按照业务规则在投影中转换符号。现有信息无法确认 Type == "C" 是否一定代表付款,因此不应在规则尚未明确时直接修改金额。
备注:内容仅供参考。
