テストでLinqToSqlを使用したSQLでのクエリ変換

私たちは数年前からマーケティングオートメーション製品を製造してきましたが、CI、またはむしろ多数の自動テストが機能を高速にカットするのに役立ちます。



製品には、すべてのカスタマイズを含む約700,000行のコードが含まれており、このために約7,000のテストがあり、その数は常に増え続けています。 そのため、システムの多くの部分に影響を与える大きなリファクタリングを行うことを恐れていません。 しかし、残念ながら、テストは万能薬ではありません。 一部のテストは利用できない場合があり、一部のテストは高価すぎる場合があり、一部の状況はテスト環境で再現できません。



システム内のほぼすべてのトランザクションは、LinqToSqlを使用したMS SQLの操作に関連付けられています。 はい、テクノロジーは古いですが、移行するのは非常に難しく、ビジネスにとっては完全に適しています。 さらに、 書いたように私たちはLinqToSqlの独自のフォークさえ持っています 。そこでは、バグをわずかに修復し、いくつかの機能を追加します。



LinqToSqlを使用してデータベースクエリを作成するには、IQueryableインターフェイスを使用する必要があります。 Enumeratorを受け取ったとき、またはQueryProviderからExecuteを実行したときに、Extensionメソッドを使用して構築された式ツリーがSQLのIQueryableに変換され、SQL Serverで実行されます。



私たちのビジネスロジックはデータベース内のエンティティに強く結び付けられているため、テストはデータベースで多くの機能を果たします。 ただし、テストの95%では、実際のデータベースを使用していません。これは、時間がかかり、InMemoryDatabaseに満足しているためです。 これはテストインフラストラクチャの一部であり、別の記事を書くことができます。実際には、既存の各エンティティタイプの単なる辞書<Type、List>です。 テストでは、UnitOfWorkはこのようなデータベースと透過的に連携し、EnumerableQueryableへのアクセスを許可します。これは、AsQueryable()を呼び出すことでIEnumerableから簡単に取得できます。



何が起こっているのかを理解するためのテストの例を示します。



[TestMethod] public void () { var customer = new CustomerTestDataBuilder(TestDatabase).Build(); using (var modelContext = CreateModelContext()) { var filter = new SegmentFilter<Customer>(null, modelContext) { Segmentation = Controller.PeriodicalSegmentation, Segment = FilterValueWithPresence<Segment>.Concrete(Controller.PeriodicalSegment1) }; var result = modelContext.Repositories.Get<CustomerRepository>().GetFiltered(filter).ToList(); Assert.IsFalse(result.Contains(customer)); } }
      
      





テストでは、modelContext(UnitOfWork)を作成します。UnitOfWorkは、あらゆる種類の便利な機能を備えたDataContextのラッパーであり、それを使用してリポジトリにアクセスし、いくつかのセグメントを除外します。 もちろん、リポジトリはテストを認識していません。ModelContextがInMemoryDatabaseと連携するだけです。 GetFiltered(フィルター)メソッドはIQueryableを形成し、それを具体化します。



このアプローチには問題があります。GetFilteredから取得したIQueryableがSQLに変換されることをテストしません。 その結果、次のようなものの生成に関するバグを取得できます。

[NotSupportedException:メソッド 'Boolean DoesCurrentUserHaveSmsPermissionOnProject(Int32)'には、SQLへのサポートされた変換がありません。]

System.Data.Linq.SqlClient.PostBindDotNetConverter.Visitor.VisitMethodCall(SqlMethodCall mc)で

System.Data.Linq.SqlClient.SqlVisitor.Visit(SqlNodeノード)

System.Data.Linq.SqlClient.SqlVisitor.VisitExpression(SqlExpression exp)で

System.Data.Linq.SqlClient.SqlVisitor.VisitSelectCore(SqlSelect select)で

System.Data.Linq.SqlClient.PostBindDotNetConverter.Visitor.VisitSelect(SqlSelect select)で

...



