A better way of loading ContentItems in Orchard

In this post, I want to explain the detail of the IExtendedContentManager service of the ContentExtensions module. The service is aim to improve the performance of loading ContentItem’s in the Orchard CMS. A ContentItem consists of several ContentParts and ContentFields. In most of the cases, there is a database table per ContentPart type. For example, TitleParts are stored in the TitlePartRecord table, BodyParts are stored in the BodyPartRecord and etc. The default IContentManager of the Orchard, generates several SQL requests to load a ContentItem, actually one request per ContentPart. Normally, there are minimum 5 or 6 ContentParts per ContentItem and loading them in this way, especially in case of having a network latency, imposes some unnecessary delay. The IExtendedContentManger only uses one SQL query, to load the ContentItem. Here is the IExtendedContentManager interface.

using Orchard.ContentManagement;
namespace Orchard.ContentExtensions.Services
{
        public interface IExtendedContentManager : IDependency
    {
        ContentItem Get(int id, string contentType);
        ContentItem Get(int id, VersionOptions options, string contentType);
        ContentItem Get(int id, VersionOptions options, QueryHints hints, string contentType);
    }
}

Compare to the IContentManger, the Get methods here have an additional contentType parameter. Using the contentType, ExtendedContentManager can find the ContentParts it should load. In the DefaultContentManger of the Orchard, the Get method don’t have a contentType parameter. The Get method in the DefaultContentManger is implemented as follows:

        public virtual ContentItem Get(int id, VersionOptions options, QueryHints hints) {
        var session = _contentManagerSession();
            ContentItem contentItem;
            ContentItemVersionRecord versionRecord = null;
        // obtain the root records based on version options
        if (options.VersionRecordId != 0) {
        // short-circuit if item held in session
        if (session.RecallVersionRecordId(options.VersionRecordId, out contentItem)) {
        return contentItem;
                }
                versionRecord = _contentItemVersionRepository.Get(options.VersionRecordId);
            }
        else if (session.RecallContentRecordId(id, out contentItem)) {
        // try to reload a previously loaded published content item
        if (options.IsPublished) {
        return contentItem;
                }
                versionRecord = contentItem.VersionRecord;
            }
        else {
        // do a query to load the records in case Get is called directly
        var contentItemVersionRecords = GetManyImplementation(hints,
                    (contentItemCriteria, contentItemVersionCriteria) => {
                        contentItemCriteria.Add(Restrictions.Eq("Id", id));
        if (options.IsPublished) {
                            contentItemVersionCriteria.Add(Restrictions.Eq("Published", true));
                        }
        else if (options.IsLatest) {
                            contentItemVersionCriteria.Add(Restrictions.Eq("Latest", true));
                        }
        else if (options.IsDraft && !options.IsDraftRequired) {
                            contentItemVersionCriteria.Add(
                                Restrictions.And(Restrictions.Eq("Published", false),
                                                Restrictions.Eq("Latest", true)));
                        }
        else if (options.IsDraft || options.IsDraftRequired) {
                            contentItemVersionCriteria.Add(Restrictions.Eq("Latest", true));
                        }
                        contentItemVersionCriteria.SetFetchMode("ContentItemRecord", FetchMode.Eager);
                        contentItemVersionCriteria.SetFetchMode("ContentItemRecord.ContentType", FetchMode.Eager);
                        contentItemVersionCriteria.SetMaxResults(1);
                    });
        if (options.VersionNumber != 0) {
                    versionRecord = contentItemVersionRecords.FirstOrDefault(
                        x => x.Number == options.VersionNumber) ??
                           _contentItemVersionRepository.Get(
                               x => x.ContentItemRecord.Id == id && x.Number == options.VersionNumber);
                }
        else {
                    versionRecord = contentItemVersionRecords.FirstOrDefault();
                }
            }
        // no record means content item is not in db
        if (versionRecord == null) {
        // check in memory
        var record = _contentItemRepository.Get(id);
        if (record == null) {
        return null;
                }
                versionRecord = GetVersionRecord(options, record);
        if (versionRecord == null) {
        return null;
                }
            }
        // return item if obtained earlier in session
        if (session.RecallVersionRecordId(versionRecord.Id, out contentItem)) {
        if (options.IsDraftRequired && versionRecord.Published) {
        return BuildNewVersion(contentItem);
                }
        return contentItem;
            }
        // allocate instance and set record property
            contentItem = New(versionRecord.ContentItemRecord.ContentType.Name);
            contentItem.VersionRecord = versionRecord;
        // store in session prior to loading to avoid some problems with simple circular dependencies
            session.Store(contentItem);
            
        // create a context with a new instance to load            
        var context = new LoadContentContext(contentItem);
        // invoke handlers to acquire state, or at least establish lazy loading callbacks
            Handlers.Invoke(handler => handler.Loading(context), Logger);
            Handlers.Invoke(handler => handler.Loaded(context), Logger);
        // when draft is required and latest is published a new version is appended 
        if (options.IsDraftRequired && versionRecord.Published) {
                contentItem = BuildNewVersion(context.ContentItem);
            }
        return contentItem;
        }
        private IEnumerable<ContentItemVersionRecord> GetManyImplementation(QueryHints hints, Action<ICriteria, ICriteria> predicate) {
        var session = _sessionLocator.Value.For(typeof (ContentItemRecord));
        var contentItemVersionCriteria = session.CreateCriteria(typeof (ContentItemVersionRecord));
        var contentItemCriteria = contentItemVersionCriteria.CreateCriteria("ContentItemRecord");
            predicate(contentItemCriteria, contentItemVersionCriteria);
            
        var contentItemMetadata = session.SessionFactory.GetClassMetadata(typeof (ContentItemRecord));
        var contentItemVersionMetadata = session.SessionFactory.GetClassMetadata(typeof (ContentItemVersionRecord));
        if (hints != QueryHints.Empty) {
        // break apart and group hints by their first segment
        var hintDictionary = hints.Records
                    .Select(hint => new { Hint = hint, Segments = hint.Split('.') })
                    .GroupBy(item => item.Segments.FirstOrDefault())
                    .ToDictionary(grouping => grouping.Key, StringComparer.InvariantCultureIgnoreCase);
        // locate hints that match properties in the ContentItemVersionRecord
        foreach (var hit in contentItemVersionMetadata.PropertyNames.Where(hintDictionary.ContainsKey).SelectMany(key => hintDictionary[key])) {
                    contentItemVersionCriteria.SetFetchMode(hit.Hint, FetchMode.Eager);
                    hit.Segments.Take(hit.Segments.Count() - 1).Aggregate(contentItemVersionCriteria, ExtendCriteria);
                }
        // locate hints that match properties in the ContentItemRecord
        foreach (var hit in contentItemMetadata.PropertyNames.Where(hintDictionary.ContainsKey).SelectMany(key => hintDictionary[key])) {
                    contentItemVersionCriteria.SetFetchMode("ContentItemRecord." + hit.Hint, FetchMode.Eager);
                    hit.Segments.Take(hit.Segments.Count() - 1).Aggregate(contentItemCriteria, ExtendCriteria);
                }
        if (hintDictionary.SelectMany(x => x.Value).Any(x => x.Segments.Count() > 1))
                    contentItemVersionCriteria.SetResultTransformer(new DistinctRootEntityResultTransformer());
            }
            contentItemCriteria.SetCacheable(true);
        return contentItemVersionCriteria.List<ContentItemVersionRecord>();
        }

