From 4562f7bb2822f17cca0daa7195379ca69ad2982d Mon Sep 17 00:00:00 2001 From: Smit Patel Date: Tue, 24 Sep 2019 17:12:48 -0700 Subject: [PATCH] Add RelationalCommandCaching based on parameter value nullability Resolves #15892 --- ...ingExpressionVisitor.QueryingEnumerable.cs | 121 +++++++----------- ...xpressionVisitor.RelationalCommandCache.cs | 114 +++++++++++++++++ ...alShapedQueryCompilingExpressionVisitor.cs | 15 ++- 3 files changed, 169 insertions(+), 81 deletions(-) create mode 100644 src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.RelationalCommandCache.cs diff --git a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.QueryingEnumerable.cs b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.QueryingEnumerable.cs index 7e6eb1d62cb..1fa982a1c91 100644 --- a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.QueryingEnumerable.cs +++ b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.QueryingEnumerable.cs @@ -9,7 +9,6 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore.Diagnostics; -using Microsoft.EntityFrameworkCore.Query.SqlExpressions; using Microsoft.EntityFrameworkCore.Storage; namespace Microsoft.EntityFrameworkCore.Query @@ -19,29 +18,23 @@ public partial class RelationalShapedQueryCompilingExpressionVisitor private class QueryingEnumerable : IEnumerable, IAsyncEnumerable { private readonly RelationalQueryContext _relationalQueryContext; - private readonly SelectExpression _selectExpression; + private readonly RelationalCommandCache _relationalCommandCache; + private readonly IReadOnlyList _columnNames; private readonly Func _shaper; - private readonly IQuerySqlGeneratorFactory _querySqlGeneratorFactory; private readonly Type _contextType; private readonly IDiagnosticsLogger _logger; - private readonly ISqlExpressionFactory _sqlExpressionFactory; - private readonly IParameterNameGeneratorFactory _parameterNameGeneratorFactory; public QueryingEnumerable( RelationalQueryContext relationalQueryContext, - IQuerySqlGeneratorFactory querySqlGeneratorFactory, - ISqlExpressionFactory sqlExpressionFactory, - IParameterNameGeneratorFactory parameterNameGeneratorFactory, - SelectExpression selectExpression, + RelationalCommandCache relationalCommandCache, + IReadOnlyList columnNames, Func shaper, Type contextType, IDiagnosticsLogger logger) { _relationalQueryContext = relationalQueryContext; - _querySqlGeneratorFactory = querySqlGeneratorFactory; - _sqlExpressionFactory = sqlExpressionFactory; - _parameterNameGeneratorFactory = parameterNameGeneratorFactory; - _selectExpression = selectExpression; + _relationalCommandCache = relationalCommandCache; + _columnNames = columnNames; _shaper = shaper; _contextType = contextType; _logger = logger; @@ -53,28 +46,25 @@ public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToke private sealed class Enumerator : IEnumerator { - private RelationalDataReader _dataReader; - private int[] _indexMap; - private ResultCoordinator _resultCoordinator; private readonly RelationalQueryContext _relationalQueryContext; - private readonly SelectExpression _selectExpression; + private readonly RelationalCommandCache _relationalCommandCache; + private readonly IReadOnlyList _columnNames; private readonly Func _shaper; - private readonly IQuerySqlGeneratorFactory _querySqlGeneratorFactory; private readonly Type _contextType; private readonly IDiagnosticsLogger _logger; - private readonly ISqlExpressionFactory _sqlExpressionFactory; - private readonly IParameterNameGeneratorFactory _parameterNameGeneratorFactory; + + private RelationalDataReader _dataReader; + private int[] _indexMap; + private ResultCoordinator _resultCoordinator; public Enumerator(QueryingEnumerable queryingEnumerable) { _relationalQueryContext = queryingEnumerable._relationalQueryContext; + _relationalCommandCache = queryingEnumerable._relationalCommandCache; + _columnNames = queryingEnumerable._columnNames; _shaper = queryingEnumerable._shaper; - _selectExpression = queryingEnumerable._selectExpression; - _querySqlGeneratorFactory = queryingEnumerable._querySqlGeneratorFactory; _contextType = queryingEnumerable._contextType; _logger = queryingEnumerable._logger; - _sqlExpressionFactory = queryingEnumerable._sqlExpressionFactory; - _parameterNameGeneratorFactory = queryingEnumerable._parameterNameGeneratorFactory; } public T Current { get; private set; } @@ -89,12 +79,8 @@ public bool MoveNext() { if (_dataReader == null) { - var selectExpression = new ParameterValueBasedSelectExpressionOptimizer( - _sqlExpressionFactory, - _parameterNameGeneratorFactory) - .Optimize(_selectExpression, _relationalQueryContext.ParameterValues); - - var relationalCommand = _querySqlGeneratorFactory.Create().GetCommand(selectExpression); + var relationalCommand = _relationalCommandCache.GetRelationalCommand( + _relationalQueryContext.ParameterValues); _dataReader = relationalCommand.ExecuteReader( @@ -104,28 +90,22 @@ public bool MoveNext() _relationalQueryContext.Context, _relationalQueryContext.CommandLogger)); - if (selectExpression.IsNonComposedFromSql()) + // Non-Composed FromSql + if (_columnNames != null) { - var projection = _selectExpression.Projection.ToList(); var readerColumns = Enumerable.Range(0, _dataReader.DbDataReader.FieldCount) .ToDictionary(i => _dataReader.DbDataReader.GetName(i), i => i, StringComparer.OrdinalIgnoreCase); - _indexMap = new int[projection.Count]; - for (var i = 0; i < projection.Count; i++) + _indexMap = new int[_columnNames.Count]; + for (var i = 0; i < _columnNames.Count; i++) { - if (projection[i].Expression is ColumnExpression columnExpression) + var columnName = _columnNames[i]; + if (!readerColumns.TryGetValue(columnName, out var ordinal)) { - var columnName = columnExpression.Name; - if (columnName != null) - { - if (!readerColumns.TryGetValue(columnName, out var ordinal)) - { - throw new InvalidOperationException(RelationalStrings.FromSqlMissingColumn(columnName)); - } - - _indexMap[i] = ordinal; - } + throw new InvalidOperationException(RelationalStrings.FromSqlMissingColumn(columnName)); } + + _indexMap[i] = ordinal; } } else @@ -191,31 +171,28 @@ public void Dispose() private sealed class AsyncEnumerator : IAsyncEnumerator { - private RelationalDataReader _dataReader; - private int[] _indexMap; - private ResultCoordinator _resultCoordinator; private readonly RelationalQueryContext _relationalQueryContext; - private readonly SelectExpression _selectExpression; + private readonly RelationalCommandCache _relationalCommandCache; + private readonly IReadOnlyList _columnNames; private readonly Func _shaper; - private readonly IQuerySqlGeneratorFactory _querySqlGeneratorFactory; private readonly Type _contextType; private readonly IDiagnosticsLogger _logger; - private readonly ISqlExpressionFactory _sqlExpressionFactory; - private readonly IParameterNameGeneratorFactory _parameterNameGeneratorFactory; private readonly CancellationToken _cancellationToken; + private RelationalDataReader _dataReader; + private int[] _indexMap; + private ResultCoordinator _resultCoordinator; + public AsyncEnumerator( QueryingEnumerable queryingEnumerable, CancellationToken cancellationToken) { _relationalQueryContext = queryingEnumerable._relationalQueryContext; + _relationalCommandCache = queryingEnumerable._relationalCommandCache; + _columnNames = queryingEnumerable._columnNames; _shaper = queryingEnumerable._shaper; - _selectExpression = queryingEnumerable._selectExpression; - _querySqlGeneratorFactory = queryingEnumerable._querySqlGeneratorFactory; _contextType = queryingEnumerable._contextType; _logger = queryingEnumerable._logger; - _sqlExpressionFactory = queryingEnumerable._sqlExpressionFactory; - _parameterNameGeneratorFactory = queryingEnumerable._parameterNameGeneratorFactory; _cancellationToken = cancellationToken; } @@ -229,12 +206,8 @@ public async ValueTask MoveNextAsync() { if (_dataReader == null) { - var selectExpression = new ParameterValueBasedSelectExpressionOptimizer( - _sqlExpressionFactory, - _parameterNameGeneratorFactory) - .Optimize(_selectExpression, _relationalQueryContext.ParameterValues); - - var relationalCommand = _querySqlGeneratorFactory.Create().GetCommand(selectExpression); + var relationalCommand = _relationalCommandCache.GetRelationalCommand( + _relationalQueryContext.ParameterValues); _dataReader = await relationalCommand.ExecuteReaderAsync( @@ -245,28 +218,22 @@ public async ValueTask MoveNextAsync() _relationalQueryContext.CommandLogger), _cancellationToken); - if (selectExpression.IsNonComposedFromSql()) + // Non-Composed FromSql + if (_columnNames != null) { - var projection = _selectExpression.Projection.ToList(); var readerColumns = Enumerable.Range(0, _dataReader.DbDataReader.FieldCount) .ToDictionary(i => _dataReader.DbDataReader.GetName(i), i => i, StringComparer.OrdinalIgnoreCase); - _indexMap = new int[projection.Count]; - for (var i = 0; i < projection.Count; i++) + _indexMap = new int[_columnNames.Count]; + for (var i = 0; i < _columnNames.Count; i++) { - if (projection[i].Expression is ColumnExpression columnExpression) + var columnName = _columnNames[i]; + if (!readerColumns.TryGetValue(columnName, out var ordinal)) { - var columnName = columnExpression.Name; - if (columnName != null) - { - if (!readerColumns.TryGetValue(columnName, out var ordinal)) - { - throw new InvalidOperationException(RelationalStrings.FromSqlMissingColumn(columnName)); - } - - _indexMap[i] = ordinal; - } + throw new InvalidOperationException(RelationalStrings.FromSqlMissingColumn(columnName)); } + + _indexMap[i] = ordinal; } } else diff --git a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.RelationalCommandCache.cs b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.RelationalCommandCache.cs new file mode 100644 index 00000000000..5d9bb1577c7 --- /dev/null +++ b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.RelationalCommandCache.cs @@ -0,0 +1,114 @@ +// 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. + +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using Microsoft.EntityFrameworkCore.Query.SqlExpressions; +using Microsoft.EntityFrameworkCore.Storage; + +namespace Microsoft.EntityFrameworkCore.Query +{ + public partial class RelationalShapedQueryCompilingExpressionVisitor + { + private class RelationalCommandCache + { + private readonly ConcurrentDictionary _commandCache + = new ConcurrentDictionary(CommandCacheKeyComparer.Instance); + private readonly ISqlExpressionFactory _sqlExpressionFactory; + private readonly IParameterNameGeneratorFactory _parameterNameGeneratorFactory; + private readonly IQuerySqlGeneratorFactory _querySqlGeneratorFactory; + private readonly SelectExpression _selectExpression; + private readonly ParameterValueBasedSelectExpressionOptimizer _parameterValueBasedSelectExpressionOptimizer; + + public RelationalCommandCache( + ISqlExpressionFactory sqlExpressionFactory, + IParameterNameGeneratorFactory parameterNameGeneratorFactory, + IQuerySqlGeneratorFactory querySqlGeneratorFactory, + SelectExpression selectExpression) + { + _sqlExpressionFactory = sqlExpressionFactory; + _parameterNameGeneratorFactory = parameterNameGeneratorFactory; + _querySqlGeneratorFactory = querySqlGeneratorFactory; + _selectExpression = selectExpression; + _parameterValueBasedSelectExpressionOptimizer = new ParameterValueBasedSelectExpressionOptimizer( + _sqlExpressionFactory, + _parameterNameGeneratorFactory); + } + + public virtual IRelationalCommand GetRelationalCommand(IReadOnlyDictionary parameters) + { + var key = new CommandCacheKey(parameters); + + if (_commandCache.TryGetValue(key, out var relationalCommand)) + { + return relationalCommand; + } + + var selectExpression = _parameterValueBasedSelectExpressionOptimizer.Optimize(_selectExpression, parameters); + + relationalCommand = _querySqlGeneratorFactory.Create().GetCommand(selectExpression); + + if (ReferenceEquals(selectExpression, _selectExpression)) + { + _commandCache.TryAdd(key, relationalCommand); + } + + return relationalCommand; + } + + private sealed class CommandCacheKeyComparer : IEqualityComparer + { + public static readonly CommandCacheKeyComparer Instance = new CommandCacheKeyComparer(); + + private CommandCacheKeyComparer() + { + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Equals(CommandCacheKey x, CommandCacheKey y) + { + if (x.ParameterValues.Count > 0) + { + foreach (var parameterValue in x.ParameterValues) + { + var value = parameterValue.Value; + + if (!y.ParameterValues.TryGetValue(parameterValue.Key, out var otherValue)) + { + return false; + } + + if (value == null + != (otherValue == null)) + { + return false; + } + + if (value is IEnumerable + && value.GetType() == typeof(object[])) + { + // FromSql parameters must have the same number of elements + return ((object[])value).Length == (otherValue as object[])?.Length; + } + } + } + + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int GetHashCode(CommandCacheKey obj) => 0; + } + + private readonly struct CommandCacheKey + { + public readonly IReadOnlyDictionary ParameterValues; + + public CommandCacheKey(IReadOnlyDictionary parameterValues) + => ParameterValues = parameterValues; + } + } + } +} diff --git a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.cs index c3d72f37af9..323b0fedf35 100644 --- a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Data.Common; +using System.Linq; using System.Linq.Expressions; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Query.SqlExpressions; @@ -55,20 +56,26 @@ protected override Expression VisitShapedQueryExpression(ShapedQueryExpression s dataReaderParameter, resultCoordinatorParameter, IsTracking) .Visit(shaper); + IReadOnlyList columnNames = null; if (selectExpression.IsNonComposedFromSql()) { shaper = new IndexMapInjectingExpressionVisitor(indexMapParameter).Visit(shaper); + columnNames = selectExpression.Projection.Select(pe => ((ColumnExpression)pe.Expression).Name).ToList(); } + var relationalCommandCache = new RelationalCommandCache( + RelationalDependencies.SqlExpressionFactory, + RelationalDependencies.ParameterNameGeneratorFactory, + RelationalDependencies.QuerySqlGeneratorFactory, + selectExpression); + var shaperLambda = (LambdaExpression)shaper; return Expression.New( typeof(QueryingEnumerable<>).MakeGenericType(shaperLambda.ReturnType).GetConstructors()[0], Expression.Convert(QueryCompilationContext.QueryContextParameter, typeof(RelationalQueryContext)), - Expression.Constant(RelationalDependencies.QuerySqlGeneratorFactory), - Expression.Constant(RelationalDependencies.SqlExpressionFactory), - Expression.Constant(RelationalDependencies.ParameterNameGeneratorFactory), - Expression.Constant(selectExpression), + Expression.Constant(relationalCommandCache), + Expression.Constant(columnNames, typeof(IReadOnlyList)), Expression.Constant(shaperLambda.Compile()), Expression.Constant(_contextType), Expression.Constant(_logger));