EntityFrameworkの直接SQL。 強力なタイピングで

こんにちは


今日は、EntityFrameworkについて少しお話します。 ほんの少し。 はい、私はあなたが彼を異なって扱うことができることを知っています、多くは彼に吐き出しますが、より良い代替物がないため、彼らはそれを使い続けます。


だからここに。 ORMが設定されたC#-projectのデータベースへの直接SQLクエリを頻繁に使用しますか? ああ、あきらめないで。 使用します。 さもなければ、バッチでエンティティを削除/更新する方法を実装しますか? そして生き続けた ...


ダイレクトSQLの何が一番好きですか? スピードとシンプルさ。 「ORMの最高の伝統」では、オブジェクトのトレーラをメモリにアンロードし、 context.Remove作成する必要があります。すべての人のためにcontext.Removeます(まあ、またはAttachを操作します)。
ダイレクトSQLで最も嫌いなものは何ですか? そうだね。 タイピングの欠如と爆発の危険。 通常、ダイレクトSQLはDbContext.Database.ExecuteSqlCommandを介してDbContext.Database.ExecuteSqlCommandれ、文字列のみを入力として受け入れます。 そのため、スタジオでの使用状況の検索では、直接SQLが影響するエンティティのフィールドが表示されることはありません。また、特に、感じるすべてのテーブル/列の正確な名前に関してはメモリに依存する必要があります。 また、シェーカーがモデルを掘り下げず、リファクタリング中またはEntityFrameworkツールがスリープ中にすべての名前を変更しないように祈ります。


喜んで、小さな生のSQLクエリの支持者! この記事では、それらをEFと組み合わせ、鉱山の安定性を失わず、起爆装置を生成しない方法を示します。 すぐに猫の下に飛び込みましょう!


そして、正確に何を達成したいのでしょうか?


そのため、この記事では、直接的なSQLが通常EntityFrameworkと連携して引き起こす問題を心配することからあなたを救う素晴らしいアプローチを紹介します。 クエリは人間の外見を取得し、使用状況の検索を通じて検索され、リファクタリング(エンティティ内のフィールドの削除/名前変更)に耐性があります。 足が温まり、潰瘍が溶け、カルマが浄化されます


C#6.0(文字列補間が実装されているもの)、ラムダ式、および少しのストレートハンドが必要です。 この手法を「SQL Stroke」と呼びました。 最終的に、 DbContext拡張メソッドをいくつかDbContext 、厳密に型指定された挿入を含むSQLをデータベースに送信できるようにします。 これを行うには、EntityFrameworkメタデータとやり取りし、ラムダ式を解析し、コース中に発生するすべてのバグとコーナーケースを修正する必要があります。