As can be seen, it first loads ContentItemVersionRecord and ContentItemRecord. The ConentParts don’t load with the ContentItemRecord. They will be loaded in a Lazy mode, at the first time that they will accessed. The overloaded Get method has a parameter of type QueryHint. QueryHint is used to specify the related data that must be loaded with the ContentItemRecord, but it seems that it is not used in the Orcard core modules. If we fill the Records of the QueryHint with the ContentPart record names, the GetManyImplementation method generates a SQL that fetches all of the ContentParts. Yes, it generates the necessary SQL, but it doesn’t return them. The returned value is of type List () and it doesn’t include the ContentParts. I think the developers of the ContentManager had the idea of loading ContentParts with the ContentItem, but they forgot to complete it :D. The solution to get all of the data is simple. We must fill the QueryHint with the ContentPart records, and also we must change the output of the GetManyImplementation in such a way that it includes all of the returned values from database. The simplest solution is implementing the IResultTransformer interface. The IResultTransformer in NHibernate provides a way to change the received data from database. Here is the implementation of the IResultTransformer.

using NHibernate.Transform;
using Orchard.ContentManagement;
using Orchard.ContentManagement.MetaData.Models;
using Orchard.ContentManagement.Records;
using System;
using System.Collections;
using System.Collections.Generic;
namespace Orchard.ContentExtensions.Services
{
        public class ContentItemTransformer : IResultTransformer
    {
        private ContentTypeDefinition contentTypeDefinition;
        private IPartTypeRecordMatchingService partTypeRecordMatchingService;
        private Func<string, ContentItem> createContentItem;
        public ContentItemTransformer(ContentTypeDefinition contentTypeDefinition, IPartTypeRecordMatchingService partTypeGenricsService, Func<string, ContentItem> createContentItem)
        {
        this.createContentItem = createContentItem;
        this.partTypeRecordMatchingService = partTypeGenricsService;
        this.contentTypeDefinition = contentTypeDefinition;
        }
        public IList TransformList(IList collection)
        {
        return collection;
        }
        public object TransformTuple(object[] tuple, string[] aliases)
        {
        return Transform(tuple);
        }
        private ContentItem Transform(object[] tuple)
        {
            ContentItemVersionRecord contentItemVersionRecord = null;
            ContentItemRecord contentItemRecord = null;
            List<ContentPartRecord> contentParts = new List<ContentPartRecord>();
        foreach (var item in tuple)
            {
        if (item is ContentItemRecord)
                {
                    contentItemRecord = item as ContentItemRecord;
                }
        else if (item is ContentItemVersionRecord)
                {
                    contentItemVersionRecord = item as ContentItemVersionRecord;
                }
        else if (item is ContentPartRecord)
                {
                    contentParts.Add(item as ContentPartRecord);
                }
            }
        if (contentItemRecord != null && contentItemVersionRecord != null)
            {
                contentItemVersionRecord.ContentItemRecord = contentItemRecord;
            }
            ContentItem contentItem = this.createContentItem(contentTypeDefinition.Name);
            
        if (contentItemVersionRecord != null)
            {
                contentItem.VersionRecord = contentItemVersionRecord;
            }
            
        foreach (var partRecord in contentParts)
            {
        var partRecordType = partRecord.GetType();
        foreach (var part in contentItem.Parts)
                {
        if (this.partTypeRecordMatchingService.Match(part,partRecord))
                    {
        this.partTypeRecordMatchingService.Set(part, partRecord);
                    }
                }
            }
        return contentItem;
        }
    }
}

