LINQを使用して自分の足で撃たないでください

この記事では、LINQ to SQLを使用する際の非自明なポイントの例をいくつか説明しました。 あなたが.NETの第一人者なら、この退屈なものを見つけるかもしれません。

この例から始めましょう。 「アクションのタイプ」というエンティティがあるとします。 アクションタイプには、人間が読み取れる名前とシステム名があります。これは、コードからこのエンティティのオブジェクトを操作するための一意の識別子です。 コード内のオブジェクトの形でこのような構造を示します。



class ActionType { public int id; public string systemname; public string name; }
      
      





 var ActionTypes = new ActionType[] { new ActionType { id = 1, systemname = "Registration", name = "" }, new ActionType { id = 2, systemname = "LogOn", name = "  " }, new ActionType { id = 3, systemname = null, name = "     " } };
      
      





同様のデータを持つ同じ構造の場合、LINQ to SQLを使用するためにデータベースと補助オブジェクトにテーブルが作成されました。 システム名がNotExistingActionTypeのアクションタイプがあるかどうかを調べる必要があるとします。 問題は、これらの指示に従った後に何が表示されるかです。



 var resultForObjects = ActionTypes.All(actionType => actionType.systemname != "NotExistingActionType"); var context = new LinqForHabr.DataClasses1DataContext(); var resultForLTS = context.ActionTypes.All(actionType => actionType.SystemName != "NotExistingActionType"); Console.WriteLine("Result for objects: " + resultForObjects + "\nResult for Linq to sql: " + resultForLTS); Console.ReadLine();
      
      





この場合の答えは一見奇妙です。 アプリケーションの結果は次のようになります。

オブジェクトの結果:True

LINQ to SQLの結果:False
「同じメソッド」が同じデータに対して異なる値を返すのはなぜですか? 問題は、これはまったく同じメソッドではなく、同じ名前の異なるメソッドであるということです。 1つ目はメモリ内のオブジェクトを反復処理し、2つ目はSQLクエリに変換されます。SQLクエリはサーバーで実行され、別の結果を返します。 アクションのタイプ間で未定義のシステム名を持つものが存在するため、結果は異なります。 そしてこの時点で、2つのランタイムの特定の違いが現れます:.NETの場合、式null!= ObjRefはtrue(もちろんobjRefnullでない限り)であるため、この状況では式「すべてのタイプのアクションのシステム名がNotExistingActionTypeに等しくない」の値はtrueになります。

ただし、LINQ to SQL式はSQLに変換され、サーバーで実行されます。SQLでは、 NULLとの比較の動作が異なります。 式の値はNULL ==何か、NULL!=何か、さらにはNULL == NULLは常にfalseであり、これが標準です。したがって、式「すべてのタイプのアクションのシステム名はNotExistingActionTypeと等しくありません。NULL != 'NotExistingActionType'嘘。



次に別の例を考えてみましょう。 現在の残高を持つユーザーがいるとします:



 class User { public int id; public int balance; public string name; }
      
      





 var users = new User[] { new User { id = 1, name = "", balance = 0 }, new User { id = 2, name = "", balance = 0 } };
      
      





問題は、要素の空のセットで何が返されるかです。 たとえば、私にとって明らかな値は0ですが、ここでもそれほど簡単ではありません。 このようなことをしましょう:



 var resultForObjects = users.Where(user => user.id < 0).Sum(user => user.balance); var context = new LinqForHabr.DataClasses1DataContext(); var resultForLTS = context.Users.Where(user => user.Id < 0).Sum(user => user.Balance); Console.WriteLine("Result for objects: " + resultForObjects + "\nResult for Linq to sql: " + resultForLTS); Console.WriteLine(context.ActionTypes.First().Name); Console.ReadLine();
      
      





結果は再び異常になります。 一般的に、実行中に例外が発生するため、これらの命令をこの形式で実行することはできません。
System.InvalidOperationException:「NULL値を許可しないSystem.Int32型のメンバーにNULL値を割り当てることはできません。」


再び発生する理由は、SQLへの呼び出しを翻訳することです。 通常のIEnumerable拡張機能の場合、 Sumは空のセットに対して0を返します。これは、 resultForLTSを計算しなくても簡単に確認できます(まあ、または最終的には、こちらでmsdn.microsoft.com/en-us/library/bb549046をお読みください )。 ただし、DBMSは空のセットの合計をNULLとして計算し(それが正しいかどうか-質問は全体論的ですが、現在は事実にすぎません)、整数の代わりにNULLを返そうとするLINQはすぐに失敗します。 この場所の修正は非常に簡単ですが、注意してください。



 var resultForLTS = context.Users.Where(user => user.Id < 0).Sum(user => (int?)user.Balance) ?? 0;
      
      





ここで、 Sum関数の戻り値はintではなくnullable int (これは明示的にジェネリック型を指定することで実現できます)になり、LINQはnullを返すことができますが、?? このnullを0に変更します。



さて、最後の例です。 驚くべきことに、SQLに翻訳すると、いくつかの構文上の砂糖が与えられます。 この例を考えてみましょう。 Locationオブジェクトを追加すると、ユーザーは自分の都市へのリンクを取得できます。



 class User { public int id; public int balance; public string name; public Location location; } class Location { public int id; public string Name; }
      
      





Locationオブジェクトを作成したり、ユーザーを変更したりすることはありません。次のコードが重要です。



 var resultForObjects = users.Select(user => user.location == null ? "  " : user.location.Name == null ? "  " : user.location.Name) .First(); var context = new LinqForHabr.DataClasses1DataContext(); var resultForLTS = context.Users.Select(user => user.Location == null ? "  " : user.Location.name == null ? "  " : user.Location.name) .First();
      
      





どちらの場合も、実際には指定されていないため、結果は「Location not specified」という行になりますが、次のように書くとどうなりますか。



 var resultForLTS = context.Users.Select(user => user.Location.name ?? "  ");
      
      





明示的なNullReferenceExceptionがあるため、これは機能しないと思うかもしれません(ユーザーは先験的にLocationオブジェクトを持っていません。作成しませんでした。データベースに書き込みませんでした)。しかし、このコードが環境で実行されないことを忘れません.NET、ただしSQLに変換され、DBMSが起動されます。 実際、このコードからのクエリは次のようになります(LINQPadが役立ちます)。



 SELECT COALESCE([t1].[name],@p0) AS [value] FROM [Users] AS [t0] LEFT OUTER JOIN [Locations] AS [t1] ON [t1].[Id] = [t0].[LocationId]
      
      





この「トリック」により、LINQクエリに無数の三項演算子を記述できなくなります。



結論:



コードを書くとき、私たちは常に低レベルの関数に依存しており、これらの関数が正しく機能すると信じています。 複雑さを軽減するような方法があるのは素晴らしいことですが、使用するこの機能またはその機能が十分に理解しているかどうかを常に認識しておく必要があります。 そしてこのために-RTFM!



All Articles