この記事を読んだ後のダイレクトSQLは次のようになります。


 using (var dc = new MyDbContext()) { //---------- dc.Stroke<Order>(x => $"DELETE FROM {x} WHERE {x.Subtotal} = 0"); // ^ IntelliSense! //---------- var old = DateTime.Today.AddDays(-30); dc.Stroke<Customer>(x => $"UPDATE {x} SET {x.IsActive} = 0 WHERE {x.RegisterDate} < {old}"); //---------- dc.Stroke<Item, Order>((i, o) => $@" UPDATE {i} SET {i.Name} = '[FREE] ' + {i.Name} FROM {i} INNER JOIN {o} ON {i.OrderId} = {o.Id} WHERE {o.Subtotal} = 0" , true); } 

TL; DR要するに、ここはgithubにあります。


ここで.Stroke型パラメーターを使用して.Stroke呼び出すときに、 .Strokeするエンティティ(テーブルにマップされる)を指定することがわかります。 これらは、後続のラムダ式の引数になります。 要するに、 Strokeはパーサーを介して渡されたラムダを渡し、 {x}をテーブルに、 {x.Property}を対応する列名に変換します。


そのようなもの。 それでは、詳細を見てみましょう。


クラスとプロパティのテーブルと列へのマッピング


Reflectionの知識を更新しましょう。クラス(より正確にはType )があり、このクラスのプロパティの名前の行があると想像してください。 EF DbContextます。 これらの2つのフォークとスリッパを使用して、クラスがマップされるテーブルの名前と、ファイルがマップされるデータベース内の列の名前を取得する必要があります。 すぐに予約します。この問題の解決策はEF Coreで異なりますが、これは記事の主要なアイデアには影響しません。 そこで、この問題の解決策を独自に実装/グーグルすることを読者に提案します。


IObjectContextAdapterコンテキストをIObjectContextAdapterキャストするという非常に人気のあるマジックを通じて、 IObjectContextAdapter取得できます。


 public static void GetTableName(this DbContext context, Type t) { //     ObjectContext- var objectContext = ((IObjectContextAdapter)context).ObjectContext; //   var metadata = objectContext.MetadataWorkspace; //        CLR- var objectItemCollection = ((ObjectItemCollection)metadata.GetItemCollection(DataSpace.OSpace)); //      .  EF-    var entityType = metadata.GetItems<EntityType>(DataSpace.OSpace) .FirstOrDefault(x => objectItemCollection.GetClrType(x) == t); //        var container = metadata .GetItems<EntityContainer>(DataSpace.CSpace) .Single() .EntitySets .Single(s => s.ElementType.Name == entityType.Name); //       - var mapping = metadata.GetItems<EntityContainerMapping>(DataSpace.CSSpace) .Single() .EntitySetMappings .Single(s => s.EntitySet == container); // ,      () var tableEntitySet = mapping .EntityTypeMappings.Single() .Fragments.Single() .StoreEntitySet; //    var tableName = tableEntitySet.MetadataProperties["Table"].Value ?? tableEntitySet.Name; //   return tableName; } 

そして、EntityFramework開発者とは何も聞かないでください smoke製 そのような抽象化の迷宮と、あらゆる隅々が意味するものを作成することを意味します。 正直なところ、私自身はこの迷路の中で迷子になります。上に書いたのではなく、 見つけて中傷しました。


それで、テーブルを整理しました。 これで、列の名前。 幸いなことに、エンティティコンテナのマッピングの近くにあります。


 public static void GetTableName(this DbContext context, Type t, string propertyName) { //      ,  var mappings = ... //       var columnName = mapping .EntityTypeMappings.Single() .Fragments.Single() .PropertyMappings .OfType<ScalarPropertyMapping>() .Single(m => m.Property.Name == propertyName) .Column .Name; // ,   ? return columnName; } 

それで、ここで私はすぐに読者大きな文字警告します: EFメタデータの調査は遅いです! ジョークに加えて。 したがって、到達したすべてをキャッシュします。 この記事には私のコードへのリンクがあります-私はすでにキャッシュについて心配していました-あなたはそれを使用することができます。 ただし、心に留めておいてください:EFの実際の概念モデルは、小隊やさまざまなオブジェクトの分割を格納するワンショットモンスターです。 テーブルとタイプ/プロパティのリレーションタイプ名のみが必要な場合(列の名前)、それを取得して1回キャッシュすることをおDbContextします(メモリリークにDbContextしないでくださいDbContextから何も保存しないでDbContext )。 EF Coreでは、これがより良いと彼らは言います。


表現


最も退屈な。 ラムダ式の場合。 次のように呼び出すことができるように、 Strokeメソッドが必要だとします。


 context.Stroke<MyEntity>(x => $"UPDATE {x} WHERE {x.Age} > 10") 

Strokeメソッド自体は単純です。


 public static void Stroke<T>(this DbContext s, Expression<Func<T, string>> stroke) { object[] pars = null; var sql = Parse(context, stroke, out pars); context.Database.ExecuteSqlCommand(sql, pars); } 

これは、すべての主要な作業を行うParseメソッドに基づいています。 ご想像のとおり、このメソッドは文字列補間から取得したラムダ式を解析する必要があります。 文字列のSharpe補間がString.Format構文糖衣であることは秘密ではありません。 したがって、 $"String containing {varA} and {varB}"を記述すると、コンパイラはこの構成をString.Format("String containing {0} and {1}", varA, varB)への呼び出しに変換します。 このメソッドの最初のパラメーターはフォーマット文字列です。 その中で、肉眼で{0}{1}などのプレースホルダーを観察します。 Formatは、これらのプレースホルダーを、プレースホルダー内の数字で示された順序で、フォーマット行の後に来るものと単純に置き換えます。 プレースホルダーが4つ以上ある場合、補間された文字列は、2つのパラメーターからのString.FormatオーバーロードString.Formatコンパイルされます。フォーマット文字列自体と、結果の文字列に入るすべてのパラメーターがパックされる配列です。


それでは、 Parseメソッドで今何をしますか? 元のフォーマット文字列にチェックを入れ、フォーマット引数を再計算し、必要に応じてテーブルと列の名前に置き換えます。 その後、 Format自分で呼び出し、元のフォーマット文字列と処理された引数を結果のSQL文字列に収集します。 正直なところ、説明するよりもコーディングがはるかに簡単です:)


それでは、始めましょう:


 public static string Parse(DbContext context, LambdaExpression query, out object[] parameters){ //       const string err = ",  !"; var bdy = query.Body as MethodCallExpression; //     ? if (bdy == null) throw new Exception(err); //    -  String.Format? if (bdy.Method.DeclaringType != typeof(String) || bdy.Method.Name != "Format") { throw new Exception(err); } 

ご存じのとおり、C#のラムダ式は文字通り式です。 つまり、 =>後に続くものはすべて、ただ1つの式でなければなりません。 演算子をデリゲートに詰め込み、セミコロンで区切ることができます。 しかし、 Expression<>を書くとき-それだけです。 これからは、入力を1つだけの式に制限します。 これは、 Strokeメソッドで発生します。 LambdaExpressionExpression<>祖先ですが、ジェネリックがなければ必要ありません。 したがって、 query含まれる唯一の式がstring.Format呼び出しでstring.Format 、それ以外は何も行わないことを確認する必要がありquery 。 次に、彼がどのような議論を呼んだかを見ていきましょう。 さて、最初の引数で、すべてが明確です-これはフォーマット文字列です。 私たちはすべての誠実な人々の喜びにそれを取ります:


  //     var fmtExpr = bdy.Arguments[0] as ConstantExpression; if (fmtExpr == null) throw new Exception(err); // ...    var format = fmtExpr.Value.ToString(); 

次に、耳で小さなフェイントを作成する必要があります:上記のように、補間された文字列に4つ以上のプレースホルダーがある場合、それはstring.Format呼び出しに変換されますstring.Format番目のパラメーターは配列(2番目はnew [] { ... } ) この状況を処理しましょう:


  //  ,        // 1 -     -   int startingIndex = 1; //    var arguments = bdy.Arguments; bool longFormat = false; //       if (bdy.Arguments.Count == 2) { var secondArg = bdy.Arguments[1]; // ...    - new[] {...} if (secondArg.NodeType == ExpressionType.NewArrayInit) { var array = secondArg as NewArrayExpression; //          arguments = array.Expressions; //   startingIndex = 0; //  ,        longFormat = true; } } 

次に、結果のargumentsコレクションをargumentsて、最後に、ラムダのパラメーターに関連付けられているすべての引数をテーブル/列の名前に変換し、テーブルおよび列への参照ではないすべてのものを計算し、クエリパラメーターのリストにドロップして、パラメーターを残しますformat {i} 、ここでiは対応するパラメーターのインデックスです。 経験豊富なExecuteSqlCommandユーザーにとって新しいものはありません。


  //        //   string.Format List<string> formatArgs = new List<string>(); //   -   List<object> sqlParams = new List<object>(); 

最初にすることは、C#-lambda lambdasの小さな技術的特徴です:厳密な類型化の観点から、たとえばx => "a" + 10を書くと、コンパイラはConvert-型変換(明らかに、文字列)でトップ10をラップします。 基本的に、すべてが正しいのですが、lambdの解析中にこの状況が邪魔になります。 したがって、ここで小さなUnconvertメソッドを作成します。このメソッドは、 Convertでラップするための引数をチェックし、必要に応じて展開します。


 private static Expression Unconvert(Expression ex) { if (ex.NodeType == ExpressionType.Convert) { var cex = ex as UnaryExpression; ex = cex.Operand; } return ex; } 

素晴らしい 次に、次の引数が式のパラメーターに関連しているかどうかを理解する必要があります。 つまり、形式はp.Field1.Field2... 。ここで、 pは式のパラメーター(ラムダ演算子=>前にあるもの)です。 そうでない場合は、この引数を計算し、結果をSQLクエリのパラメーターとして覚えておくだけで、後のEFへの供給が可能になります。 パラメータのフィールドにアクセスしているかどうかを判断する最も簡単で不器用な方法は、次の2つの方法です。


最初に、ルートに到達するまでメンバーの呼び出しのチェーンをループします( GetRootMemberと呼びGetRootMember )。


 private static Expression GetRootMember(MemberExpression expr) { var accessee = expr.Expression as MemberExpression; var current = expr.Expression; while (accessee != null) { accessee = accessee.Expression as MemberExpression; if (accessee != null) current = accessee.Expression; } return current; } 

2番目では、必要な条件を実際にチェックします。


 private static bool IsScopedParameterAccess(Expression expr) { //     -    {x},  ,   if (expr.NodeType == ExpressionType.Parameter) return true; var ex = expr as MemberExpression; //        -   if (ex == null) return false; //     var root = GetRootMember(ex); // ,    if (root == null) return false; //     -  if (root.NodeType != ExpressionType.Parameter) return false; //       return true; } 

できた 引数の列挙に戻ります。


  //  for (int i = startingIndex; i < arguments.Count; i++) { //   Convert var cArg = Unconvert(arguments[i]); //      / if (!IsScopedParameterAccess(cArg)) { //   - var lex = Expression.Lambda(cArg); //  var compiled = lex.Compile(); //  var result = compiled.DynamicInvoke(); //     {i},  i -   formatArgs.Add(string.Format("{{{0}}}", sqlParams.Count)); //     SQL- sqlParams.Add(result); //     continue; } 

素晴らしい。 テーブル/列へのリンクではないことが保証されているすべてのパラメーターを切り取ります。 次に、 sqlParamsのリストがoutパラメーターを介して返さoutます-結果行とともに2番目の引数context.Database.ExecuteSqlCommand sqlParamsます。 それまでの間、テーブルリンクを処理します。


  //   {x},  if (cArg.NodeType == ExpressionType.Parameter) { //     ,    formatArgs.Add(string.Format("[{0}]", context.GetTableName(cArg.Type))) //      continue; } 

ここでは、集約にアクセスする機能を遮断する必要があります。これにより、JOINを使用してリクエストをオーバーシュートする必要が生じますが、これは技術的には不可能です。 ああ-ああ、ああ。 引数がメンバーにアピールするが、式パラメーターのメンバー自体にはアピールしない場合は、お電話ください。


  var argProp = cArg as MemberExpression; if (argProp.Expression.NodeType != ExpressionType.Parameter) { var root = GetRootMember(argProp); throw new Exception(string.Format(",     {0}", root.Type)); } 

そして最後に、列名を取得して、処理された形式引数のリストに追加できます。


  var colId = string.Format("[{0}]", context.GetColumnName(argProp.Member.DeclaringType, argProp.Member.Name)); formatArgs.Add(colId); //     - } 

すべての引数が列挙されたので、ついに自分でstring.Format 、SQL文字列とパラメーターの配列を取得して、 ExecuteSqlCommandを供給する準備ができました。


  var sqlString = string.Format(format, formatArgs.ToArray()); parameters = sqlParams.ToArray(); return sqlString; } 

完了


そのようなもの。 この記事では、コードを意図的に単純化しました。 特に、フルバージョンはテーブルエイリアスを自動的に置き換え、通常はテーブル名と列名をキャッシュし、 .Stroke 8つのパラメーターの.Strokeオーバーロードも含みます。 私のgithubで完全なソースコードを読むことができます。 私はシムに別れを告げ、開発におけるすべての成功をお祈りします。


ああ、まあ、最後の質問:



Source: https://habr.com/ru/post/J347820/


All Articles