And here is the the implementation of the IExtendedContentManager. The source code of the module can be downloaded from here.

using Autofac;
using NHibernate;
using NHibernate.Criterion;
using NHibernate.SqlCommand;
using NHibernate.Transform;
using Orchard.Caching;
using Orchard.ContentManagement;
using Orchard.ContentManagement.Handlers;
using Orchard.ContentManagement.MetaData;
using Orchard.ContentManagement.MetaData.Builders;
using Orchard.ContentManagement.MetaData.Models;
using Orchard.ContentManagement.Records;
using Orchard.Data;
using Orchard.Data.Providers;
using Orchard.Environment.Configuration;
using Orchard.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Orchard.ContentExtensions.Services
{
        public class ExtendedContentManager : IExtendedContentManager
    {
        private readonly IComponentContext _context;
        private readonly IRepository<ContentTypeRecord> _contentTypeRepository;
        private readonly IRepository<ContentItemRecord> _contentItemRepository;
        private readonly IRepository<ContentItemVersionRecord> _contentItemVersionRepository;
        private readonly IContentDefinitionManager _contentDefinitionManager;
        private readonly ICacheManager _cacheManager;
        private readonly IPartTypeRecordMatchingService partTypeRecordMatchingService;
        private readonly Func<IContentManagerSession> _contentManagerSession;
        private readonly Lazy<IContentDisplay> _contentDisplay;
        private readonly Lazy<ISessionLocator> _sessionLocator;
        private readonly Lazy<IEnumerable<IContentHandler>> _handlers;
        private readonly Lazy<IEnumerable<IIdentityResolverSelector>> _identityResolverSelectors;
        private readonly Lazy<IEnumerable<ISqlStatementProvider>> _sqlStatementProviders;
        private readonly ShellSettings _shellSettings;
        private readonly ISignals _signals;
        private IContentManager contentManager;
        private const string Published = "Published";
        private const string Draft = "Draft";
        public ExtendedContentManager(
            IComponentContext context,
            IRepository<ContentTypeRecord> contentTypeRepository,
            IRepository<ContentItemRecord> contentItemRepository,
            IRepository<ContentItemVersionRecord> contentItemVersionRepository,
            IContentDefinitionManager contentDefinitionManager,
            ICacheManager cacheManager,
            Func<IContentManagerSession> contentManagerSession,
            Lazy<IContentDisplay> contentDisplay,
            IPartTypeRecordMatchingService partTypeRecordMatchingService,
            Lazy<ISessionLocator> sessionLocator,
            Lazy<IEnumerable<IContentHandler>> handlers,
            Lazy<IEnumerable<IIdentityResolverSelector>> identityResolverSelectors,
            Lazy<IEnumerable<ISqlStatementProvider>> sqlStatementProviders,
            ShellSettings shellSettings,
            ISignals signals,
            IContentManager contentManager)
        {
        this.partTypeRecordMatchingService = partTypeRecordMatchingService;
        this.contentManager = contentManager;
            _context = context;
            _contentTypeRepository = contentTypeRepository;
            _contentItemRepository = contentItemRepository;
            _contentItemVersionRepository = contentItemVersionRepository;
            _contentDefinitionManager = contentDefinitionManager;
            _cacheManager = cacheManager;
            _contentManagerSession = contentManagerSession;
            _identityResolverSelectors = identityResolverSelectors;
            _sqlStatementProviders = sqlStatementProviders;
            _shellSettings = shellSettings;
            _signals = signals;
            _handlers = handlers;
            _contentDisplay = contentDisplay;
            _sessionLocator = sessionLocator;
            Logger = NullLogger.Instance;
        }
        public ILogger Logger { get; set; }
        public IEnumerable<IContentHandler> Handlers
        {
        get { return _handlers.Value; }
        }
        public virtual ContentItem Get(int id, string contentType)
        {
        return this.Get(id, VersionOptions.Published, contentType);
        }
        public virtual ContentItem Get(int id, VersionOptions options, string contentType)
        {
        return this.Get(id, options, QueryHints.Empty, contentType);
        }
        public virtual ContentItem Get(int id, VersionOptions options, QueryHints hints, string contentType)
        {
        var session = _contentManagerSession();
            ContentItem contentItem;
            ContentItemVersionRecord versionRecord = null;
        var contentTypeDefinition = _contentDefinitionManager.GetTypeDefinition(contentType);
        if (contentTypeDefinition == null)
            {
                contentTypeDefinition = new ContentTypeDefinitionBuilder().Named(contentType).Build();
            }
        if (hints == QueryHints.Empty)
            {
                hints = new QueryHints();
            }
            hints = hints.ExpandRecords(contentTypeDefinition.Parts.Select(c => c.PartDefinition.Name + "Record"));
        // do a query to load the records in case Get is called directly
            contentItem = GetContentItem(hints, contentTypeDefinition,
                (contentItemCriteria, contentItemVersionCriteria) =>
                {
                    contentItemCriteria.Add(Restrictions.Eq("Id", id));
        if (options.IsPublished)
                    {
                        contentItemVersionCriteria.Add(Restrictions.Eq("Published", true));
                    }
        else if (options.IsLatest)
                    {
                        contentItemVersionCriteria.Add(Restrictions.Eq("Latest", true));
                    }
        else if (options.IsDraft && !options.IsDraftRequired)
                    {
                        contentItemVersionCriteria.Add(
                            Restrictions.And(Restrictions.Eq("Published", false),
                                            Restrictions.Eq("Latest", true)));
                    }
        else if (options.IsDraft || options.IsDraftRequired)
                    {
                        contentItemVersionCriteria.Add(Restrictions.Eq("Latest", true));
                    }
        if (options.VersionNumber != default(int))
                    {
                        contentItemVersionCriteria.Add(Restrictions.Eq("VersionNumber", options.VersionNumber));
                    }
                    contentItemVersionCriteria.SetFetchMode("ContentItemRecord", FetchMode.Eager);
                    contentItemVersionCriteria.SetFetchMode("ContentItemRecord.ContentType", FetchMode.Eager);
                    contentItemVersionCriteria.SetMaxResults(1);
                });
        // store in session prior to loading to avoid some problems with simple circular dependencies
            session.Store(contentItem);
        // create a context with a new instance to load            
        var context = new LoadContentContext(contentItem);
        // invoke handlers to acquire state, or at least establish lazy loading callbacks
            Handlers.Invoke(handler => handler.Loading(context), Logger);
            Handlers.Invoke(handler => handler.Loaded(context), Logger);
        // when draft is required and latest is published a new version is appended 
        if (options.IsDraftRequired && versionRecord.Published)
            {
                contentItem = BuildNewVersion(context.ContentItem, contentType);
            }
        return contentItem;
        }
        public virtual ContentItem New(string contentType)
        {
        var contentTypeDefinition = _contentDefinitionManager.GetTypeDefinition(contentType);
        if (contentTypeDefinition == null)
            {
                contentTypeDefinition = new ContentTypeDefinitionBuilder().Named(contentType).Build();
            }
        // create a new kernel for the model instance
        var context = new ActivatingContentContext
            {
                ContentType = contentTypeDefinition.Name,
                Definition = contentTypeDefinition,
                Builder = new ContentItemBuilder(contentTypeDefinition)
            };
        // invoke handlers to weld aspects onto kernel
            Handlers.Invoke(handler => handler.Activating(context), Logger);
        var context2 = new ActivatedContentContext
            {
                ContentType = contentType,
                ContentItem = context.Builder.Build()
            };
        // back-reference for convenience (e.g. getting metadata when in a view)
            context2.ContentItem.ContentManager = this.contentManager;
            Handlers.Invoke(handler => handler.Activated(context2), Logger);
        var context3 = new InitializingContentContext
            {
                ContentType = context2.ContentType,
                ContentItem = context2.ContentItem,
            };
            Handlers.Invoke(handler => handler.Initializing(context3), Logger);
        // composite result is returned
        return context3.ContentItem;
        }
        protected virtual ContentItem BuildNewVersion(ContentItem existingContentItem, string contentType)
        {
        var contentItemRecord = existingContentItem.Record;
        // locate the existing and the current latest versions, allocate building version
        var existingItemVersionRecord = existingContentItem.VersionRecord;
        var buildingItemVersionRecord = new ContentItemVersionRecord
            {
                ContentItemRecord = contentItemRecord,
                Latest = true,
                Published = false,
                Data = existingItemVersionRecord.Data,
            };
        var latestVersion = contentItemRecord.Versions.SingleOrDefault(x => x.Latest);
        if (latestVersion != null)
            {
                latestVersion.Latest = false;
                buildingItemVersionRecord.Number = latestVersion.Number + 1;
            }
        else
            {
                buildingItemVersionRecord.Number = contentItemRecord.Versions.Max(x => x.Number) + 1;
            }
            contentItemRecord.Versions.Add(buildingItemVersionRecord);
            _contentItemVersionRepository.Create(buildingItemVersionRecord);
        var buildingContentItem = New(contentType);
            buildingContentItem.VersionRecord = buildingItemVersionRecord;
        var context = new VersionContentContext
            {
                Id = existingContentItem.Id,
                ContentType = existingContentItem.ContentType,
                ContentItemRecord = contentItemRecord,
                ExistingContentItem = existingContentItem,
                BuildingContentItem = buildingContentItem,
                ExistingItemVersionRecord = existingItemVersionRecord,
                BuildingItemVersionRecord = buildingItemVersionRecord,
            };
            Handlers.Invoke(handler => handler.Versioning(context), Logger);
            Handlers.Invoke(handler => handler.Versioned(context), Logger);
        return context.BuildingContentItem;
        }
        private ContentItem GetContentItem(QueryHints hints, ContentTypeDefinition contentTypeDefinition, Action<ICriteria, ICriteria> predicate)
        {
        var session = _sessionLocator.Value.For(typeof(ContentItemRecord));
        var contentItemVersionCriteria = session.CreateCriteria(typeof(ContentItemVersionRecord));
        var contentItemCriteria = contentItemVersionCriteria.CreateCriteria("ContentItemRecord");
            predicate(contentItemCriteria, contentItemVersionCriteria);
        var contentItemMetadata = session.SessionFactory.GetClassMetadata(typeof(ContentItemRecord));
        var contentItemVersionMetadata = session.SessionFactory.GetClassMetadata(typeof(ContentItemVersionRecord));
        if (hints != QueryHints.Empty)
            {
        // break apart and group hints by their first segment
        var hintDictionary = hints.Records
                    .Select(hint => new { Hint = hint, Segments = hint.Split('.') })
                    .GroupBy(item => item.Segments.FirstOrDefault())
                    .ToDictionary(grouping => grouping.Key, StringComparer.InvariantCultureIgnoreCase);
        // locate hints that match properties in the ContentItemVersionRecord
        foreach (var hit in contentItemVersionMetadata.PropertyNames.Where(hintDictionary.ContainsKey).SelectMany(key => hintDictionary[key]))
                {
                    contentItemVersionCriteria.SetFetchMode(hit.Hint, FetchMode.Eager);
                    hit.Segments.Take(hit.Segments.Count() - 1).Aggregate(contentItemVersionCriteria, ExtendCriteria);
                }
        // locate hints that match properties in the ContentItemRecord
        foreach (var hit in contentItemMetadata.PropertyNames.Where(hintDictionary.ContainsKey).SelectMany(key => hintDictionary[key]))
                {
                    contentItemVersionCriteria.SetFetchMode("ContentItemRecord." + hit.Hint, FetchMode.Eager);
                    hit.Segments.Take(hit.Segments.Count() - 1).Aggregate(contentItemCriteria, ExtendCriteria);
                }
        if (hintDictionary.SelectMany(x => x.Value).Any(x => x.Segments.Count() > 1))
                    contentItemVersionCriteria.SetResultTransformer(new DistinctRootEntityResultTransformer());
            }
            contentItemCriteria.SetCacheable(true);
            ContentItemTransformer contentItemTransformer = new ContentItemTransformer(contentTypeDefinition, this.partTypeRecordMatchingService, this.New);
        var returnValue = contentItemVersionCriteria.SetResultTransformer(contentItemTransformer).List<ContentItem>();
        return returnValue.FirstOrDefault();
        }
        private static ICriteria ExtendCriteria(ICriteria criteria, string segment)
        {
        return criteria.GetCriteriaByPath(segment) ?? criteria.CreateCriteria(segment, JoinType.LeftOuterJoin);
        }
        private ContentItemVersionRecord GetVersionRecord(VersionOptions options, ContentItemRecord itemRecord)
        {
        if (options.IsPublished)
            {
        return itemRecord.Versions.FirstOrDefault(
                    x => x.Published) ??
                       _contentItemVersionRepository.Get(
                           x => x.ContentItemRecord == itemRecord && x.Published);
            }
        if (options.IsLatest || options.IsDraftRequired)
            {
        return itemRecord.Versions.FirstOrDefault(
                    x => x.Latest) ??
                       _contentItemVersionRepository.Get(
                           x => x.ContentItemRecord == itemRecord && x.Latest);
            }
        if (options.IsDraft)
            {
        return itemRecord.Versions.FirstOrDefault(
                    x => x.Latest && !x.Published) ??
                       _contentItemVersionRepository.Get(
                           x => x.ContentItemRecord == itemRecord && x.Latest && !x.Published);
            }
        if (options.VersionNumber != 0)
            {
        return itemRecord.Versions.FirstOrDefault(
                    x => x.Number == options.VersionNumber) ??
                       _contentItemVersionRepository.Get(
                           x => x.ContentItemRecord == itemRecord && x.Number == options.VersionNumber);
            }
        return null;
        }
    }
}

No Comments

Post Reply