web-dev-qa-db-ja.com

LINQ to Entitiesで週ごとにグループ化する

ユーザーが作業に費やす時間を入力できるアプリケーションがあります。LINQto Entitiesを活用するために、このための優れたレポートを作成しようとしています。各TrackedTimeにはTargetDateがあり、これはDateTimeの「日付」部分にすぎないため、ユーザーと日付で時間をグループ化することは比較的簡単です(私は残します)簡単にするために「where」節を除外します):

var userTimes = from t in context.TrackedTimes
                group t by new {t.User.UserName, t.TargetDate} into ut
                select new
                {
                    UserName = ut.Key.UserName,
                    TargetDate = ut.Key.TargetDate,
                    Minutes = ut.Sum(t => t.Minutes)
                };

DateTime.Monthプロパティのおかげで、ユーザーと月によるグループ化は少しだけ複雑になります。

var userTimes = from t in context.TrackedTimes
                group t by new {t.User.UserName, t.TargetDate.Month} into ut
                select new
                {
                    UserName = ut.Key.UserName,
                    MonthNumber = ut.Key.Month,
                    Minutes = ut.Sum(t => t.Minutes)
                };

ここでトリッキーな部分がやって来ます。週ごとにグループ化する信頼できる方法はありますか?同様のLINQ to SQL質問に対して この応答 に基づいて次のことを試しました:

DateTime firstDay = GetFirstDayOfFirstWeekOfYear();
var userTimes = from t in context.TrackedTimes
                group t by new {t.User.UserName, WeekNumber = (t.TargetDate - firstDay).Days / 7} into ut
                select new
                {
                    UserName = ut.Key.UserName,
                    WeekNumber = ut.Key.WeekNumber,
                    Minutes = ut.Sum(t => t.Minutes)
                };

ただし、LINQ to Entitiesは、DateTimeオブジェクトの算術演算をサポートしていないようで、(t.TargetDate - firstDay).Days / 7の方法がわかりません。

数日から数週間をマッピングするだけのデータベース内のビューを作成し、そのビューをEntity Frameworkコンテキストに追加して、LINQクエリでそれに結合することを検討しましたが、これはこのような何かのための多くの作業のようです。算術アプローチの良い回避策はありますか? Linq to Entitiesで機能するもので、データベースに触れる必要なくLINQステートメントに簡単に組み込むことができますか? Linq to Entitiesにある日付から別の日付を減算する方法を伝える方法はありますか?

概要

皆様の心のこもったクリエイティブな対応に感謝いたします。これまで何度も繰り返してきたように、この質問に対する本当の答えは「.NET 4.0まで待つ」のようです。 LINQを活用する最も実用的な答えを提供してくれたNoldorinに賞金を与えます。特に、データベース側で変更を加えることなくEntity Frameworkを使用する応答を考え出したJacob Proffittに言及します。他にもすばらしい回答がありました。質問を初めて見る場合は、投票されたすべての質問とそのコメントを読むことを強くお勧めします。これは本当にStackOverflowの威力の素晴らしい例です。皆さん、ありがとうございました!

39

AsEnumerable拡張メソッドの呼び出しを使用して、グループ化にLINQ to EntitiesではなくLINQ to Objectsを使用するようにクエリを強制できるはずです。

以下を試してください:

DateTime firstDay = GetFirstDayOfFirstWeekOfYear();
var userTimes = 
    from t in context.TrackedTimes.Where(myPredicateHere).AsEnumerable()
    group t by new {t.User.UserName, WeekNumber = (t.TargetDate - firstDay).Days / 7} into ut
    select new
    {
        UserName = ut.Key.UserName,
        WeekNumber = ut.Key.WeekNumber,
        Minutes = ut.Sum(t => t.Minutes)
    };

これは、少なくともwhere句がLINQ to Entitiesによって実行されることを意味しますが、エンティティが処理するには複雑すぎるgroup句は、LINQ to Objectsによって実行されます。

運が良ければ教えてください。

更新

LINQ to Entitiesを使用して全体を可能にする別の提案を次に示します。

(t.TargetDate.Days - firstDay.Days) / 7

これは単にDateTime減算ではなく整数減算のみが実行されるように演算を拡張するだけです。

現在テストされていないため、動作する場合と動作しない場合があります...

40
Noldorin

SQLServerを使用している限り、LINQでもSQL関数を使用できます。

