Skip to content

Commit 2ee5d88

Browse files
committed
Query: Introduce AsSplitQuery operator to run collection projection in separate command for relational
Resolves #20892
1 parent c5ca9ff commit 2ee5d88

File tree

34 files changed

+3928
-332
lines changed

34 files changed

+3928
-332
lines changed

src/EFCore.Cosmos/Query/Internal/CosmosQueryMetadataExtractingExpressionVisitor.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,5 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp
4949

5050
return base.VisitMethodCall(methodCallExpression);
5151
}
52-
5352
}
5453
}

src/EFCore.Cosmos/Query/Internal/CosmosQueryTranslationPreprocessor.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,8 @@ public CosmosQueryTranslationPreprocessor(
3939
/// </summary>
4040
public override Expression NormalizeQueryableMethod(Expression query)
4141
{
42-
query = base.NormalizeQueryableMethod(query);
43-
4442
query = new CosmosQueryMetadataExtractingExpressionVisitor(_queryCompilationContext).Visit(query);
43+
query = base.NormalizeQueryableMethod(query);
4544

4645
return query;
4746
}

src/EFCore.Relational/Extensions/RelationalQueryableExtensions.cs

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Data.Common;
77
using System.Linq;
88
using System.Linq.Expressions;
9+
using System.Reflection;
910
using JetBrains.Annotations;
1011
using Microsoft.EntityFrameworkCore.Diagnostics;
1112
using Microsoft.EntityFrameworkCore.Query;
@@ -92,7 +93,7 @@ public static DbCommand CreateDbCommand([NotNull] this IQueryable source)
9293
[StringFormatMethod("sql")]
9394
public static IQueryable<TEntity> FromSqlRaw<TEntity>(
9495
[NotNull] this DbSet<TEntity> source,
95-
[NotNull] [NotParameterized] string sql,
96+
[NotNull][NotParameterized] string sql,
9697
[NotNull] params object[] parameters)
9798
where TEntity : class
9899
{
@@ -133,7 +134,7 @@ public static IQueryable<TEntity> FromSqlRaw<TEntity>(
133134
/// <returns> An <see cref="IQueryable{T}" /> representing the interpolated string SQL query. </returns>
134135
public static IQueryable<TEntity> FromSqlInterpolated<TEntity>(
135136
[NotNull] this DbSet<TEntity> source,
136-
[NotNull] [NotParameterized] FormattableString sql)
137+
[NotNull][NotParameterized] FormattableString sql)
137138
where TEntity : class
138139
{
139140
Check.NotNull(source, nameof(source));
@@ -161,5 +162,31 @@ private static FromSqlQueryRootExpression GenerateFromSqlQueryRoot(
161162
sql,
162163
Expression.Constant(arguments));
163164
}
165+
166+
/// <summary>
167+
/// <para>
168+
/// Returns a new query in which the collections in the query results will be loaded through separate database queries.
169+
/// </para>
170+
/// <para>
171+
/// This strategy fetches all the data from the server through separate database queries before generating any results.
172+
/// </para>
173+
/// </summary>
174+
/// <typeparam name="TEntity"> The type of entity being queried. </typeparam>
175+
/// <param name="source"> The source query. </param>
176+
/// <returns> A new query where collections will be loaded through separate database queries. </returns>
177+
public static IQueryable<TEntity> AsSplitQuery<TEntity>(
178+
[NotNull] this IQueryable<TEntity> source)
179+
where TEntity : class
180+
{
181+
Check.NotNull(source, nameof(source));
182+
183+
return source.Provider is EntityQueryProvider
184+
? source.Provider.CreateQuery<TEntity>(
185+
Expression.Call(AsSplitQueryMethodInfo.MakeGenericMethod(typeof(TEntity)), source.Expression))
186+
: source;
187+
}
188+
189+
internal static readonly MethodInfo AsSplitQueryMethodInfo
190+
= typeof(RelationalQueryableExtensions).GetTypeInfo().GetDeclaredMethod(nameof(AsSplitQuery));
164191
}
165192
}

