Skip to content

Commit

Permalink
Query: Introduce context level configuration for split query
Browse files Browse the repository at this point in the history
- Add enum QuerySplittingBehavior
- Add RelationalDbContextOptionsBuilder.UseQuerySplittingBehavior
- Add AsSingleQuery

Resolves #21355
  • Loading branch information
smitpatel committed Jun 25, 2020
1 parent ad93e5c commit e882272
Show file tree
Hide file tree
Showing 12 changed files with 258 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using System.Reflection;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Query;
using Microsoft.EntityFrameworkCore.Query.Internal;
using Microsoft.EntityFrameworkCore.Utilities;
Expand Down Expand Up @@ -158,12 +159,49 @@ private static FromSqlQueryRootExpression GenerateFromSqlQueryRoot(
Expression.Constant(arguments));
}

/// <summary>
/// <para>
/// Returns a new query in which the collections in the query results will be loaded in single database query.
/// </para>
/// <para>
/// This behavior ensures consistency of the data returned within the constraints of the transaction mode in use.
/// However, this can become very slow when the query will bring back multiple related collections.
/// </para>
/// <para>
/// The default query splitting behavior for queries can be controlled by
/// <see cref="RelationalDbContextOptionsBuilder{TBuilder, TExtension}.UseQuerySplittingBehavior(QuerySplittingBehavior)" />.
/// </para>
/// </summary>
/// <typeparam name="TEntity"> The type of entity being queried. </typeparam>
/// <param name="source"> The source query. </param>
/// <returns> A new query where collections will be loaded through single database query. </returns>
public static IQueryable<TEntity> AsSingleQuery<TEntity>(
[NotNull] this IQueryable<TEntity> source)
where TEntity : class
{
Check.NotNull(source, nameof(source));

return source.Provider is EntityQueryProvider
? source.Provider.CreateQuery<TEntity>(
Expression.Call(AsSingleQueryMethodInfo.MakeGenericMethod(typeof(TEntity)), source.Expression))
: source;
}

internal static readonly MethodInfo AsSingleQueryMethodInfo
= typeof(RelationalQueryableExtensions).GetTypeInfo().GetDeclaredMethod(nameof(AsSingleQuery));

/// <summary>
/// <para>
/// Returns a new query in which the collections in the query results will be loaded through separate database queries.
/// </para>
/// <para>
/// This strategy fetches all the data from the server through separate database queries before generating any results.
/// This behavior can significantly improve performance, but can result in inconsistency in the results returned
/// if the data changes between the two queries. Serializable or snapshot transactions can be used to mitigate this
/// and achieve consistency with split queries, but that may bring other performance costs and behavioral difference.
/// </para>
/// <para>
/// The default query splitting behavior for queries can be controlled by
/// <see cref="RelationalDbContextOptionsBuilder{TBuilder, TExtension}.UseQuerySplittingBehavior(QuerySplittingBehavior)" />.
/// </para>
/// </summary>
/// <typeparam name="TEntity"> The type of entity being queried. </typeparam>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,13 @@ public virtual TBuilder MigrationsHistoryTable([NotNull] string tableName, [CanB
public virtual TBuilder UseRelationalNulls(bool useRelationalNulls = true)
=> WithOption(e => (TExtension)e.WithUseRelationalNulls(useRelationalNulls));

/// <summary>
/// Configures the <see cref="QuerySplittingBehavior"/> to use when loading related collections in a query.
/// </summary>
/// <returns> The same builder instance so that multiple calls can be chained. </returns>
public virtual TBuilder UseQuerySplittingBehavior(QuerySplittingBehavior querySplittingBehavior)
=> WithOption(e => (TExtension)e.WithUseQuerySplittingBehavior(querySplittingBehavior));

