DateTime 値の比較に関して、C# での Entity Framework (Code First) に問題があります。以下に定義されたクラス Validity (この例では単純化されています) を、時間内に定義された有効性を持つ他のエンティティのスーパークラスとして使用します。
public abstract partial class Validity {
[Key]
public int ID { get; set; }
public DateTime? ValidFrom { get; set; }
public DateTime? ValidTo { get; set; }
/**
* @brief This method builds an IQueryable from another IQueryable,
* with added restriction on ValidityFrom/To
*
* An object's validitiy is defined to
* 1. start at timestamp ValidFrom (=inclusive) and
* 2. to end before ValidTo (=exclusive).
* 3. If ValidFrom or ValidTo is NULL, it means to be "unbounded"
* in start or end time (respectively)
*
**/
public static IQueryable<T> isValidAt<T>(IQueryable<T> query, DateTime time) where T : Validity
{
return query.Where<T>(c =>
(!c.ValidFrom.HasValue || time >= c.ValidFrom) // If ValidFrom != NULL, the given timestamp must be equal or "after" ValidFrom
&& (!c.ValidTo.HasValue || time < c.ValidTo)); // If ValidTo != NULL, the given timestamp must be "before" ValidTo
}
/**
* @brief Shall invalidate the object at timestamp time (implicitly sets validTo attribute).
**/
public void inValidate(DateTime time)
{
ValidTo = time;
}
}
public class Item : Validity {
public string property { get; set; }
}
最後の 3 行には、例として取り上げるクラス "Item" があります。このクエリを見てみましょう。
DateTime requestTime = DateTime.Now;
var items = from n in Validity.isValidAt(db.Items, requestTime)
select n;
このクエリは、"requestTime" で "有効" なクラス Item の戻りオブジェクトのみを返す必要があります。ValidTo == requestTime の場合、Item は「無効」と見なされることに注意してください (ValidFrom から ValidTo までのタイムスパンは -exclusive- ValidTo です。上記のソース コードのコメントを参照してください)。
問題
私は実際に、結果セットの「アイテム」にValidTo == requestTime
. 私はちょうどこれを介してチェックしました
Item i= items.FirstOrDefault();
if ((i.ValidFrom.HasValue && i.ValidFrom > requestTime)
|| (i.ValidTo.HasValue && requestTime >= i.ValidTo)) {
// ... SOME ERROR OUTPUT ...
}
** 注: このエラーはめったに発生するわけではありませんが、ソフトウェアではほぼ常に .inValidate(requestTime); として発生します。多くの場合、オブジェクトを無効にするために呼び出されます。**
LinQ によって生成された SQL クエリを使用して、Microsoft SQL Server Management Studio (Microsoft SQL Server 2008 がバックエンドとして使用されます) を介して手動でチェックしました。@p__linq__0、@p__linq__1 を自分で宣言/設定する必要がありました (どちらも requestTime を意味します)...
DECLARE @p__linq__0 DATETIME
DECLARE @p__linq__1 DATETIME
SET @p__linq__0 = '2012-10-23 15:15:11.473'
SET @p__linq__1 = '2012-10-23 15:15:11.473'
これは実際に期待どおりに機能します。しかし、代わりに「2012-10-23 15:15:11」を値として使用すると、(予想どおり) 間違った結果が返されます。それらは私のプログラムのものと似ています。だから、それが問題だと思います...
データベースでは、「DateTime」にはミリ秒が定義されており、ValidFrom/ValidTo はミリ秒を含めて格納されます。しかし、クエリには何らかの理由でタイムスタンプのミリ秒の部分が含まれていないと思います...変数 requestTime にはミリ秒の値が設定されています。
残念ながら、これを確認するためにクエリで送信された実際の値を確認する方法がわかりません。items.toString()-Method を使用して、プレースホルダーを含む生成された SQL を出力する方法しか知りません。
私は試しました:1. db.Log = Console.Out;
「db.Log」が定義されないというエラーのためにコンパイルされませんでした(また、オートコンプリートは「ログ」を提案しませんでした)。一方、db は DbContext から派生しています。2. 「項目」を ObjectQuery にキャストしてから .ToTraceString() を使用しても機能しません。実行時にプログラムがクラッシュし、キャストが無効であるというエラー メッセージが表示されます。
これが重要な場合: 私は .NET 4.0 と EntityFramework.5.0.0 を使用します。
質問
- 完全な SQL (プレースホルダーの値を含む) をログに記録/出力する方法は?
- その問題をエレガントな方法で修正するにはどうすればよいですか? ...inValidate() で「ValidTo」に割り当てられた「時間」から 1 秒を差し引くだけのハックという意味ではありません。
よろしくお願いします、
ステファン
編集(詳細が見つかりました)
SQLプロファイラーで何が起こるかを確認しましたが、問題ないようです。照会時に、精度の高い (7 桁) タイムスタンプが正しく提供されます。BUT:間違った結果を引き起こす SELECT が得られません。だから私は推測しました:それは何らかのキャッシングに違いありません。db.SaveChanges();
そのため、LINQ クエリの直前に a を配置しました。これで、プロファイラーですべてのクエリを取得できました。
次のコードを試して、データベースのデータ型を変更しました。Slauma によって提案されたとおりです ( https://stackoverflow.com/a/8044310/270591を参照)。
modelBuilder.Entity<Item>().Property(f => f.ValidFrom)
.HasColumnType("datetime2").HasPrecision(3);
modelBuilder.Entity<Item>().Property(f => f.ValidTo)
.HasColumnType("datetime2").HasPrecision(3);
再起動する前にデータベース全体を削除しました...
結果: HasPrecision(x) を使用しても成功しません。ここで、x は 0、3 のいずれかです。(直前に db.SaveChanges() があってもなくても); しかし: x = 7 は db.SaveChanges(); でかなり機能します。クエリの直前...
残念ながら、この問題はまだ存在しています...
現在の回避策
データベース オブジェクトのプロパティに割り当てる前に、DateTime 値に次のメソッドを適用します。DateTime を完全な秒の精度に丸めるだけです(DBで設定しました)。また、これは比較に使用されるすべての DateTime に適用されます。
結果: これは解決策というよりハックです! 誤って直接代入が発生しないように、すべてのセッター メソッドに対してアクセス関数を記述する必要があります。
public static DateTime DateTimeDBRound(DateTime time) {
DateTime t = time;
long fraction = (t.Ticks % TimeSpan.TicksPerSecond);
if (fraction >= TimeSpan.TicksPerSecond / 2)
{
t = t.AddTicks(TimeSpan.TicksPerSecond - fraction);
}
else
{
t = t.AddTicks(-fraction);
}
return t;
}