LINQ to SQL версия GROUP BY WITH ROLLUP - PullRequest
       40

LINQ to SQL версия GROUP BY WITH ROLLUP

9 голосов
/ 28 августа 2009

Я пытаюсь переписать старый SQL в LINQ to SQL. У меня есть sproc с GROUP BY WITH ROLLUP, но я не уверен, что будет эквивалент LINQ. У LINQ есть GroupBy, но не похоже, что он поддерживает ROLLUP.

Упрощенный пример результатов, которые я пытаюсь получить, будет примерно таким:

+-----------+---------------+--------------------+
|   City    |  ServicePlan  |  NumberOfCustomers |
+-----------+---------------+--------------------+
| Seattle   |  Plan A       |                 10 |
| Seattle   |  Plan B       |                  5 |
| Seattle   |  All          |                 15 |
| Portland  |  Plan A       |                 20 |
| Portland  |  Plan C       |                 10 |
| Portland  |  All          |                 30 |
| All       |  All          |                 45 |
+-----------+---------------+--------------------+

Есть идеи, как получить эти результаты, используя LINQ to SQL?

Ответы [ 4 ]

11 голосов
/ 09 сентября 2009

Я нашел гораздо более простое решение. Я пытался сделать это намного сложнее, чем нужно. Вместо 3-5 классов / методов мне нужен только один метод.

Обычно вы выполняете сортировку и группировку самостоятельно, а затем звоните WithRollup(), чтобы получить List<> предметов с промежуточными итогами и общим итогом. Я не мог понять, как сгенерировать промежуточные итоги и итоговую сумму на стороне SQL, так что это делается с помощью LINQ to Objects. Вот код:

/// <summary>
/// Adds sub-totals to a list of items, along with a grand total for the whole list.
/// </summary>
/// <param name="elements">Group and/or sort this yourself before calling WithRollup.</param>
/// <param name="primaryKeyOfElement">Given a TElement, return the property that you want sub-totals for.</param>
/// <param name="calculateSubTotalElement">Given a group of elements, return a TElement that represents the sub-total.</param>
/// <param name="grandTotalElement">A TElement that represents the grand total.</param>
public static List<TElement> WithRollup<TElement, TKey>(this IEnumerable<TElement> elements,
    Func<TElement, TKey> primaryKeyOfElement,
    Func<IGrouping<TKey, TElement>, TElement> calculateSubTotalElement,
    TElement grandTotalElement)
{
    // Create a new list the items, subtotals, and the grand total.
    List<TElement> results = new List<TElement>();
    var lookup = elements.ToLookup(primaryKeyOfElement);
    foreach (var group in lookup)
    {
        // Add items in the current group
        results.AddRange(group);
        // Add subTotal for current group
        results.Add(calculateSubTotalElement(group));
    }
    // Add grand total
    results.Add(grandTotalElement);

    return results;
}

И пример того, как его использовать:

class Program
{
    static void Main(string[] args)
    {
        IQueryable<CustomObject> dataItems = (new[]
        {
            new CustomObject { City = "Seattle", Plan = "Plan B", Charges = 20 },
            new CustomObject { City = "Seattle", Plan = "Plan A", Charges = 10 },
            new CustomObject { City = "Seattle", Plan = "Plan B", Charges = 20 },
            new CustomObject { City = "Seattle", Plan = "Plan A", Charges = 10 },
            new CustomObject { City = "Seattle", Plan = "Plan A", Charges = 10 },
            new CustomObject { City = "Seattle", Plan = "Plan A", Charges = 10 },
            new CustomObject { City = "Portland", Plan = "Plan A", Charges = 10 },
            new CustomObject { City = "Portland", Plan = "Plan A", Charges = 10 },
            new CustomObject { City = "Portland", Plan = "Plan C", Charges = 30 },
            new CustomObject { City = "Portland", Plan = "Plan C", Charges = 30 },
            new CustomObject { City = "Portland", Plan = "Plan C", Charges = 30 }
        }).AsQueryable();

        IQueryable<CustomObject> orderedElements = from item in dataItems
                                                   orderby item.City, item.Plan
                                                   group item by new { item.City, item.Plan } into grouping
                                                   select new CustomObject
                                                   {
                                                       City = grouping.Key.City,
                                                       Plan = grouping.Key.Plan,
                                                       Charges = grouping.Sum(item => item.Charges),
                                                       Count = grouping.Count()
                                                   };

        List<CustomObject> results = orderedElements.WithRollup(
            item => item.City,
            group => new CustomObject
            {
                City = group.Key,
                Plan = "All",
                Charges = group.Sum(item => item.Charges),
                Count = group.Sum(item => item.Count)
            },
            new CustomObject
            {
                City = "All",
                Plan = "All",
                Charges = orderedElements.Sum(item => item.Charges),
                Count = orderedElements.Sum(item => item.Count)
            });

        foreach (var result in results)
            Console.WriteLine(result);

        Console.Read();
    }
}

