using System; using System.Collections; using System.Collections.Generic; using System.Data.Common; using System.Linq; using NHibernate.Cache; using NHibernate.Engine; using NHibernate.SqlCommand; using NHibernate.Type; using NHibernate.Util; namespace NHibernate.Multi { /// <summary> /// Base class for both ICriteria and IQuery queries /// </summary> public abstract partial class QueryBatchItemBase<TResult> : IQueryBatchItem<TResult>, IQueryBatchItemWithAsyncProcessResults { private static readonly INHibernateLogger Log = NHibernateLogger.For(typeof(QueryBatch)); protected ISessionImplementor Session; private List<EntityKey[]>[] _subselectResultKeys; private List<QueryInfo> _queryInfos; private CacheMode? _cacheMode; private IList<TResult> _finalResults; private DbDataReader _reader; private List<object>[] _hydratedObjects; protected class QueryInfo : ICachingInformation, ICachingInformationWithFetches { /// <summary> /// The query loader. /// </summary> public Loader.Loader Loader { get; set; } /// <summary> /// The query result. /// </summary> public IList Result { get; set; } /// <inheritdoc /> public QueryParameters Parameters { get; } /// <inheritdoc /> public ISet<string> QuerySpaces { get; } //Cache related properties: /// <inheritdoc /> public bool IsCacheable { get; } /// <inheritdoc /> public QueryKey CacheKey { get; } /// <inheritdoc /> public bool CanGetFromCache { get; } // Do not store but forward instead: Loader.ResultTypes can be null initially (if AutoDiscoverTypes // is enabled). /// <inheritdoc /> public IType[] ResultTypes => Loader.ResultTypes; /// <inheritdoc /> public IType[] CacheTypes => Loader.CacheTypes; /// <inheritdoc /> public string QueryIdentifier => Loader.QueryIdentifier; /// <inheritdoc /> public IList ResultToCache { get; set; } /// <summary> /// Indicates if the query result was obtained from the cache. /// </summary> public bool IsResultFromCache { get; private set; } /// <summary> /// Should a result retrieved from database be cached? /// </summary> public bool CanPutToCache { get; } /// <summary> /// The cache batcher to use for entities and collections puts. /// </summary> public CacheBatcher CacheBatcher { get; private set; } /// <summary> /// Create a new <c>QueryInfo</c>. /// </summary> /// <param name="parameters">The query parameters.</param> /// <param name="loader">The loader.</param> /// <param name="querySpaces">The query spaces.</param> /// <param name="session">The session of the query.</param> public QueryInfo( QueryParameters parameters, Loader.Loader loader, ISet<string> querySpaces, ISessionImplementor session) { Parameters = parameters; Loader = loader; QuerySpaces = querySpaces; IsCacheable = loader.IsCacheable(parameters); if (!IsCacheable) return; CacheKey = Loader.GenerateQueryKey(session, Parameters); CanGetFromCache = Parameters.CanGetFromCache(session); CanPutToCache = Parameters.CanPutToCache(session); } /// <inheritdoc /> public void SetCachedResult(IList result) { if (!IsCacheable) throw new InvalidOperationException("Cannot set cached result on a non cacheable query"); if (Result != null) throw new InvalidOperationException("Result is already set"); Result = result; IsResultFromCache = result != null; } /// <inheritdoc /> public void SetCacheBatcher(CacheBatcher cacheBatcher) { CacheBatcher = cacheBatcher; } } protected abstract List<QueryInfo> GetQueryInformation(ISessionImplementor session); /// <inheritdoc /> public IEnumerable<ICachingInformation> CachingInformation { get { ThrowIfNotInitialized(); return _queryInfos; } } /// <inheritdoc /> public virtual void Init(ISessionImplementor session) { Session = session; _queryInfos = GetQueryInformation(session); // Cache and readonly parameters are the same for all translators _cacheMode = _queryInfos.First().Parameters.CacheMode; var count = _queryInfos.Count; _subselectResultKeys = new List<EntityKey[]>[count]; _finalResults = null; } /// <inheritdoc /> public IEnumerable<string> GetQuerySpaces() { return _queryInfos.SelectMany(q => q.QuerySpaces); } /// <inheritdoc /> public IEnumerable<ISqlCommand> GetCommands() { ThrowIfNotInitialized(); foreach (var qi in _queryInfos) { if (qi.IsResultFromCache) continue; yield return qi.Loader.CreateSqlCommand(qi.Parameters, Session); } } /// <inheritdoc /> public int ProcessResultsSet(DbDataReader reader) { ThrowIfNotInitialized(); var dialect = Session.Factory.Dialect; var hydratedObjects = new List<object>[_queryInfos.Count]; var isDebugLog = Log.IsDebugEnabled(); using (Session.SwitchCacheMode(_cacheMode)) { var rowCount = 0; for (var i = 0; i < _queryInfos.Count; i++) { var queryInfo = _queryInfos[i]; var loader = queryInfo.Loader; var queryParameters = queryInfo.Parameters; //Skip processing for items already loaded from cache if (queryInfo.IsResultFromCache) { continue; } var entitySpan = loader.EntityPersisters.Length; hydratedObjects[i] = entitySpan == 0 ? null : new List<object>(entitySpan); var keys = new EntityKey[entitySpan]; var selection = queryParameters.RowSelection; var createSubselects = loader.IsSubselectLoadingEnabled; _subselectResultKeys[i] = createSubselects ? new List<EntityKey[]>() : null; var maxRows = Loader.Loader.HasMaxRows(selection) ? selection.MaxRows : int.MaxValue; var advanceSelection = !dialect.SupportsLimitOffset || !loader.UseLimit(selection, dialect); if (advanceSelection) { Loader.Loader.Advance(reader, selection); } var forcedResultTransformer = queryInfo.CacheKey?.ResultTransformer; if (queryParameters.HasAutoDiscoverScalarTypes) { loader.AutoDiscoverTypes(reader, queryParameters, forcedResultTransformer); } var lockModeArray = loader.GetLockModes(queryParameters.LockModes); var optionalObjectKey = Loader.Loader.GetOptionalObjectKey(queryParameters, Session); var tmpResults = new List<object>(); var queryCacheBuilder = queryInfo.IsCacheable ? new QueryCacheResultBuilder(loader) : null; var cacheBatcher = queryInfo.CacheBatcher; var ownCacheBatcher = cacheBatcher == null; if (ownCacheBatcher) cacheBatcher = new CacheBatcher(Session); if (isDebugLog) Log.Debug("processing result set"); int count; for (count = 0; count < maxRows && reader.Read(); count++) { if (isDebugLog) Log.Debug("result set row: {0}", count); rowCount++; var o = loader.GetRowFromResultSet( reader, Session, queryParameters, lockModeArray, optionalObjectKey, hydratedObjects[i], keys, true, forcedResultTransformer, queryCacheBuilder, (persister, data) => cacheBatcher.AddToBatch(persister, data) ); if (loader.IsSubselectLoadingEnabled) { _subselectResultKeys[i].Add(keys); keys = new EntityKey[entitySpan]; //can't reuse in this case } tmpResults.Add(o); } if (isDebugLog) Log.Debug("done processing result set ({0} rows)", count); queryInfo.Result = tmpResults; if (queryInfo.CanPutToCache) queryInfo.ResultToCache = queryCacheBuilder.Result; if (ownCacheBatcher) cacheBatcher.ExecuteBatch(); reader.NextResult(); } StopLoadingCollections(reader); _reader = reader; _hydratedObjects = hydratedObjects; return rowCount; } } /// <inheritdoc cref="IQueryBatchItem.ProcessResults" /> public void ProcessResults() { ThrowIfNotInitialized(); using (Session.SwitchCacheMode(_cacheMode)) InitializeEntitiesAndCollections(_reader, _hydratedObjects); for (var i = 0; i < _queryInfos.Count; i++) { var queryInfo = _queryInfos[i]; if (_subselectResultKeys[i] != null) { queryInfo.Loader.CreateSubselects(_subselectResultKeys[i], queryInfo.Parameters, Session); } if (queryInfo.IsCacheable) { if (queryInfo.IsResultFromCache) { var queryCacheBuilder = new QueryCacheResultBuilder(queryInfo.Loader); queryInfo.Result = queryCacheBuilder.GetResultList(queryInfo.Result); } // This transformation must not be applied to ResultToCache. queryInfo.Result = queryInfo.Loader.TransformCacheableResults( queryInfo.Parameters, queryInfo.CacheKey.ResultTransformer, queryInfo.Result); } } AfterLoadCallback?.Invoke(GetResults()); } /// <inheritdoc /> public void ExecuteNonBatched() { _finalResults = GetResultsNonBatched(); AfterLoadCallback?.Invoke(_finalResults); } protected abstract IList<TResult> GetResultsNonBatched(); protected List<T> GetTypedResults<T>() { ThrowIfNotInitialized(); if (_queryInfos.Any(qi => qi.Result == null)) { throw new InvalidOperationException("Some query results are missing, batch is likely not fully executed yet."); } var results = new List<T>(_queryInfos.Sum(qi => qi.Result.Count)); foreach (var queryInfo in _queryInfos) { var list = queryInfo.Loader.GetResultList( queryInfo.Result, queryInfo.Parameters.ResultTransformer); ArrayHelper.AddAll(results, list); } return results; } /// <inheritdoc /> public IList<TResult> GetResults() { return _finalResults ?? (_finalResults = DoGetResults()); } /// <inheritdoc /> public Action<IList<TResult>> AfterLoadCallback { get; set; } protected abstract List<TResult> DoGetResults(); private void InitializeEntitiesAndCollections(DbDataReader reader, List<object>[] hydratedObjects) { for (var i = 0; i < _queryInfos.Count; i++) { var queryInfo = _queryInfos[i]; if (queryInfo.IsResultFromCache) continue; queryInfo.Loader.InitializeEntitiesAndCollections( hydratedObjects[i], reader, Session, queryInfo.Parameters.IsReadOnly(Session), queryInfo.CacheBatcher); } } private void StopLoadingCollections(DbDataReader reader) { for (var i = 0; i < _queryInfos.Count; i++) { var queryInfo = _queryInfos[i]; if (queryInfo.IsResultFromCache) continue; queryInfo.Loader.StopLoadingCollections(Session, reader); } } private void ThrowIfNotInitialized() { if (_queryInfos == null) throw new InvalidOperationException( "The query item has not been initialized. A query item must belong to a batch " + $"({nameof(IQueryBatch)}) and the batch must be executed ({nameof(IQueryBatch)}." + $"{nameof(IQueryBatch.Execute)} or {nameof(IQueryBatch)}.{nameof(IQueryBatch.GetResult)}) " + "before retrieving the item result."); } } }