src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ public override EntityFrameworkServicesBuilder TryAddCoreServices()
169169
TryAdd<IQueryTranslationPreprocessorFactory, RelationalQueryTranslationPreprocessorFactory>();
170170
TryAdd<IRelationalParameterBasedSqlProcessorFactory, RelationalParameterBasedSqlProcessorFactory>();
171171
TryAdd<IRelationalQueryStringFactory, RelationalQueryStringFactory>();
172+
TryAdd<IQueryCompilationContextFactory, RelationalQueryCompilationContextFactory>();
172173

173174
ServiceCollectionMap.GetInfrastructure()
174175
.AddDependencySingleton<RelationalSqlGenerationHelperDependencies>()
@@ -201,7 +202,8 @@ public override EntityFrameworkServicesBuilder TryAddCoreServices()
201202
.AddDependencyScoped<RelationalCompiledQueryCacheKeyGeneratorDependencies>()
202203
.AddDependencyScoped<RelationalConnectionDependencies>()
203204
.AddDependencyScoped<RelationalDatabaseDependencies>()
204-
.AddDependencyScoped<RelationalQueryContextDependencies>();
205+
.AddDependencyScoped<RelationalQueryContextDependencies>()
206+
.AddDependencyScoped<RelationalQueryCompilationContextDependencies>();
205207

206208
return base.TryAddCoreServices();
207209
}

src/EFCore.Relational/Query/Internal/CollectionJoinApplyingExpressionVisitor.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,20 @@ namespace Microsoft.EntityFrameworkCore.Query.Internal
1515
/// </summary>
1616
public class CollectionJoinApplyingExpressionVisitor : ExpressionVisitor
1717
{
18+
private readonly bool _splitQuery;
1819
private int _collectionId;
1920

21+
/// <summary>
22+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
23+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
24+
/// any release. You should only use it directly in your code with extreme caution and knowing that
25+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
26+
/// </summary>
27+
public CollectionJoinApplyingExpressionVisitor(bool splitQuery)
28+
{
29+
_splitQuery = splitQuery;
30+
}
31+
2032
/// <summary>
2133
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
2234
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
@@ -48,7 +60,8 @@ protected override Expression VisitExtension(Expression extensionExpression)
4860
collectionId,
4961
innerShaper,
5062
collectionShaperExpression.Navigation,
51-
collectionShaperExpression.ElementType);
63+
collectionShaperExpression.ElementType,
64+
_splitQuery);
5265
}
5366

5467
return extensionExpression is ShapedQueryExpression shapedQueryExpression