class CustomObject
{
    public string City { get; set; }
    public string Plan { get; set; }
    public int Count { get; set; }
    public decimal Charges { get; set; }

    public override string ToString()
    {
        return String.Format("{0} - {1} ({2} - {3})", City, Plan, Count, Charges);
    }
}
4 голосов
/ 31 августа 2009

Я понял! Универсальный GroupByWithRollup. Он группируется только по двум столбцам, но может быть легко расширен для поддержки большего. У меня, вероятно, будет другая версия, которая принимает три столбца. Ключевыми классами / методами являются Grouping <>, GroupByMany <> () и GroupByWithRollup <> (). Методы SubTotal () и GrandTotal () являются помощниками, когда вы фактически используете GroupByWithRollup <> (). Ниже приведен код, а затем пример того, как его использовать.

/// <summary>
/// Represents an instance of an IGrouping<>.  Used by GroupByMany(), GroupByWithRollup(), and GrandTotal().
/// </summary>
public class Grouping<TKey, TElement> : IGrouping<TKey, TElement>
{
    public TKey Key { get; set; }
    public IEnumerable<TElement> Items { get; set; }

    public IEnumerator<TElement> GetEnumerator()
    {
        return Items.GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return Items.GetEnumerator();
    }
}

public static class Extensions
{
    /// <summary>
    /// Groups by two columns.
    /// </summary>
    /// <typeparam name="TElement">Type of elements to group.</typeparam>
    /// <typeparam name="TKey1">Type of the first expression to group by.</typeparam>
    /// <typeparam name="TKey2">Type of the second expression to group by.</typeparam>
    /// <param name="orderedElements">Elements to group.</param>
    /// <param name="groupByKey1Expression">The first expression to group by.</param>
    /// <param name="groupByKey2Expression">The second expression to group by.</param>
    /// <param name="newElementExpression">An expression that returns a new TElement.</param>
    public static IQueryable<Grouping<TKey1, TElement>> GroupByMany<TElement, TKey1, TKey2>(this IOrderedQueryable<TElement> orderedElements,
        Func<TElement, TKey1> groupByKey1Expression,
        Func<TElement, TKey2> groupByKey2Expression,
        Func<IGrouping<TKey1, TElement>, IGrouping<TKey2, TElement>, TElement> newElementExpression
        )
    {
        // Group the items by Key1 and Key2
        return from element in orderedElements
               group element by groupByKey1Expression(element) into groupByKey1
               select new Grouping<TKey1, TElement>
               {
                   Key = groupByKey1.Key,
                   Items = from key1Item in groupByKey1
                           group key1Item by groupByKey2Expression(key1Item) into groupByKey2
                           select newElementExpression(groupByKey1, groupByKey2)
               };
    }

    /// <summary>
    /// Returns a List of TElement containing all elements of orderedElements as well as subTotals and a grand total.
    /// </summary>
    /// <typeparam name="TElement">Type of elements to group.</typeparam>
    /// <typeparam name="TKey1">Type of the first expression to group by.</typeparam>
    /// <typeparam name="TKey2">Type of the second expression to group by.</typeparam>
    /// <param name="orderedElements">Elements to group.</param>
    /// <param name="groupByKey1Expression">The first expression to group by.</param>
    /// <param name="groupByKey2Expression">The second expression to group by.</param>
    /// <param name="newElementExpression">An expression that returns a new TElement.</param>
    /// <param name="subTotalExpression">An expression that returns a new TElement that represents a subTotal.</param>
    /// <param name="totalExpression">An expression that returns a new TElement that represents a grand total.</param>
    public static List<TElement> GroupByWithRollup<TElement, TKey1, TKey2>(this IOrderedQueryable<TElement> orderedElements,
        Func<TElement, TKey1> groupByKey1Expression,
        Func<TElement, TKey2> groupByKey2Expression,
        Func<IGrouping<TKey1, TElement>, IGrouping<TKey2, TElement>, TElement> newElementExpression,
        Func<IGrouping<TKey1, TElement>, TElement> subTotalExpression,
        Func<IQueryable<Grouping<TKey1, TElement>>, TElement> totalExpression
        )
    {
        // Group the items by Key1 and Key2
        IQueryable<Grouping<TKey1, TElement>> groupedItems = orderedElements.GroupByMany(groupByKey1Expression, groupByKey2Expression, newElementExpression);

        // Create a new list the items, subtotals, and the grand total.
        List<TElement> results = new List<TElement>();
        foreach (Grouping<TKey1, TElement> item in groupedItems)
        {
            // Add items under current group
            results.AddRange(item);
            // Add subTotal for current group
            results.Add(subTotalExpression(item));
        }
        // Add grand total
        results.Add(totalExpression(groupedItems));

        return results;
    }