そのようなバグが本番環境に落ちないようにする方法は? 実際のベースでテストを作成できます。 これらはInMemoryDatabaseで動作するものとそれほど違いはありません。テストクラスは単に異なる親を持ちます。 以下に例を示します。

 [TestMethod] public void () { Controller.CurrentDateTimeUtc = new DateTime(2016, 11, 1, 0, 0, 0, DateTimeKind.Utc); var sessionStartDateTime = Controller.CurrentDateTimeUtc.Value.AddHours(-1); using (var modelContext = CreateModelContext()) { var customer = new CustomerTestDataBuilder(modelContext).Build(); var activeSession = new CustomerSessionTestDataBuilder(modelContext) .WithLastCustomer(customer) .Active() .WithStartDateTimeUtc(sessionStartDateTime) .Build(); modelContext.SubmitTestData(); var newSession = new CustomerSession { PointOfContact = activeSession.PointOfContact, DeviceGuid = activeSession.DeviceGuid, IpAddress = activeSession.IpAddress, ScreenResolution = activeSession.IpAddress, IsAuthenticated = false }; newSession.SetStartDateTimeUtc(modelContext, Controller.CurrentDateTimeUtc.Value, customer); newSession.SetUserAgent(activeSession.UserAgent.UserAgentString, modelContext); newSession.SetLastCustomer(modelContext, customer, copyWebSiteVisitActions: false); modelContext.Repositories.Get<CustomerSessionRepository>().Add(newSession); modelContext.SubmitChanges(); Assert.IsNull(activeSession.IsActiveOrNull); Assert.IsNotNull(newSession.IsActiveOrNull); } }
      
      





このテストでは、すべてが実際のデータベースで発生し、その後のスナップショットトランザクションのロールバックが発生し、そのようなエラーをクロールできません。 しかし、もちろん、そのようなテストは多くはなく、100程度しかありません。 数は7,000と比較されません。また、通常よりはるかに時間がかかります。



ソリューションは、IQueryableの実装を記述し、それに応じてEnumberableQuerybleとSystem.Data.Linq.DataQueryを修飾するIQueryProviderを記述しました。 このような実装は、列挙子を取得するか、クエリの即時実行につながるメソッド(Any、Count、Singleなど)を呼び出してクエリの結果を取得しようとするとき、まずそのようなクエリをブロードキャストできるかどうかを確認する必要がありますSQL。できれば、通常のコレクションで実行します。



次に、これがどのように実装されているかを正確に説明し、そのような翻訳が一般的に機能することをテストします。

 [TestMethod] public void () { var testEntity1 = new SomeTestEntityTestDataBuilder(TestDatabase).Build(); var testEntity2 = new SomeTestEntityTestDataBuilder(TestDatabase).Build(); using (var modelContext = CreateModelContext()) { var query = modelContext.Repositories.Get<SomeTestEntityRepository>().Items.Select(e => e.Id); var sqlQuery = query.ToString(); var expectedQuery = $"SELECT [t0].[{nameof(SomeTestEntity.Id)}]\r\n" + $"FROM [{SomeTestEntity.TableName}] AS [t0]"; Assert.AreEqual(expectedQuery, sqlQuery); var entities = query.ToList(); Assert.AreEqual(2, entities.Count); Assert.IsTrue(entities.Contains(testEntity1.Id)); Assert.IsTrue(entities.Contains(testEntity2.Id)); } }
      
      





このテストと他のいくつかのテストは、SQL変換が実際に機能し、正しく機能していることを確認するために作成されました。 以下に例を示します。