using System.Data.Objects.SqlClient;

var codes = (from c in _twsEntities.PromoHistory
                         where (c.UserId == userId)
                         group c by SqlFunctions.DatePart("wk", c.DateAdded) into g
                         let MaxDate = g.Max(c => SqlFunctions.DatePart("wk",c.DateAdded))
                         let Count = g.Count()
                         orderby MaxDate
                         select new { g.Key, MaxDate, Count }).ToList();

このサンプルは、このスレッドのMVP Zeeshan Hiraniから採用されたものです。 a MSDN

8
HBlackorby

週ごとにタスクを報告する必要がある時間追跡アプリケーションで、まさにこの問題がありました。私は算術的アプローチを試みましたが、妥当な動作を得ることができませんでした。最終的に、データベースに新しい列を追加し、その中に週番号を入れて、新しい行ごとにこれを計算しました。突然すべての痛みが消えました。私は [〜#〜] dry [〜#〜] を信じているので、これを行うことは嫌いでしたが、進歩する必要がありました。あなたが望む答えではなく、別のユーザーの視点。まともな答えを求めてこれを監視します。

6
Sam Mackrill

奇妙なことに、.NET FrameworkのGetWeekOfYearメソッドはまだ投稿されていません。

最初の選択を行い、ダミーの週番号プロパティを追加してから、それらすべてをループし、このメソッドを使用してタイプに正しい週番号を設定します。

public static int GetISOWeek(DateTime day)
{
  return System.Globalization.CultureInfo.CurrentCulture.Calendar.GetWeekOfYear(day, System.Globalization.CalendarWeekRule.FirstFourDayWeek, DayOfWeek.Monday);
}
2
Colin

これは可能ですが、巨大なPITAであり、Entity FrameworkのLINQ部分をバイパスします。ある程度の調査が必要でしたが、SqlServerプロバイダー固有の関数SqlServer.DateDiff()と「Entity SQL」を使用すると、希望どおりの結果を得ることができます。つまり、文字列クエリに戻り、カスタムオブジェクトを手動で入力しますが、必要なことは実行されます。あなたはforeachループなどの代わりにSelectを使用することでこれを少し洗練することができるかもしれませんが、私は実際にこれをデザイナーが生成したYourEntitiesのエンティティセットで動作するようにしました。

void getTimes()
{
    YourEntities context = new YourEntities();
    DateTime firstDay = GetFirstDayOfFirstWeekOfYear();
    var weeks = context.CreateQuery<System.Data.Common.DbDataRecord>(
        "SELECT t.User.UserName, SqlServer.DateDiff('DAY', @beginDate, t.TargetDate)/7 AS WeekNumber " +
        "FROM YourEntities.TrackedTimes AS t " +
        "GROUP BY SqlServer.DateDiff('DAY', @beginDate, t.TargetDate)/7, t.User.UserName", new System.Data.Objects.ObjectParameter("beginDate", firstDay));
    List<customTimes> times = new List<customTimes>();
    foreach (System.Data.Common.DbDataRecord rec in weeks)
    {
        customTimes time = new customTimes()
        {
            UserName = rec.GetString(0),
            WeekNumber = rec.GetInt32(1)
        };
        times.Add(time);
    }
}

class customTimes
{
    public string UserName{ get; set; }
    public int WeekNumber { get; set; }
}
2
Jacob Proffitt

私はLinq to Entitiesを使用していませんが、.Year、.Day .Week、整数除算、モジュロをサポートしている場合は、Zellersアルゴリズム/合同法を使用して週番号を計算できるはずです。

詳細は http://en.wikipedia.org/wiki/Zeller 's_congruenceを参照してください

努力するに値するかどうか、私はあなた/他に任せます。

編集:

ウィキペディアが間違っているか、私が持っているフォーラムラがゼラーのものではないか、名前があるかどうかはわかりませんが、ここでは

 weekno = (( 1461 * ( t.TransactionDate.Year + 4800 
           + ( t.TransactionDate.Month - 14 ) / 12 ) ) / 4
           +  ( 367 * ( t.TransactionDate.Month - 2 - 12 *
                    ( ( t.TransactionDate.Month - 14 ) / 12 ) ) ) / 12
            -   ( 3 * ( ( t.TransactionDate.Year + 4900 
            + ( t.TransactionDate.Month - 14 ) / 12 ) / 100 ) ) / 4 
            +   t.TransactionDate.Day - 32075) / 7 

したがって、グループでこれを使用できるようにする必要があります(これは、週の開始日によっては微調整が必​​要になる場合があります)。

プロビソ。これは、私が「実際に」使用したことがない式です。私は手で入力しました(覚えていませんが、本からコピーした可能性があります)。つまり、かっこが正しいことを3回チェックしたにもかかわらず、ソースが間違っている可能性があります。したがって、使用する前に動作することを確認する必要があります。

個人的には、データ量が大量でない限り、このようなものを使用するよりも、データを7倍に戻してローカルでフィルター処理することを好みます(おそらく、私がフォーラムラを使用したことがない理由を説明します)。

2
sgmoore

ここ は、LINQ to Entitiesでサポートされているメソッドのリストです。

私が見る唯一のオプションは、DateTime.MonthとDateTime.Dayから週番号を取得することです。このようなもの:

int yearToQueryIn = ...;
int firstWeekAdjustment = (int)GetDayOfWeekOfFirstDayOfFirstWeekOfYear(yearToQueryIn);

from t in ...
where t.TargetDate.Year == yearToQueryIn
let month = t.TargetDate.Month
let precedingMonthsLength =
    month == 1 ? 0 :
    month == 2 ? 31 :
    month == 3 ? 31+28 :
    month == 4 ? 31+28+31 : 
    ...
let dayOfYear = precedingMonthsLength + t.TargetDate.Day
let week = (dayOfYear + firstWeekAdjustment) / 7
group by ... 
2
Pavel Minaev

私が持っている唯一の問題は、DateTime型の算術演算だと思います。以下の例を試してください。これは、操作をスカラー関数としてDBに保持し、必要なグループ化された結果のみを返します。 TimeSpanの実装は制限されており、(主に)SQL 2008と.Net 3.5 SP1でのみ利用できるため、.Days関数はあなたの側では機能しません ここでの追加の注意

var userTimes = from t in context.TrackedTimes
    group t by new 
    {
        t.User.UserName, 
        WeekNumber = context.WeekOfYear(t.TargetDate)
    } into ut
    select new 
    {
        UserName = ut.Key.UserName,
        WeekNumber = ut.Key.WeekNumber,
        Minutes = ut.Sum(t => t.Minutes)
    };

コンテキストに追加されたデータベース内の関数(context.WeekOfYearとして):

CREATE FUNCTION WeekOfYear(@Date DateTime) RETURNS Int AS 
BEGIN
RETURN (CAST(DATEPART(wk, @Date) AS INT))
END

追加参照: LINQ to SQLシナリオで適切に変換されるサポートされているDateTimeメソッドは次のとおりです

1
Nick Craver

別の解決策があります。毎日グループレコードを作成し、それらを繰り返し処理します。週の開始が表示されたら、合計またはカウントの操作を行います。

これは、週ごとの収入統計を計算するサンプルコードです。

GetDailyIncomeStatisticsDataメソッドは、データベースから毎日データを取得します。

   private async Task<List<IncomeStastistic>> GetWeeklyIncomeStatisticsData(DateTime startDate, DateTime endDate)
    {
        var dailyRecords = await GetDailyIncomeStatisticsData(startDate, endDate);
        var firstDayOfWeek = DateTimeFormatInfo.CurrentInfo == null
            ? DayOfWeek.Sunday
            : DateTimeFormatInfo.CurrentInfo.FirstDayOfWeek;

        var incomeStastistics = new List<IncomeStastistic>();
        decimal weeklyAmount = 0;
        var weekStart = dailyRecords.First().Date;
        var isFirstWeek = weekStart.DayOfWeek == firstDayOfWeek;

        dailyRecords.ForEach(dailyRecord =>
        {
            if (dailyRecord.Date.DayOfWeek == firstDayOfWeek)
            {
                if (!isFirstWeek)
                {
                    incomeStastistics.Add(new IncomeStastistic(weekStart, weeklyAmount));
                }

                isFirstWeek = false;
                weekStart = dailyRecord.Date;
                weeklyAmount = 0;
            }

            weeklyAmount += dailyRecord.Amount;
        });

        incomeStastistics.Add(new IncomeStastistic(weekStart, weeklyAmount));
        return incomeStastistics;
    }
1
Alper Ebicoglu