【问题标题】:LINQ to SQL and a running total on ordered resultsLINQ to SQL 和排序结果的运行总计
【发布时间】:2010-12-22 12:40:48
【问题描述】:

我想在DataGridView 中显示客户的会计历史记录,并且我希望有一列显示其余额的累计。我这样做的旧方法是获取数据,遍历数据,并将行逐一添加到DataGridView 并计算当时的运行总数。瘸。我宁愿使用 LINQ to SQL,或者如果不能使用 LINQ to SQL,则使用 LINQ 来计算运行总数,这样我就可以将DataGridView.DataSource 设置为我的数据。

这是我正在拍摄的一个超级简化的示例。假设我有以下课程。

class Item
{
    public DateTime Date { get; set; }
    public decimal Amount { get; set; }
    public decimal RunningTotal { get; set; }
}

我想要一个 L2S 或 LINQ 语句,它可以生成如下所示的结果:

   Date       Amount  RunningTotal
12-01-2009      5          5
12-02-2009     -5          0
12-02-2009     10         10
12-03-2009      5         15
12-04-2009    -15          0

请注意,可以有多个项目具有相同的日期 (12-02-2009)。在计算运行总计之前,应按日期对结果进行排序。我猜这意味着我需要两条语句,一条用于获取数据并对其进行排序,另一条用于执行运行总计。

我希望Aggregate 能做到这一点,但它并没有像我希望的那样工作。或者我只是想不通。

这个question 似乎在追求我想要的同一件事,但我看不出接受/唯一的答案如何解决我的问题。

关于如何实现这一点的任何想法?

编辑 结合 Alex 和 DOK 的答案,我得出了以下结论:

decimal runningTotal = 0;
var results = FetchDataFromDatabase()
    .OrderBy(item => item.Date)
    .Select(item => new Item
    {
        Amount = item.Amount,
        Date = item.Date,
        RunningTotal = runningTotal += item.Amount
    });

【问题讨论】:

  • 感谢新工具! : RunningTotal = runningTotal += item.Amount
  • 这个解决方案不会强制在客户端执行吗? (即它必须拉下整个结果集才能得到正确的答案?)——如果在 SQL 服务器上完成类似这样的事情,似乎性能会更高......
  • 使用查询外部的变量是非常危险的!因为results 变量是IEnumerable 类型,所以它的执行将被推迟 到以后。如果在此之前更改 runningTotal 的值,则生成的查询将不再正确。为了安全起见,您需要立即枚举它(列表或数组)。在这里使用简单的foreach 循环没有任何问题。
  • 正是我试图解决的问题。感谢您在六年前提出这个问题!

标签: c# linq linq-to-sql linq-to-objects


【解决方案1】:

使用闭包和匿名方法:

List<Item> myList = FetchDataFromDatabase();

decimal currentTotal = 0;
var query = myList
               .OrderBy(i => i.Date)
               .Select(i => 
                           {
                             currentTotal += i.Amount;
                             return new { 
                                            Date = i.Date, 
                                            Amount = i.Amount, 
                                            RunningTotal = currentTotal 
                                        };
                           }
                      );
foreach (var item in query)
{
    //do with item
}

【讨论】:

  • 我希望我能将你们俩都标记为答案!您的回答很容易理解,并且与我的示例相符。不过,我确实喜欢 DOK 在分配期间如何增加 currentTotal 内联。
  • 我已经用 Linq to Entities (EF) 进行了尝试,并得到“无法将带有语句体的 lambda 表达式转换为表达式树”编译错误。与 L2O 相比,这是 EF 特有的吗?
  • 请注意,如果您重复使用查询结果,这最终会出错。 This question is a result of this. 您可以通过在查询末尾添加.ToList() 来解决此问题。
  • 这个解决方案将强制在客户端执行(即它必须拉下整个结果集才能获得正确的答案)——如果这样的话,似乎这样的事情会更有效率在 SQL 服务器上完成...
  • @NeilBarnwell 它告诉您,唯一可行的方法是首先将整个数据集拉到客户端(注意他将数据集存储在列表中,而不是 IQueryable 或 IEnumerable 中)。必须有更好的方法。
【解决方案2】:

这个怎么样:(感谢this source

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ConsoleApplication1
{
    class Program
    {
        delegate string CreateGroupingDelegate(int i);

        static void Main(string[] args)
        {
            List<int> list = new List<int>() { 1, 2, 3, 4, 5, 7, 8, 9, 10, 11, 12, 13, 69, 2007};
            int running_total = 0;

            var result_set =
                from x in list
                select new
                {
                    num = x,
                    running_total = (running_total = running_total + x)
                };

            foreach (var v in result_set)
            {
                Console.WriteLine( "list element: {0}, total so far: {1}",
                    v.num,
                    v.running_total);
            }

            Console.ReadLine();
        }
    }
}

【讨论】:

  • 谢谢!我喜欢你如何分配running_total inline。这很漂亮。
  • running_total = (running_total = running_total + x) => 大吃一惊。下次我一定会记住这一点的:)
  • 酷!它也适用于强类型对象,而不仅仅是匿名类型
  • 请注意,如果您重复使用查询结果,这最终会出错。 This question is a result of this. 您可以通过在查询末尾添加.ToList() 来解决此问题。
  • 这是另一个仅限客户端的解决方案(需要枚举客户端上的整个数据集——这对于大型数据集来说很糟糕)。 :(
【解决方案3】:

如果这个问题还没有得到解答,我有一个解决方案,我一直在我的项目中使用。这与 Oracle 分区组非常相似。关键是让running total中的where子句匹配orig列表,然后按日期分组。

var itemList = GetItemsFromDBYadaYadaYada();

var withRuningTotals = from i in itemList    
                       select i.Date, i.Amount,    
                              Runningtotal = itemList.Where( x=> x.Date == i.Date).
                                                      GroupBy(x=> x.Date).
                                                      Select(DateGroup=> DateGroup.Sum(x=> x.Amount)).Single();

【讨论】:

  • 有趣。似乎这样可以将数据正确地卸载到数据库中。
【解决方案4】:

Aggregate 也可用于获得运行总计:

var src = new [] { 1, 4, 3, 2 };
var running = src.Aggregate(new List<int>(), (a, i) => {
    a.Add(a.Count == 0 ? i : a.Last() + i);
    return a;
});

【讨论】:

  • 但这如何将运行总计应用于原始集合? OP 要求一种将其附加到每个项目的方法。
【解决方案5】:
using System;
using System.Linq;
using System.Collections.Generic;

public class Program
{
    public static void Main()
    {
        var list = new List<int>{1, 5, 4, 6, 8, 11, 3, 12};

        int running_total = 0;

        list.ForEach(x=> Console.WriteLine(running_total = x+running_total));
    }
}

【讨论】:

    【解决方案6】:

    对此的大多数其他答案(正确设置对象内的运行总计)都依赖于副作用变量,这不符合函数式编码和.Aggregate() 之类的精神。该解决方案消除了副作用变量。

    (注意 - 此解决方案与其他答案一样在客户端上运行,因此可能不是您需要的最佳解决方案。)

    var results = FetchDataFromDatabase()
        .OrderBy(item => item.Date)
        .Aggregate(new List<Item>(), (list, i) =>
            {
                var item = new Item
                {
                    Amount = i.Amount,
                    Date = i.Date,
                    RunningTotal = i.Amount + (list.LastOrDefault()?.RunningTotal ?? 0)
                };
    
                return list.Append(item).ToList();
                // Or, possibly more efficient:
                // list.Add(item);
                // return list;
            });
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2020-06-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多