ネタバレの下でのいくつかのテスト
 [TestMethod] public void EntityRefInheritanceMapping() { SomeAbstractTestEntity testEntity1 = new SomeTestEntityChildTestDataBuilder(TestDatabase).WithId(1).Build(); var testEntity2 = new SomeTestEntityTestDataBuilder(TestDatabase).WithId(2).Build(); var anotherTestEntity1 = new AnotherTestEntityTestDataBuilder(TestDatabase).WithLinkedEntity(testEntity1).Build(); var anotherTestEntity2 = new AnotherTestEntityTestDataBuilder(TestDatabase).WithId(3).Build(); using (var modelContext = CreateModelContext()) { var query = modelContext.Repositories.Get<AnotherTestEntityRepository>() .Items .Where(a => a.SomeTestEntity == testEntity1) .Select(a => a.Id); var entities = query.ToList(); Assert.AreEqual(1, entities.Count); Assert.IsTrue(entities.Contains(anotherTestEntity1.Id)); var sqlQuery = query.ToString(); var expectedQuery = $"SELECT [t0].[{nameof(AnotherTestEntity.Id)}]\r\n" + $"FROM [{AnotherTestEntity.TableName}] AS [t0]\r\n" + $"WHERE [t0].[{nameof(AnotherTestEntity.SomeTestEntityId)}] = @p0"; Assert.AreEqual(expectedQuery, sqlQuery); } }
      
      







 [TestMethod] public void SQL() { using (var modelContext = CreateModelContext()) { var query = modelContext.Repositories.Get<SomeTestEntityRepository>().Items.Where(e => e.ToString() == "asdf"); AssertException.Throws<InvalidOperationException>( () => query.ToList(), "ToStringOnlySupportedForPrimitiveTypes"); } }
      
      







最後の例にあるように、SQLに変換されていないIQueryableで列挙しようとすると、テストで例外が発生します。



次に、実装に直接進みます。 モデル内で発生するクエリに関心があります。つまり、実際には、リポジトリへの呼び出しに関心があります。 各エンティティのリポジトリには、特定のビジネスメソッドのセットがあり、Itemsプロパティ(単なるDataTable)を介してIQueryableにアクセスできます。 Itemsプロパティの使用例を見てみましょう。



すべてのリポジトリの基本クラス:

 public abstract class Repository<TEntity> : Repository { private ITable<TEntity> table; public IQueryable<TEntity> Items { get { return table; } } }
      
      





リポジトリ内でアイテムを使用する例

 public class CustomerRepository : ChangeRestrictedRepository<Customer, int, CustomerInitialState> { public List<Customer> GetCustomersByEmail(string email) { if (String.IsNullOrEmpty(email)) throw new ArgumentException("Email  .", nameof(email)); return Items.Where(user => user.Email == email).ToList(); } }
      
      







リポジトリ外での使用例:

 FmcgPurchase = Add(ReverseSingleLinkedItemFilter<CustomerAction, FmcgPurchase>.GetFactory( "fmcgpurchase", modelContext => customerAction => modelContext .Repositories .Get<FmcgPurchaseRepository>() .Items .Where(fmcgPurchase => fmcgPurchase.CustomerAction == customerAction), canLinkedItemBeAbsent: true));
      
      





Repository.ItemsがトリッキーなIQueryableを返すようにする必要があることがわかりました。 さて、トリッキーなIQueryableを書いてください:)



上記のように、Repository.Itemsは実際にITableを返し、UnitOfWorkの作成時にテーブル自体が初期化されます。

 public override void SetRepositoryRegistry(RepositoryRegistry repositories) { table = repositories.DatabaseContext.GetTable<TEntity>(); }
      
      





DatabaseContext.GetTable()メソッドは抽象です。 DatabaseContextには、LinqDatabaseContextとInMemoryDatabaseContextの2つの子孫があります。 実際のデータベースを操作するときに使用されるLinqDatabaseContextでは、すべてが簡単です。GetTableはSystem.Data.Linq.Tableを返します。 InMemoryDatabaseでは、コードは次のように記述されます。

 protected internal override ITable<T> GetTable<T>() { if (!tables.ContainsKey(typeof(T))) tables.Add( typeof(T), new StubTableImpl<T>( this, (InMemoryTable<T>)database.GetTable<T>(), linqToSqlTranslateHelperContext)); return (ITable<T>)tables[typeof(T)]; }
      
      





キャッシュにはちょっとした魔法があり、まだあまり明確ではないlinqToSqlTranslateHelperContextですが、置き換える必要があるIQueryableがStubTableImplであり、database.GetTable()呼び出しも使用されることは既に明らかです。