    /// <summary>
    /// Returns the subTotal sum of sumExpression.
    /// </summary>
    /// <param name="sumExpression">An expression that returns the value to sum.</param>
    public static int SubTotal<TKey, TElement>(this IGrouping<TKey, TElement> query, Func<TElement, int> sumExpression)
    {
        return query.Sum(group => sumExpression(group));
    }

    /// <summary>
    /// Returns the subTotal sum of sumExpression.
    /// </summary>
    /// <param name="sumExpression">An expression that returns the value to sum.</param>
    public static decimal SubTotal<TKey, TElement>(this IGrouping<TKey, TElement> query, Func<TElement, decimal> sumExpression)
    {
        return query.Sum(group => sumExpression(group));
    }

    /// <summary>
    /// Returns the grand total sum of sumExpression.
    /// </summary>
    /// <param name="sumExpression">An expression that returns the value to sum.</param>
    public static int GrandTotal<TKey, TElement>(this IQueryable<Grouping<TKey, TElement>> query, Func<TElement, int> sumExpression)
    {
        return query.Sum(group => group.Sum(innerGroup => sumExpression(innerGroup)));
    }

    /// <summary>
    /// Returns the grand total sum of sumExpression.
    /// </summary>
    /// <param name="sumExpression">An expression that returns the value to sum.</param>
    public static decimal GrandTotal<TKey, TElement>(this IQueryable<Grouping<TKey, TElement>> query, Func<TElement, decimal> sumExpression)
    {
        return query.Sum(group => group.Sum(innerGroup => sumExpression(innerGroup)));
    }

И пример использования:

class Program
{
    static void Main(string[] args)
    {
        IQueryable<CustomObject> dataItems = (new[]
        {
            new CustomObject { City = "Seattle", Plan = "Plan B", Charges = 20 },
            new CustomObject { City = "Seattle", Plan = "Plan A", Charges = 10 },
            new CustomObject { City = "Seattle", Plan = "Plan B", Charges = 20 },
            new CustomObject { City = "Seattle", Plan = "Plan A", Charges = 10 },
            new CustomObject { City = "Seattle", Plan = "Plan A", Charges = 10 },
            new CustomObject { City = "Seattle", Plan = "Plan A", Charges = 10 },
            new CustomObject { City = "Portland", Plan = "Plan A", Charges = 10 },
            new CustomObject { City = "Portland", Plan = "Plan A", Charges = 10 },
            new CustomObject { City = "Portland", Plan = "Plan C", Charges = 30 },
            new CustomObject { City = "Portland", Plan = "Plan C", Charges = 30 },
            new CustomObject { City = "Portland", Plan = "Plan C", Charges = 30 }
        }).AsQueryable();

        List<CustomObject> results = dataItems.OrderBy(item => item.City).ThenBy(item => item.Plan).GroupByWithRollup(
            item => item.City,
            item => item.Plan,
            (primaryGrouping, secondaryGrouping) => new CustomObject
            {
                City = primaryGrouping.Key,
                Plan = secondaryGrouping.Key,
                Count = secondaryGrouping.Count(),
                Charges = secondaryGrouping.Sum(item => item.Charges)
            },
            item => new CustomObject
            {
                City = item.Key,
                Plan = "All",
                Count = item.SubTotal(subItem => subItem.Count),
                Charges = item.SubTotal(subItem => subItem.Charges)
            },
            items => new CustomObject
            {
                City = "All",
                Plan = "All",
                Count = items.GrandTotal(subItem => subItem.Count),
                Charges = items.GrandTotal(subItem => subItem.Charges)
            }
            );
        foreach (var result in results)
            Console.WriteLine(result);

        Console.Read();
    }
}

class CustomObject
{
    public string City { get; set; }
    public string Plan { get; set; }
    public int Count { get; set; }
    public decimal Charges { get; set; }