src/EFCore.Relational/Query/Internal/QueryingEnumerable.cs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ public class QueryingEnumerable<T> : IEnumerable<T>, IAsyncEnumerable<T>, IRelat
2323
{
2424
private readonly RelationalQueryContext _relationalQueryContext;
2525
private readonly RelationalCommandCache _relationalCommandCache;
26-
private readonly Func<QueryContext, DbDataReader, ResultContext, ResultCoordinator, T> _shaper;
26+
private readonly Func<QueryContext, DbDataReader, ResultContext, SingleQueryResultCoordinator, T> _shaper;
2727
private readonly Type _contextType;
2828
private readonly IDiagnosticsLogger<DbLoggerCategory.Query> _queryLogger;
2929
private readonly bool _performIdentityResolution;
@@ -37,7 +37,7 @@ public class QueryingEnumerable<T> : IEnumerable<T>, IAsyncEnumerable<T>, IRelat
3737
public QueryingEnumerable(
3838
[NotNull] RelationalQueryContext relationalQueryContext,
3939
[NotNull] RelationalCommandCache relationalCommandCache,
40-
[NotNull] Func<QueryContext, DbDataReader, ResultContext, ResultCoordinator, T> shaper,
40+
[NotNull] Func<QueryContext, DbDataReader, ResultContext, SingleQueryResultCoordinator, T> shaper,
4141
[NotNull] Type contextType,
4242
bool performIdentityResolution)
4343
{
@@ -106,13 +106,13 @@ private sealed class Enumerator : IEnumerator<T>
106106
{
107107
private readonly RelationalQueryContext _relationalQueryContext;
108108
private readonly RelationalCommandCache _relationalCommandCache;
109-
private readonly Func<QueryContext, DbDataReader, ResultContext, ResultCoordinator, T> _shaper;
109+
private readonly Func<QueryContext, DbDataReader, ResultContext, SingleQueryResultCoordinator, T> _shaper;
110110
private readonly Type _contextType;
111111
private readonly IDiagnosticsLogger<DbLoggerCategory.Query> _queryLogger;
112112
private readonly bool _performIdentityResolution;
113113

114114
private RelationalDataReader _dataReader;
115-
private ResultCoordinator _resultCoordinator;
115+
private SingleQueryResultCoordinator _resultCoordinator;
116116
private IExecutionStrategy _executionStrategy;
117117

118118
public Enumerator(QueryingEnumerable<T> queryingEnumerable)
@@ -200,7 +200,7 @@ private bool InitializeReader(DbContext _, bool result)
200200
_relationalQueryContext.Context,
201201
_relationalQueryContext.CommandLogger));
202202

203-
_resultCoordinator = new ResultCoordinator();
203+
_resultCoordinator = new SingleQueryResultCoordinator();
204204

205205
_relationalQueryContext.InitializeStateManager(_performIdentityResolution);
206206

@@ -220,14 +220,14 @@ private sealed class AsyncEnumerator : IAsyncEnumerator<T>
220220
{
221221
private readonly RelationalQueryContext _relationalQueryContext;
222222
private readonly RelationalCommandCache _relationalCommandCache;
223-
private readonly Func<QueryContext, DbDataReader, ResultContext, ResultCoordinator, T> _shaper;
223+
private readonly Func<QueryContext, DbDataReader, ResultContext, SingleQueryResultCoordinator, T> _shaper;
224224
private readonly Type _contextType;
225225
private readonly IDiagnosticsLogger<DbLoggerCategory.Query> _queryLogger;
226226
private readonly bool _performIdentityResolution;
227227
private readonly CancellationToken _cancellationToken;
228228

229229
private RelationalDataReader _dataReader;
230-
private ResultCoordinator _resultCoordinator;
230+
private SingleQueryResultCoordinator _resultCoordinator;
231231
private IExecutionStrategy _executionStrategy;
232232

233233
public AsyncEnumerator(
@@ -319,7 +319,7 @@ private async Task<bool> InitializeReaderAsync(DbContext _, bool result, Cancell
319319
cancellationToken)
320320
.ConfigureAwait(false);
321321

322-
_resultCoordinator = new ResultCoordinator();
322+
_resultCoordinator = new SingleQueryResultCoordinator();
323323

324324
_relationalQueryContext.InitializeStateManager(_performIdentityResolution);
325325

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using JetBrains.Annotations;
5+
using Microsoft.EntityFrameworkCore.Utilities;
6+
using Microsoft.Extensions.DependencyInjection;
7+
8+
namespace Microsoft.EntityFrameworkCore.Query.Internal
9+
{
10+
/// <summary>
11+
/// <para>
12+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
13+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
14+
/// any release. You should only use it directly in your code with extreme caution and knowing that
15+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
16+
/// </para>
17+
/// <para>
18+
/// The service lifetime is <see cref="ServiceLifetime.Scoped" />. This means that each
19+
/// <see cref="DbContext" /> instance will use its own instance of this service.
20+
/// The implementation may depend on other services registered with any lifetime.
21+
/// The implementation does not need to be thread-safe.
22+
/// </para>
23+
/// </summary>
24+
public class RelationalQueryCompilationContextFactory : IQueryCompilationContextFactory
25+
{
26+
private readonly QueryCompilationContextDependencies _dependencies;
27+
private readonly RelationalQueryCompilationContextDependencies _relationalDependencies;
28+
29+
/// <summary>
30+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
31+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
32+
/// any release. You should only use it directly in your code with extreme caution and knowing that
33+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
34+
/// </summary>
35+
public RelationalQueryCompilationContextFactory(
36+
[NotNull] QueryCompilationContextDependencies dependencies,
37+
[NotNull] RelationalQueryCompilationContextDependencies relationalDependencies)
38+
{
39+
Check.NotNull(dependencies, nameof(dependencies));
40+
Check.NotNull(relationalDependencies, nameof(relationalDependencies));
41+
42+
_dependencies = dependencies;
43+
_relationalDependencies = relationalDependencies;
44+
}
45+
46+
/// <summary>
47+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
48+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
49+
/// any release. You should only use it directly in your code with extreme caution and knowing that
50+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
51+
/// </summary>
52+
public virtual QueryCompilationContext Create(bool async)
53+
=> new RelationalQueryCompilationContext(_dependencies, _relationalDependencies, async);
54+
}
55+
}
56+
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System.Linq.Expressions;
5+
using JetBrains.Annotations;
6+
using Microsoft.EntityFrameworkCore.Utilities;
7+
8+
namespace Microsoft.EntityFrameworkCore.Query.Internal
9+
{
10+
/// <summary>
11+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
12+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
13+
/// any release. You should only use it directly in your code with extreme caution and knowing that
14+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
15+
/// </summary>
16+
public class RelationalQueryMetadataExtractingExpressionVisitor : ExpressionVisitor
17+
{
18+
private readonly RelationalQueryCompilationContext _relationalQueryCompilationContext;
19+
20+
/// <summary>
21+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
22+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
23+
/// any release. You should only use it directly in your code with extreme caution and knowing that
24+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
25+
/// </summary>
26+
public RelationalQueryMetadataExtractingExpressionVisitor([NotNull] RelationalQueryCompilationContext relationalQueryCompilationContext)
27+
{
28+
Check.NotNull(relationalQueryCompilationContext, nameof(relationalQueryCompilationContext));
29+
30+
_relationalQueryCompilationContext = relationalQueryCompilationContext;
31+
}
32+
33+
/// <summary>
34+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
35+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
36+
/// any release. You should only use it directly in your code with extreme caution and knowing that
37+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
38+
/// </summary>
39+
protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression)
40+
{
41+
if (methodCallExpression.Method.IsGenericMethod
42+
&& methodCallExpression.Method.GetGenericMethodDefinition() == RelationalQueryableExtensions.AsSplitQueryMethodInfo)
43+
{
44+
var innerQueryable = Visit(methodCallExpression.Arguments[0]);
45+
46+
_relationalQueryCompilationContext.IsSplitQuery = true;
47+
48+
return innerQueryable;
49+
}
50+
51+
return base.VisitMethodCall(methodCallExpression);
52+
}
53+
}
54+
}

src/EFCore.Relational/Query/Internal/CollectionMaterializationContext.cs renamed to src/EFCore.Relational/Query/Internal/SingleQueryCollectionContext.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) .NET Foundation. All rights reserved.
1+
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using JetBrains.Annotations;
@@ -11,16 +11,16 @@ namespace Microsoft.EntityFrameworkCore.Query.Internal
1111
/// any release. You should only use it directly in your code with extreme caution and knowing that
1212
/// doing so can result in application failures when updating to a new Entity Framework Core release.
1313
/// </summary>
14-
public class CollectionMaterializationContext
14+
public class SingleQueryCollectionContext
1515
{
1616
/// <summary>
1717
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
1818
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
1919
/// any release. You should only use it directly in your code with extreme caution and knowing that
2020
/// doing so can result in application failures when updating to a new Entity Framework Core release.
2121
/// </summary>
22-
public CollectionMaterializationContext(
23-
[NotNull] object parent,
22+
public SingleQueryCollectionContext(
23+
[CanBeNull] object parent,
2424
[NotNull] object collection,
2525
[NotNull] object[] parentIdentifier,
2626
[NotNull] object[] outerIdentifier)

0 commit comments

Comments
 (0)