database.GetTable()から始めましょう。 ここでのポイントは、UnitOfWorkが既に作成されているときにいくつかのリポジトリを参照すると、StubTableが作成されることです。 ただし、テストでは多くのUnitOfWorkが存在する可能性があり、それらはすべて1つのベースで動作するはずです。 データベースはこのベースであり、StubTableはこのデータベースにアクセスするための単なる方法です。



次に、StubTableImplクラスを詳しく見てみましょう。

 public class StubTableImpl<T> : ITable<T>, IStubTable where T : class { internal StubTableImpl( InMemoryDatabaseContext databaseContext, InMemoryTable<T> inMemoryTable, DataContext linqToSqlTranslateHelperContext) { InnerTable = inMemoryTable; innerQueryable = new StubTableQueryable<T>( databaseContext, linqToSqlTranslateHelperContext.GetTable<T>()); } public Type ElementType { get { return innerQueryable.ElementType; } } public Expression Expression { get { return innerQueryable.Expression; } } public IQueryProvider Provider { get { return innerQueryable.Provider; } } Type IStubTable.EntityType { get { return typeof(T); } } public override string ToString() { return innerQueryable.Select(e => e).ToString(); } IEnumerable IStubTable.Items { get { return InnerTable; } } }
      
      





StubTableImplはIQueryableとIQueryProviderを実装し、StubTableQueryable innerQueryableの実装全体を委任します。 StubTableQueryable自体は次のようになります。

 internal class StubTableQueryable<TEntity> : IOrderedQueryable<TEntity> { private readonly InMemoryDatabaseContext inMemoryContext; private readonly IQueryable<TEntity> dataContextQueryable; private readonly StubTableQueryProvider stubTableQueryProvider; public StubTableQueryable( InMemoryDatabaseContext inMemoryContext, IQueryable<TEntity> dataContextQueryable) { this.inMemoryContext = inMemoryContext; this.dataContextQueryable = dataContextQueryable; stubTableQueryProvider = new StubTableQueryProvider(inMemoryContext, dataContextQueryable); } public IEnumerator<TEntity> GetEnumerator() { inMemoryContext.CheckConvertionToSql(Expression); IEnumerable<TEntity> enumerable = new EnumerableQuery<TEntity>(inMemoryContext.ConvertDataContextExpressionToInMemory(Expression)); return enumerable.GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } public Expression Expression { get { return dataContextQueryable.Expression; } } public Type ElementType { get { return dataContextQueryable.ElementType; } } public IQueryProvider Provider { get { return stubTableQueryProvider; } } public override string ToString() { return inMemoryContext.GetQueryText(Expression); } }
      
      





StubTableQueryProviderのコードはすぐに提供します。これらは非常に相互接続されているためです(この1つのクラスを作成することも可能だと思われます)。

 internal class StubTableQueryProvider : IQueryProvider { private static readonly IQueryProvider enumerableQueryProvider = Array.Empty<object>().AsQueryable().Provider; private readonly InMemoryDatabaseContext inMemoryContext; private readonly IQueryable dataContextQueryable; public StubTableQueryProvider( InMemoryDatabaseContext inMemoryContext, IQueryable dataContextQueryable) { this.inMemoryContext = inMemoryContext; this.dataContextQueryable = dataContextQueryable; } public IQueryable<TElement> CreateQuery<TElement>(Expression expression) { return new StubTableQueryable<TElement>( inMemoryContext, dataContextQueryable.Provider.CreateQuery<TElement>(expression)); } public object Execute(Expression expression) { inMemoryContext.CheckConvertionToSql(expression); return enumerableQueryProvider.Execute(inMemoryContext.ConvertDataContextExpressionToInMemory(expression)); } public TResult Execute<TResult>(Expression expression) { inMemoryContext.CheckConvertionToSql(expression); return enumerableQueryProvider.Execute<TResult>(inMemoryContext.ConvertDataContextExpressionToInMemory(expression)); } }
      
      