/// <summary>
/// Configures the context to use the provided <see cref="IExecutionStrategy" />.
/// </summary>
Expand Down
27 changes: 27 additions & 0 deletions src/EFCore.Relational/Infrastructure/RelationalOptionsExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public abstract class RelationalOptionsExtension : IDbContextOptionsExtension
private int? _maxBatchSize;
private int? _minBatchSize;
private bool _useRelationalNulls;
private QuerySplittingBehavior? _querySplittingBehavior;
private string _migrationsAssembly;
private string _migrationsHistoryTableName;
private string _migrationsHistoryTableSchema;
Expand All @@ -60,6 +61,7 @@ protected RelationalOptionsExtension([NotNull] RelationalOptionsExtension copyFr
_maxBatchSize = copyFrom._maxBatchSize;
_minBatchSize = copyFrom._minBatchSize;
_useRelationalNulls = copyFrom._useRelationalNulls;
_querySplittingBehavior = copyFrom._querySplittingBehavior;
_migrationsAssembly = copyFrom._migrationsAssembly;
_migrationsHistoryTableName = copyFrom._migrationsHistoryTableName;
_migrationsHistoryTableSchema = copyFrom._migrationsHistoryTableSchema;
Expand Down Expand Up @@ -225,6 +227,26 @@ public virtual RelationalOptionsExtension WithUseRelationalNulls(bool useRelatio
return clone;
}

/// <summary>
/// The <see cref="QuerySplittingBehavior"/> to use when loading related collections in a query.
/// </summary>
public virtual QuerySplittingBehavior? QuerySplittingBehavior => _querySplittingBehavior;

/// <summary>
/// Creates a new instance with all options the same as for this instance, but with the given option changed.
/// It is unusual to call this method directly. Instead use <see cref="DbContextOptionsBuilder" />.
/// </summary>
/// <param name="querySplittingBehavior"> The option to change. </param>
/// <returns> A new instance with the option changed. </returns>
public virtual RelationalOptionsExtension WithUseQuerySplittingBehavior(QuerySplittingBehavior querySplittingBehavior)
{
var clone = Clone();

clone._querySplittingBehavior = querySplittingBehavior;

return clone;
}

/// <summary>
/// The name of the assembly that contains migrations, or <see langword="null" /> if none has been set.
/// </summary>
Expand Down Expand Up @@ -417,6 +439,11 @@ public override string LogFragment
builder.Append("UseRelationalNulls ");
}

if (Extension._querySplittingBehavior != null)
{
builder.Append("QuerySplittingBehavior=").Append(Extension._querySplittingBehavior).Append(' ');
}