    public override string ToString()
    {
        return String.Format("{0} - {1} ({2} - {3})", City, Plan, Count, Charges);
    }
}
2 голосов
/ 27 января 2015

@ Ecyrb, привет через пять лет!

Я только смутно знаком с LINQ to SQL, выходящим за рамки стандартного LINQ (для объектов). Однако, поскольку у вас есть тег «LINQ», отдельный от вашего тега «LINQ-2-SQL», потому что вы, кажется, в первую очередь заинтересованы в результатах (в отличие от регистрации изменений в базе данных), и потому что это единственный действительно релевантный ресурс, который появился, когда я поискал в поиске LINQ-эквивалента функции группировки «свертка» SQL Server, я предложу свое собственное альтернативное решение для тех, кто сегодня испытывает аналогичную потребность.

По сути, мой подход заключается в создании цепочечного синтаксиса ".GroupBy (). ThenBy ()", аналогичного синтаксису ".OrderBy (). ThenBy ()". Мое расширение ожидает коллекцию объектов IGrouping - результат, который вы получите при запуске ".GroupBy ()" - в качестве его источника. Затем он берет коллекцию и разгруппирует их, чтобы вернуться к исходному объекту перед группировкой. Наконец, он перегруппирует данные в соответствии с новой функцией группировки, создавая другой набор объектов IGrouping, и добавляет вновь сгруппированные объекты в набор исходных объектов.

public static class mySampleExtensions {

    public static IEnumerable<IGrouping<TKey, TSource>> ThenBy<TSource, TKey> (     
        this IEnumerable<IGrouping<TKey, TSource>> source,
        Func<TSource, TKey> keySelector) {

        var unGroup = source.SelectMany(sm=> sm).Distinct(); // thank you flq at /256444/preobrazovat-spisok-list-t-v-spisok-t-v-c
        var reGroup = unGroup.GroupBy(keySelector);

        return source.Concat(reGroup);}

}

Вы можете использовать метод для сопоставления логики свертывания SQL-сервера, помещая постоянные значения в соответствующую область функции ".ThenBy ()". Я предпочитаю использовать нулевое значение, потому что это наиболее гибкая константа для приведения. Приведение важно, потому что функция, которую вы используете в .GroupBy () и .ThenBy (), должна приводить к одному и тому же типу объекта. Используя переменную «dataItems», которую вы создали в своем первом ответе 31 августа 2009 года, она выглядела бы так:

var rollItUp = dataItems
    .GroupBy(g=> new {g.City, g.Plan})
        .ThenBy(g=> new {g.City, Plan = (string) null})
        .ThenBy(g=> new {City = (string) null, Plan = (string) null})
    .Select(s=> new CustomObject {
        City = s.Key.City, 
        Plan = s.Key.Plan, 
        Count = s.Count(),
        Charges = s.Sum(a=> a.Charges)})
    .OrderBy(o=> o.City) // This line optional
        .ThenBy(o=> o.Plan); // This line optional

Вы можете заменить нули в логике ".ThenBy ()" на "все", если хотите.

С помощью функции ".ThenBy ()" вы можете эмулировать наборы группировок SQL Server и, возможно, куб. Кроме того, ".ThenBy ()" работает нормально для меня, и я не вижу никаких проблем с именем, эквивалентным ".ThenBy ()" метода ".OrderBy ()", так как они имеют разные подписи, но если есть проблема, вы можете рассмотреть возможность присвоения ей имени ".ThenGroupBy ()".

Как уже упоминалось, я не использую Linq-to-SQL, но я использую систему провайдера типов F #, которая, как я понимаю, во многих отношениях использует Linq-to-SQL под капотом. Поэтому я попробовал расширение на таком объекте из моего проекта F #, и оно работает, как я и ожидал. Хотя я абсолютно не знаю, означает ли это что-нибудь интересное или нет в этом отношении.

0 голосов
/ 03 февраля 2017

Довольно интересное решение предоставляется здесь

https://blogs.msdn.microsoft.com/mitsu/2007/12/21/playing-with-linq-grouping-groupbymany/

В нем описывается, как выполнить групповую обработку несколькими свойствами. То есть:

var result = customers.GroupByMany(c => c.Country, c => c.City);

В результате вы получите иерархическую структуру, которую можно просто преобразовать в плоский список.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...