ここでは、System.LinqのIQueryableで拡張メソッドを使用した式ツリーの構築が一般的にどのように機能するかを説明する必要があります。

これらのメソッド自体は、静的なQueryableクラスで定義されています。 何が起きているのかを理解するためのこのクラスの一部を次に示します。

 public static class Queryable { public static IQueryable<TSource> Where<TSource>(this IQueryable<TSource> source, Expression<Func<TSource, bool>> predicate) { return source.Provider.CreateQuery<TSource>( Expression.Call( null, GetMethodInfo(Queryable.Where, source, predicate), new Expression[] { source.Expression, Expression.Quote(predicate) } )); } public static int Count<TSource>(this IQueryable<TSource> source) { return source.Provider.Execute<int>( Expression.Call( null, GetMethodInfo(Queryable.Count, source), new Expression[] { source.Expression } )); } }
      
      





ここでは、2つのメソッド、WhereとCountの実装例を示しました。 IQueryableインターフェイスとIQueryProviderインターフェイスの相互作用のさまざまな方法を示しているため、私の選択はそれらにかかっていました。

最初に、Whereメソッドの実装を見てみましょう。 このメソッドは、IQueryableおよびフィルター条件を受け入れ、IQueryableを返します。 ただし、この方法では何もフィルタリングされないことが簡単にわかります。 彼が行うことは、式ツリーを作成することだけです。彼は、着信IQueryableから式ツリーを取得し、Whereメソッドへの呼び出し、つまり自分自身をフィルター条件パラメーターで追加します。 その後、結果の新しい式ツリーがIQueryProvider.CreateQueryに渡されます。これは、IQueryableでExpressionをラップするためだけに必要です。



例を解析してみましょう。 このコードがあるとしましょう:

Customers.Where(c => c.Sex == Sex.Male)

同時に顧客-テーブル。 次に、IQueryableがWhereメソッドに渡され、Expression-Tableが含まれます。 その後、Whereは、渡された条件を使用して、この式の最後に自分自身を追加します。 これにより、Table.Where(c => c.Sex == Sex.Male)が生成されます。 次に、この式は自身をIQueryableにラップし、メソッドから戻ります。 データベースへの呼び出しはなく、クリーンな関数呼び出しのみです。



それでは、Countメソッドを見てみましょう。 要求されたコレクションにアクセスするとすぐに、要求されたコレクションの要素の数を計算します。 これは、IQueryProvider.Executeメソッドを呼び出すことで発生します。 このメソッドは、リクエストの作成に基づいてExpressionを受け入れ、このリクエストの結果(数量)を返します。 ここでのExpressionの構築は、Whereメソッドに似ています:ソースIQueryableが取得され、Expressionがそれから取得され、Countによって完了されます。 したがって、IQueryProvider.ExecuteはこのExpressionをバイパスし、必要なものを理解して、データベースに対応する要求を行う必要があります。



ここで、新しい知識を備えて、StubTableQueryableとStubTableQueryProviderに戻ります。 StubTableQueryable.GetEnumeratorおよびStubTableQueryProvider.Executeメソッドを呼び出すとき、ExpressionまたはIQueryableを取得し、DataContextを使用してSQLに変換し、メモリからデータを取得する必要があります。 これを行うために、同様のコードがStubTableQueryProvider.ExecuteおよびStubTableQueryable.GetEnumeratorで記述されています。最初にCheckConvertionToSqlを呼び出し、ConvertDataContextExpressionToInMemoryを使用して元の式を変換し、EnumerableQuerybleで実行するか、Enumerableを呼び出します。



クエリが実際にSQLに変換される方法を確認することから始めましょう。 CheckConversionToSqlメソッドは、DataContext.GetCommandを使用する式でクエリテキストを取得しようとします。 小さな問題は、GetCommandがIQueryableを受け入れ、Expressionを持っていることですが、これは問題ではありません。実際、Expressionのみが必要です:)