if (Extension._migrationsAssembly != null)
{
builder.Append("MigrationsAssembly=").Append(Extension._migrationsAssembly).Append(' ');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Linq.Expressions;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
using Microsoft.EntityFrameworkCore.Utilities;

Expand All @@ -16,6 +18,7 @@ namespace Microsoft.EntityFrameworkCore.Query.Internal
public class CollectionJoinApplyingExpressionVisitor : ExpressionVisitor
{
private readonly bool _splitQuery;
private readonly bool _userConfiguredBehavior;
private int _collectionId;

/// <summary>
Expand All @@ -24,9 +27,12 @@ public class CollectionJoinApplyingExpressionVisitor : ExpressionVisitor
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public CollectionJoinApplyingExpressionVisitor(bool splitQuery)
public CollectionJoinApplyingExpressionVisitor([NotNull] RelationalQueryCompilationContext queryCompilationContext)
{
_splitQuery = splitQuery;
Check.NotNull(queryCompilationContext, nameof(queryCompilationContext));

_splitQuery = queryCompilationContext.QuerySplittingBehavior == QuerySplittingBehavior.SplitQuery;
_userConfiguredBehavior = RelationalOptionsExtension.Extract(queryCompilationContext.ContextOptions).QuerySplittingBehavior.HasValue;
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,17 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp
{
var innerQueryable = Visit(methodCallExpression.Arguments[0]);

_relationalQueryCompilationContext.IsSplitQuery = true;
_relationalQueryCompilationContext.QuerySplittingBehavior = QuerySplittingBehavior.SplitQuery;

return innerQueryable;
}

if (methodCallExpression.Method.IsGenericMethod
&& methodCallExpression.Method.GetGenericMethodDefinition() == RelationalQueryableExtensions.AsSingleQueryMethodInfo)
{
var innerQueryable = Visit(methodCallExpression.Arguments[0]);

_relationalQueryCompilationContext.QuerySplittingBehavior = QuerySplittingBehavior.SingleQuery;

return innerQueryable;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,15 @@ public override object GenerateCacheKey(Expression query, bool async)
/// <param name="async"> A value indicating whether the query will be executed asynchronously. </param>
/// <returns> The cache key. </returns>
protected new RelationalCompiledQueryCacheKey GenerateCacheKeyCore([NotNull] Expression query, bool async) // Intentionally non-virtual
=> new RelationalCompiledQueryCacheKey(
{
var relationalOptions = RelationalOptionsExtension.Extract(RelationalDependencies.ContextOptions);

return new RelationalCompiledQueryCacheKey(
base.GenerateCacheKeyCore(query, async),
RelationalOptionsExtension.Extract(RelationalDependencies.ContextOptions).UseRelationalNulls,
relationalOptions.UseRelationalNulls,
relationalOptions.QuerySplittingBehavior,
shouldBuffer: Dependencies.IsRetryingExecutionStrategy);
}

/// <summary>
/// <para>
Expand All @@ -83,19 +88,23 @@ protected readonly struct RelationalCompiledQueryCacheKey
{
private readonly CompiledQueryCacheKey _compiledQueryCacheKey;
private readonly bool _useRelationalNulls;
private readonly QuerySplittingBehavior? _querySplittingBehavior;
private readonly bool _shouldBuffer;

/// <summary>
/// Initializes a new instance of the <see cref="RelationalCompiledQueryCacheKey" /> class.
/// </summary>
/// <param name="compiledQueryCacheKey"> The non-relational cache key. </param>
/// <param name="useRelationalNulls"> True to use relational null logic. </param>
/// <param name="querySplittingBehavior"> <see cref="QuerySplittingBehavior"/> to use when loading related collections. </param>
/// <param name="shouldBuffer"> <see langword="true"/> if the query should be buffered. </param>
public RelationalCompiledQueryCacheKey(
CompiledQueryCacheKey compiledQueryCacheKey, bool useRelationalNulls, bool shouldBuffer)
CompiledQueryCacheKey compiledQueryCacheKey, bool useRelationalNulls,
QuerySplittingBehavior? querySplittingBehavior, bool shouldBuffer)
{
_compiledQueryCacheKey = compiledQueryCacheKey;
_useRelationalNulls = useRelationalNulls;
_querySplittingBehavior = querySplittingBehavior;
_shouldBuffer = shouldBuffer;
}

Expand All @@ -117,6 +126,7 @@ public override bool Equals(object obj)
private bool Equals(RelationalCompiledQueryCacheKey other)
=> _compiledQueryCacheKey.Equals(other._compiledQueryCacheKey)
&& _useRelationalNulls == other._useRelationalNulls
&& _querySplittingBehavior == other._querySplittingBehavior
&& _shouldBuffer == other._shouldBuffer;

/// <summary>
Expand All @@ -125,7 +135,8 @@ private bool Equals(RelationalCompiledQueryCacheKey other)
/// <returns>
/// The hash code for the key.
/// </returns>
public override int GetHashCode() => HashCode.Combine(_compiledQueryCacheKey, _useRelationalNulls, _shouldBuffer);
public override int GetHashCode() => HashCode.Combine(
_compiledQueryCacheKey, _useRelationalNulls, _querySplittingBehavior, _shouldBuffer);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Utilities;

namespace Microsoft.EntityFrameworkCore.Query
Expand Down Expand Up @@ -32,6 +33,8 @@ public RelationalQueryCompilationContext(
Check.NotNull(relationalDependencies, nameof(relationalDependencies));

RelationalDependencies = relationalDependencies;
QuerySplittingBehavior = RelationalOptionsExtension.Extract(ContextOptions).QuerySplittingBehavior
?? QuerySplittingBehavior.SingleQuery;
}

/// <summary>
Expand All @@ -40,8 +43,8 @@ public RelationalQueryCompilationContext(
protected virtual RelationalQueryCompilationContextDependencies RelationalDependencies { get; }

/// <summary>
/// A value indicating if the query should load collections using separate database queries.
/// A value indicating the <see cref="EntityFrameworkCore.QuerySplittingBehavior"/> of the query.
/// </summary>
public virtual bool IsSplitQuery { get; internal set; }
public virtual QuerySplittingBehavior QuerySplittingBehavior { get; internal set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public override Expression Process(Expression query)
{
query = base.Process(query);
query = new SelectExpressionProjectionApplyingExpressionVisitor().Visit(query);
query = new CollectionJoinApplyingExpressionVisitor(((RelationalQueryCompilationContext)QueryCompilationContext).IsSplitQuery).Visit(query);
query = new CollectionJoinApplyingExpressionVisitor((RelationalQueryCompilationContext)QueryCompilationContext).Visit(query);
query = new TableAliasUniquifyingExpressionVisitor().Visit(query);
query = new CaseSimplifyingExpressionVisitor(RelationalDependencies.SqlExpressionFactory).Visit(query);
query = new RelationalValueConverterCompensatingExpressionVisitor(RelationalDependencies.SqlExpressionFactory).Visit(query);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public override Expression Process(Expression query)
{
query = base.Process(query);

return _relationalQueryCompilationContext.IsSplitQuery
return _relationalQueryCompilationContext.QuerySplittingBehavior == QuerySplittingBehavior.SplitQuery
? new SplitIncludeRewritingExpressionVisitor().Visit(query)
: query;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ protected override Expression VisitShapedQuery(ShapedQueryExpression shapedQuery

VerifyNoClientConstant(shapedQueryExpression.ShaperExpression);
var nonComposedFromSql = selectExpression.IsNonComposedFromSql();
var splitQuery = ((RelationalQueryCompilationContext)QueryCompilationContext).IsSplitQuery;
var splitQuery = ((RelationalQueryCompilationContext)QueryCompilationContext).QuerySplittingBehavior == QuerySplittingBehavior.SplitQuery;
var shaper = new ShaperProcessingExpressionVisitor(this, selectExpression, _tags, splitQuery, nonComposedFromSql).ProcessShaper(
shapedQueryExpression.ShaperExpression, out var relationalCommandCache, out var relatedDataLoaders);

Expand Down
34 changes: 34 additions & 0 deletions src/EFCore.Relational/QuerySplittingBehavior.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

namespace Microsoft.EntityFrameworkCore
{
/// <summary>
/// Indicates how the related collections in a query should be loaded from database.
/// </summary>
public enum QuerySplittingBehavior
{
/// <summary>
/// <para>
/// The related collections will be loaded in same database query as parent query.
/// </para>
/// <para>
/// This behavior ensures consistency of the data returned within the constraints of the transaction mode in use.
/// However, this can become very slow when the query will bring back multiple related collections.
/// </para>
/// </summary>
SingleQuery = 0,

/// <summary>
/// <para>
/// The related collections will be loaded in separate database queries from the parent query.
/// </para>
/// <para>
/// This behavior can significantly improve performance, but can result in inconsistency in the results returned
/// if the data changes between the two queries. Serializable or snapshot transactions can be used to mitigate this
/// and achieve consistency with split queries, but that may bring other performance costs and behavioral difference.
/// </para>
/// </summary>
SplitQuery
}
}
Loading

0 comments on commit e882272

Please sign in to comment.