LiteOrm 支持通过表达式扩展机制实现窗口函数(Window Functions),用于数据分析场景如累计求和、分组排名等。
窗口函数在数据的一组行上执行聚合或分析计算,同时返回每一行的明细数据。
| 函数 | 说明 |
|---|---|
SUM() OVER() |
累计求和 |
AVG() OVER() |
移动平均 |
ROW_NUMBER() OVER() |
行号 |
RANK() OVER() |
排名(跳跃) |
DENSE_RANK() OVER() |
排名(连续) |
LAG() OVER() |
前一行数据 |
LEAD() OVER() |
后一行数据 |
LiteOrm 提供两种窗口函数实现方式:
| 方式 | 说明 |
|---|---|
| Lambda 扩展方法 | 定义 C# 扩展方法,声明式调用 |
| 纯 Expr | 直接使用 FunctionExpr 的 Over 扩展 |
/// <summary>
/// 窗口函数排序项,用于指定排序字段和方向。
/// </summary>
/// <typeparam name="T">实体类型</typeparam>
public class SumOverOrderBy<T>
{
public SumOverOrderBy(Expression<Func<T, object>> field, bool ascending = true)
{
Field = field;
Ascending = ascending;
}
public Expression<Func<T, object>> Field { get; }
public bool Ascending { get; }
}
public static class WindowFunctionExtensions
{
// 仅分区字段(params 重载)
public static int SumOver<T>(this int amount,
params Expression<Func<T, object>>[] partitionBy) => amount;
// 分区 + 排序(显式数组重载)
public static int SumOver<T>(this int amount,
Expression<Func<T, object>>[] partitionBy,
SumOverOrderBy<T>[] orderBy) => amount;
// decimal 版本
public static decimal SumOver<T>(this decimal amount,
params Expression<Func<T, object>>[] partitionBy) => amount;
public static decimal SumOver<T>(this decimal amount,
Expression<Func<T, object>>[] partitionBy,
SumOverOrderBy<T>[] orderBy) => amount;
}
LambdaExprConverter.RegisterMethodHandler("SumOver", (node, converter) =>
{
var amountExpr = converter.Convert(node.Arguments[0]) as ValueTypeExpr;
var partitionExprs = new List<ValueTypeExpr>();
var orderExprs = new List<OrderByItemExpr>();
// 分区字段:NewArrayExpression,元素为 Quote(Lambda)
if (node.Arguments.Count > 1 && node.Arguments[1] is NewArrayExpression partArray)
{
foreach (var elem in partArray.Expressions)
{
if (converter.Convert(elem) is ValueTypeExpr vte)
partitionExprs.Add(vte);
}
}
// 排序字段:NewArrayExpression,元素为 SumOverOrderBy<T> 构造表达式
if (node.Arguments.Count > 2 && node.Arguments[2] is NewArrayExpression orderArray)
{
foreach (var elem in orderArray.Expressions)
{
if (elem is NewExpression ctorNew && ctorNew.Arguments.Count == 2)
{
var field = converter.Convert(ctorNew.Arguments[0]) as ValueTypeExpr;
bool isAsc = ctorNew.Arguments[1] is ConstantExpression { Value: bool b } && b;
if (field is not null)
orderExprs.Add(new OrderByItemExpr(field, isAsc));
}
}
}
return Func("SUM", amountExpr)
.Over(partitionExprs.ToArray(), orderExprs.ToArray());
});
Over 的 SQL 处理// 注意:SqlBuilder 已默认注册对 "Over" 的处理器(先输出内部函数,再追加 OVER (...) 语义),
// 因此通常无需自行调用 RegisterFunctionSqlHandler 来注册 "Over"。
// 直接使用 `Func("SUM", ...).Over(...)` 即可得到正确的 SQL 输出。
var sales = await saleService.SearchAs<SalesWindowView>(q => q
.OrderBy(s => s.ProductId)
.Select(s => new SalesWindowView
{
Id = s.Id,
ProductId = s.ProductId,
ProductName = s.ProductName,
Amount = s.Amount,
SaleTime = s.SaleTime,
ProductTotal = s.Amount.SumOver<SalesRecord>(p => p.ProductId),
RunningTotal = s.Amount.SumOver<SalesRecord>(
partitionBy: new Expression<Func<SalesRecord, object>>[] { p => p.ProductId },
orderBy: new SumOverOrderBy<SalesRecord>[]
{
new SumOverOrderBy<SalesRecord>(p => p.SaleTime, true)
})
})
);
下面这段整理自 LiteOrm.Demo\Demos\WindowFunctionDemo.cs:
// 应用启动时先注册处理器
WindowFunctionDemo.RegisterHandlers();
// 查询时直接在投影里使用窗口函数扩展
var results = await factory.SalesDAO
.WithArgs([tableMonth])
.SearchAs(q => q
.OrderBy(s => s.ProductId)
.Select(s => new SalesWindowView
{
Id = s.Id,
ProductId = s.ProductId,
ProductName = s.ProductName,
Amount = s.Amount,
SaleTime = s.SaleTime,
ProductTotal = s.Amount.SumOver<SalesRecord>(p => p.ProductId)
})
).ToListAsync();
如果你准备把窗口函数能力提供给业务层长期复用,推荐采用这种“启动期注册 + 查询期直接调用”的模式。
生成的 SQL:
SELECT
s.Id,
s.ProductId,
s.ProductName,
s.Amount,
s.SaleTime,
SUM(s.Amount) OVER (PARTITION BY s.ProductId) AS ProductTotal
FROM Sales_yyyyMM s
纯 Expr 方式无需定义扩展方法和注册 RegisterMethodHandler,直接构造表达式。
// 累计总,使用内置 Over 函数
var productTotalExpr = Func("SUM", Prop(nameof(SalesRecord.Amount)))
.Over(new[] { Prop(nameof(SalesRecord.ProductId)) });
// 按 SaleTime 升序的累计值
var runningTotalExpr = Func("SUM", Prop(nameof(SalesRecord.Amount)))
.Over(new[] { Prop(nameof(SalesRecord.ProductId)) }, new[] { Prop(nameof(SalesRecord.SaleTime)).Asc() });
方式一:嵌入 Lambda
using static LiteOrm.Common.Expr;
var results = await saleDAO
.WithArgs([tableMonth])
.SearchAs<SalesWindowView>(q => q
.OrderBy(s => s.ProductId)
.Select(s => new SalesWindowView
{
Id = s.Id,
ProductId = s.ProductId,
ProductName = s.ProductName,
Amount = s.Amount,
SaleTime = s.SaleTime,
ProductTotal = productTotalExpr.To<int>(),
RunningTotal = runningTotalExpr.To<int>()
})
).ToListAsync();
方式二:SelectExpr 链式构建
var selectExpr = new FromExpr(typeof(SalesRecord))
.OrderBy(new OrderByItemExpr(Prop(nameof(SalesRecord.ProductId)), ascending: true))
.Select(
new SelectItemExpr(Prop(nameof(SalesRecord.Id)), nameof(SalesWindowView.Id)),
new SelectItemExpr(Prop(nameof(SalesRecord.ProductId)), nameof(SalesWindowView.ProductId)),
new SelectItemExpr(Prop(nameof(SalesRecord.ProductName)), nameof(SalesWindowView.ProductName)),
new SelectItemExpr(Prop(nameof(SalesRecord.Amount)), nameof(SalesWindowView.Amount)),
new SelectItemExpr(Prop(nameof(SalesRecord.SaleTime)), nameof(SalesWindowView.SaleTime)),
new SelectItemExpr(productTotalExpr, nameof(SalesWindowView.ProductTotal)),
new SelectItemExpr(runningTotalExpr, nameof(SalesWindowView.RunningTotal)));
var results = await saleDAO
.WithArgs([tableMonth])
.SearchAs<SalesWindowView>(selectExpr)
.ToListAsync();
| 对比项 | Lambda 扩展方法 | 纯 Expr |
|---|---|---|
| 需要扩展方法定义 | ✅ 是 | ❌ 否 |
| 需要 RegisterMethodHandler | ✅ 是 | ❌ 否 |
| 需要 RegisterFunctionSqlHandler | ❌ 否(除非自定义 Over 输出) | ❌ 否 |
| 代码提示 | ✅ 高 | ⚠️ 中 |
| 适用场景 | 通用、高复用 | 快速原型 |
当前仓库中的示例主要覆盖 SumOver,底层仍然依赖 FunctionExpr.Over(...)。如果后续需要扩展 ROW_NUMBER、RANK、LAG 等函数,可以沿用同样的注册模式。
LAG/LEAD/RANK 等函数的结果