その結果、クエリがSQLに変換されることを確認するコードは次のようになります。

 public string GetQueryText(Expression expression) { return queryExpressionToQueryText.GetOrAdd( expression.ToString(), expressionText => { var fakeQueryable = new FakeQueryable(expression); var result = linqToSqlTranslateHelperContext.GetCommand(fakeQueryable); return result.CommandText; }); }
      
      





FakeQueryableクラスはアダプターとして必要です。その実装は次のとおりです。

 public class FakeQueryable : IQueryable { public FakeQueryable(Expression expression) { Expression = expression; } public IEnumerator GetEnumerator() { throw new NotSupportedException(); } public Expression Expression { get; } public Type ElementType { get { throw new NotSupportedException(); } } public IQueryProvider Provider { get { throw new NotSupportedException(); } } }
      
      





GetCommand(Expression)オーバーロードが存在するようにMindbox.Data.Linqを修正する方が正しいでしょうが、これまでのところこれは行われていません。



上記で使用されるlinqToSqlTranslateHelperContextは、その上でGetCommandを呼び出すだけでなく、そこからデータベースに関連付けられたテーブルを取得するためにも使用されるDataContextインスタンスです。 初期クエリは、これらのテーブルに関連して構築されます。 そのようなリクエストを実際に実行しようとすると、リクエストをブロードキャストするためにConnectionは必要ないが、それらを実行するために必要であるため、このDataContextに接続がないという例外が発生します。

ただし、この式からデータを取得する必要があります。 これを行うには、ConvertDataContextExpressionToInMemoryを使用するために少し変換する必要があります。



通常、Expressionsで何かを行うには、ExpressionVisitorから継承する必要があります。ExpressionVisitorでは、各タイプの式に対してオーバーライド可能なメソッドがあり、そこに独自のロジックを記述します。 ExpressionsでLinqToSqlテーブルをInMemoryDatabaseテーブルに置き換えるために、まさにそれを行いました。 このビジターは次のとおりです。

 public class ConstantObjectReplaceExpressionVisitor<T> : ExpressionVisitor where T : class { private readonly Dictionary<T, T> replacementDictionary; public ConstantObjectReplaceExpressionVisitor(Dictionary<T, T> replacementDictionary) { this.replacementDictionary = replacementDictionary; } protected override Expression VisitConstant(ConstantExpression node) { var value = node.Value as T; if (value == null) return base.VisitConstant(node); if (!replacementDictionary.ContainsKey(value)) return base.VisitConstant(node); return Expression.Constant(replacementDictionary[value]); } public Expression ReplaceConstants(Expression sourceExpression) { return Visit(sourceExpression); } }
      
      





このVisitorの意味は、ある定数を別の定数に置き換えることです。 置き換えるものはコンストラクタに渡されます。 すべてのロジックはVisitConstantで記述されており、非常に簡単です。

この訪問者のインスタンスの作成を見てみましょう。
 private ConstantObjectReplaceExpressionVisitor<IQueryable> CreateTableReplaceVisitor(DataContext dataContext) { var dataContextTableToInMemoryTableMap = new Dictionary<IQueryable, IQueryable>(); var entityTypes = ModelApplicationHostController.Instance .ModelConfiguration .DatabaseModel .GetRepositoriesByEntity() .Keys; foreach (var entityType in entityTypes) { var dataContextTable = dataContextGetTableFunc(dataContext, entityType); if (dataContextTable == null) throw new InvalidOperationException($"  {entityType}      DataContext'"); var inMemoryContextTable = GetInMemoryTable(database, entityType); if (inMemoryContextTable == null) throw new InvalidOperationException($"  {entityType}    InMemory "); dataContextTableToInMemoryTableMap.Add(dataContextTable, inMemoryContextTable); } return new ConstantObjectReplaceExpressionVisitor<IQueryable>(dataContextTableToInMemoryTableMap); }
      
      





ここでは、登録されているすべてのタイプのエンティティを確認します。各タイプについて、DataContextからテーブルを取得します。これは、最終ディクショナリのキーになり、InMemoryTableには値になります。 結果として、結果の訪問者はすべてのContantExpressionを置き換えます。ContantExpressionの値は、渡される辞書のキーに存在し、InMemoryTableのエンティティの一部のテーブルに対応します。



このようなパスでは、Tableの定数値ではなく、Tableの値を持つ式を使用する式ツリーに問題があるように思われるかもしれません。 この場合のために書かれたテストは次のとおりです。

 [TestMethod] public void () { var testEntity1 = new SomeTestEntityTestDataBuilder(TestDatabase).WithId(1).Build(); var testEntity2 = new SomeTestEntityTestDataBuilder(TestDatabase).WithId(2).Build(); var anotherTestEntity1 = new AnotherTestEntityTestDataBuilder(TestDatabase).WithId(testEntity1.Id).Build(); var anotherTestEntity2 = new AnotherTestEntityTestDataBuilder(TestDatabase).WithId(3).Build(); using (var modelContext = CreateModelContext()) { var query = modelContext.Repositories.Get<SomeTestEntityRepository>().Items .SelectMany(e => modelContext.Repositories.Get<AnotherTestEntityRepository>() .Items .Where(a => a.Id == e.Id)) .Select(a => a.Id); var entities = query.ToList(); Assert.AreEqual(1, entities.Count); Assert.IsTrue(entities.Contains(anotherTestEntity1.Id)); var sqlQuery = query.ToString(); var expectedQuery = $"SELECT [t1].[{nameof(AnotherTestEntity.Id)}]\r\n" + $"FROM [{SomeTestEntity.TableName}] AS [t0], [{AnotherTestEntity.TableName}] AS [t1]\r\n" + $"WHERE [t1].[{nameof(AnotherTestEntity.Id)}] = [t0].[{nameof(SomeTestEntity.Id)}]"; Assert.AreEqual(expectedQuery, sqlQuery); } }
      
      





ここで、modelContext.Repositories.Get()。Itemsは式ツリーの一部であり、Visitorに置き換えられません。 なぜそのようなテストに合格するのですか? リクエストはどのように正しく変換され、列挙はどのように行われますか?

要求の変換中にLinqToSqlが式ツリーをバイパスし、その中で実際の定数である式を実行するため、このような状況でのクエリ変換は驚くべきではありません。 SQLサーバーでの実行が必要なコンテキストで使用されない場合、C#メソッドの呼び出しはすべて、実際の変換の前に呼び出されます。 そのため、リクエストにmodelContext.Repositories.Get()。Items.Where(a => a.TestNumber == GetSomeTestNumber())を記述できますが、modelContext.Repositories.Get()を記述できません。 .TestNumber == GetSomeTestNumber(a))。 前者の場合、GetSomeTestNumber()の結果は翻訳の段階で計算されてリクエストに代入され、2番目のGetSomeTestNumberは引数としてクエリの本質を取ります。つまり、エンティティに依存するため、翻訳も必要です。 テストでは、modelContext.Repositories.Get()項目が翻訳段階で実行され、リポジトリの項目は式がTableであるStubTableImplを返します。 最も好奇心For盛な人のために、上記で説明したことを実行するコードへのリンクを提供します

リクエストの直接実行に関しては、まだ簡単です。 元のクエリの式で最初の唯一のテーブルを置き換えた後、通常のEnumerableとして実行を開始します。 SelectManyは、デリゲートとして式の一部を実行するだけです。 この実装の一部として、サブクエリをSQLに変換しようとします。もちろん、これはうまくいきます。TableをInMemoryTableに置き換えて、同じ方法で実行します。



このソリューションにはどのような問題がありますか? 主な問題は、この方法では検出されないマッピングのエラーがまだあることです。 IQueryableからのクエリがSQLに変換されるという事実は、LinqToSqlがデータストリームを読み取り、そこからオブジェクトを作成するマテリアライゼーションフェーズが成功することを意味しません。 たとえば、このフェーズでは、Nullを含むことができないエンティティのプロパティにNull値を書き込もうとするためにエラーが発生する場合があります。 , , , : 2 .

, , .



以上です。 :)



All Articles